##// END OF EJS Templates
audit-logs: implemented pull request and comment events.
marcink -
r1807:83e09901 default
parent child Browse files
Show More

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

@@ -1,113 +1,112 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import UserLog
23 from rhodecode.model.db import UserLog
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestClosePullRequest(object):
31 class TestClosePullRequest(object):
32
32
33 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
34 def test_api_close_pull_request(self, pr_util):
34 def test_api_close_pull_request(self, pr_util):
35 pull_request = pr_util.create_pull_request()
35 pull_request = pr_util.create_pull_request()
36 pull_request_id = pull_request.pull_request_id
36 pull_request_id = pull_request.pull_request_id
37 author = pull_request.user_id
37 author = pull_request.user_id
38 repo = pull_request.target_repo.repo_id
38 repo = pull_request.target_repo.repo_id
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'close_pull_request',
40 self.apikey, 'close_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
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 'close_status': 'Rejected',
47 'closed': True,
47 'closed': True,
48 }
48 }
49 assert_ok(id_, expected, response.body)
49 assert_ok(id_, expected, response.body)
50 action = 'user_closed_pull_request:%d' % pull_request_id
51 journal = UserLog.query()\
50 journal = UserLog.query()\
52 .filter(UserLog.user_id == author)\
51 .filter(UserLog.user_id == author) \
52 .order_by('user_log_id') \
53 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.repository_id == repo)\
54 .filter(UserLog.action == action)\
55 .all()
54 .all()
56 assert len(journal) == 1
55 assert journal[-1].action == 'repo.pull_request.close'
57
56
58 @pytest.mark.backends("git", "hg")
57 @pytest.mark.backends("git", "hg")
59 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):
60 pull_request = pr_util.create_pull_request()
59 pull_request = pr_util.create_pull_request()
61 pull_request_id = pull_request.pull_request_id
60 pull_request_id = pull_request.pull_request_id
62 pull_request_repo = pull_request.target_repo.repo_name
61 pull_request_repo = pull_request.target_repo.repo_name
63 PullRequestModel().close_pull_request(
62 PullRequestModel().close_pull_request(
64 pull_request, pull_request.author)
63 pull_request, pull_request.author)
65 id_, params = build_data(
64 id_, params = build_data(
66 self.apikey, 'close_pull_request',
65 self.apikey, 'close_pull_request',
67 repoid=pull_request_repo, pullrequestid=pull_request_id)
66 repoid=pull_request_repo, pullrequestid=pull_request_id)
68 response = api_call(self.app, params)
67 response = api_call(self.app, params)
69
68
70 expected = 'pull request `%s` is already closed' % pull_request_id
69 expected = 'pull request `%s` is already closed' % pull_request_id
71 assert_error(id_, expected, given=response.body)
70 assert_error(id_, expected, given=response.body)
72
71
73 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
74 def test_api_close_pull_request_repo_error(self):
73 def test_api_close_pull_request_repo_error(self):
75 id_, params = build_data(
74 id_, params = build_data(
76 self.apikey, 'close_pull_request',
75 self.apikey, 'close_pull_request',
77 repoid=666, pullrequestid=1)
76 repoid=666, pullrequestid=1)
78 response = api_call(self.app, params)
77 response = api_call(self.app, params)
79
78
80 expected = 'repository `666` does not exist'
79 expected = 'repository `666` does not exist'
81 assert_error(id_, expected, given=response.body)
80 assert_error(id_, expected, given=response.body)
82
81
83 @pytest.mark.backends("git", "hg")
82 @pytest.mark.backends("git", "hg")
84 def test_api_close_pull_request_non_admin_with_userid_error(self,
83 def test_api_close_pull_request_non_admin_with_userid_error(self,
85 pr_util):
84 pr_util):
86 pull_request = pr_util.create_pull_request()
85 pull_request = pr_util.create_pull_request()
87 id_, params = build_data(
86 id_, params = build_data(
88 self.apikey_regular, 'close_pull_request',
87 self.apikey_regular, 'close_pull_request',
89 repoid=pull_request.target_repo.repo_name,
88 repoid=pull_request.target_repo.repo_name,
90 pullrequestid=pull_request.pull_request_id,
89 pullrequestid=pull_request.pull_request_id,
91 userid=TEST_USER_ADMIN_LOGIN)
90 userid=TEST_USER_ADMIN_LOGIN)
92 response = api_call(self.app, params)
91 response = api_call(self.app, params)
93
92
94 expected = 'userid is not the same as your user'
93 expected = 'userid is not the same as your user'
95 assert_error(id_, expected, given=response.body)
94 assert_error(id_, expected, given=response.body)
96
95
97 @pytest.mark.backends("git", "hg")
96 @pytest.mark.backends("git", "hg")
98 def test_api_close_pull_request_no_perms_to_close(
97 def test_api_close_pull_request_no_perms_to_close(
99 self, user_util, pr_util):
98 self, user_util, pr_util):
100 user = user_util.create_user()
99 user = user_util.create_user()
101 pull_request = pr_util.create_pull_request()
100 pull_request = pr_util.create_pull_request()
102
101
103 id_, params = build_data(
102 id_, params = build_data(
104 user.api_key, 'close_pull_request',
103 user.api_key, 'close_pull_request',
105 repoid=pull_request.target_repo.repo_name,
104 repoid=pull_request.target_repo.repo_name,
106 pullrequestid=pull_request.pull_request_id,)
105 pullrequestid=pull_request.pull_request_id,)
107 response = api_call(self.app, params)
106 response = api_call(self.app, params)
108
107
109 expected = ('pull request `%s` close failed, '
108 expected = ('pull request `%s` close failed, '
110 'no permission to close.') % pull_request.pull_request_id
109 'no permission to close.') % pull_request.pull_request_id
111
110
112 response_json = response.json['error']
111 response_json = response.json['error']
113 assert response_json == expected
112 assert response_json == expected
@@ -1,209 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.comment import CommentsModel
23 from rhodecode.model.comment import CommentsModel
24 from rhodecode.model.db import UserLog
24 from rhodecode.model.db import UserLog
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCommentPullRequest(object):
32 class TestCommentPullRequest(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 @pytest.mark.backends("git", "hg")
41 @pytest.mark.backends("git", "hg")
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
43 pull_request = pr_util.create_pull_request()
43 pull_request = pr_util.create_pull_request()
44 pull_request_id = pull_request.pull_request_id
44 pull_request_id = pull_request.pull_request_id
45 author = pull_request.user_id
45 author = pull_request.user_id
46 repo = pull_request.target_repo.repo_id
46 repo = pull_request.target_repo.repo_id
47 id_, params = build_data(
47 id_, params = build_data(
48 self.apikey, 'comment_pull_request',
48 self.apikey, 'comment_pull_request',
49 repoid=pull_request.target_repo.repo_name,
49 repoid=pull_request.target_repo.repo_name,
50 pullrequestid=pull_request.pull_request_id,
50 pullrequestid=pull_request.pull_request_id,
51 message='test message')
51 message='test message')
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
54
54
55 comments = CommentsModel().get_comments(
55 comments = CommentsModel().get_comments(
56 pull_request.target_repo.repo_id, pull_request=pull_request)
56 pull_request.target_repo.repo_id, pull_request=pull_request)
57
57
58 expected = {
58 expected = {
59 'pull_request_id': pull_request.pull_request_id,
59 'pull_request_id': pull_request.pull_request_id,
60 'comment_id': comments[-1].comment_id,
60 'comment_id': comments[-1].comment_id,
61 'status': {'given': None, 'was_changed': None}
61 'status': {'given': None, 'was_changed': None}
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(
75 self, pr_util, no_notifications):
74 self, pr_util, no_notifications):
76 pull_request = pr_util.create_pull_request()
75 pull_request = pr_util.create_pull_request()
77 pull_request_id = pull_request.pull_request_id
76 pull_request_id = pull_request.pull_request_id
78 id_, params = build_data(
77 id_, params = build_data(
79 self.apikey, 'comment_pull_request',
78 self.apikey, 'comment_pull_request',
80 repoid=pull_request.target_repo.repo_name,
79 repoid=pull_request.target_repo.repo_name,
81 pullrequestid=pull_request.pull_request_id,
80 pullrequestid=pull_request.pull_request_id,
82 status='rejected')
81 status='rejected')
83 response = api_call(self.app, params)
82 response = api_call(self.app, params)
84 pull_request = PullRequestModel().get(pull_request_id)
83 pull_request = PullRequestModel().get(pull_request_id)
85
84
86 comments = CommentsModel().get_comments(
85 comments = CommentsModel().get_comments(
87 pull_request.target_repo.repo_id, pull_request=pull_request)
86 pull_request.target_repo.repo_id, pull_request=pull_request)
88 expected = {
87 expected = {
89 'pull_request_id': pull_request.pull_request_id,
88 'pull_request_id': pull_request.pull_request_id,
90 'comment_id': comments[-1].comment_id,
89 'comment_id': comments[-1].comment_id,
91 'status': {'given': 'rejected', 'was_changed': True}
90 'status': {'given': 'rejected', 'was_changed': True}
92 }
91 }
93 assert_ok(id_, expected, response.body)
92 assert_ok(id_, expected, response.body)
94
93
95 @pytest.mark.backends("git", "hg")
94 @pytest.mark.backends("git", "hg")
96 def test_api_comment_pull_request_change_status_with_specific_commit_id(
95 def test_api_comment_pull_request_change_status_with_specific_commit_id(
97 self, pr_util, no_notifications):
96 self, pr_util, no_notifications):
98 pull_request = pr_util.create_pull_request()
97 pull_request = pr_util.create_pull_request()
99 pull_request_id = pull_request.pull_request_id
98 pull_request_id = pull_request.pull_request_id
100 latest_commit_id = 'test_commit'
99 latest_commit_id = 'test_commit'
101 # inject additional revision, to fail test the status change on
100 # inject additional revision, to fail test the status change on
102 # non-latest commit
101 # non-latest commit
103 pull_request.revisions = pull_request.revisions + ['test_commit']
102 pull_request.revisions = pull_request.revisions + ['test_commit']
104
103
105 id_, params = build_data(
104 id_, params = build_data(
106 self.apikey, 'comment_pull_request',
105 self.apikey, 'comment_pull_request',
107 repoid=pull_request.target_repo.repo_name,
106 repoid=pull_request.target_repo.repo_name,
108 pullrequestid=pull_request.pull_request_id,
107 pullrequestid=pull_request.pull_request_id,
109 status='approved', commit_id=latest_commit_id)
108 status='approved', commit_id=latest_commit_id)
110 response = api_call(self.app, params)
109 response = api_call(self.app, params)
111 pull_request = PullRequestModel().get(pull_request_id)
110 pull_request = PullRequestModel().get(pull_request_id)
112
111
113 expected = {
112 expected = {
114 'pull_request_id': pull_request.pull_request_id,
113 'pull_request_id': pull_request.pull_request_id,
115 'comment_id': None,
114 'comment_id': None,
116 'status': {'given': 'approved', 'was_changed': False}
115 'status': {'given': 'approved', 'was_changed': False}
117 }
116 }
118 assert_ok(id_, expected, response.body)
117 assert_ok(id_, expected, response.body)
119
118
120 @pytest.mark.backends("git", "hg")
119 @pytest.mark.backends("git", "hg")
121 def test_api_comment_pull_request_change_status_with_specific_commit_id(
120 def test_api_comment_pull_request_change_status_with_specific_commit_id(
122 self, pr_util, no_notifications):
121 self, pr_util, no_notifications):
123 pull_request = pr_util.create_pull_request()
122 pull_request = pr_util.create_pull_request()
124 pull_request_id = pull_request.pull_request_id
123 pull_request_id = pull_request.pull_request_id
125 latest_commit_id = pull_request.revisions[0]
124 latest_commit_id = pull_request.revisions[0]
126
125
127 id_, params = build_data(
126 id_, params = build_data(
128 self.apikey, 'comment_pull_request',
127 self.apikey, 'comment_pull_request',
129 repoid=pull_request.target_repo.repo_name,
128 repoid=pull_request.target_repo.repo_name,
130 pullrequestid=pull_request.pull_request_id,
129 pullrequestid=pull_request.pull_request_id,
131 status='approved', commit_id=latest_commit_id)
130 status='approved', commit_id=latest_commit_id)
132 response = api_call(self.app, params)
131 response = api_call(self.app, params)
133 pull_request = PullRequestModel().get(pull_request_id)
132 pull_request = PullRequestModel().get(pull_request_id)
134
133
135 comments = CommentsModel().get_comments(
134 comments = CommentsModel().get_comments(
136 pull_request.target_repo.repo_id, pull_request=pull_request)
135 pull_request.target_repo.repo_id, pull_request=pull_request)
137 expected = {
136 expected = {
138 'pull_request_id': pull_request.pull_request_id,
137 'pull_request_id': pull_request.pull_request_id,
139 'comment_id': comments[-1].comment_id,
138 'comment_id': comments[-1].comment_id,
140 'status': {'given': 'approved', 'was_changed': True}
139 'status': {'given': 'approved', 'was_changed': True}
141 }
140 }
142 assert_ok(id_, expected, response.body)
141 assert_ok(id_, expected, response.body)
143
142
144 @pytest.mark.backends("git", "hg")
143 @pytest.mark.backends("git", "hg")
145 def test_api_comment_pull_request_missing_params_error(self, pr_util):
144 def test_api_comment_pull_request_missing_params_error(self, pr_util):
146 pull_request = pr_util.create_pull_request()
145 pull_request = pr_util.create_pull_request()
147 pull_request_id = pull_request.pull_request_id
146 pull_request_id = pull_request.pull_request_id
148 pull_request_repo = pull_request.target_repo.repo_name
147 pull_request_repo = pull_request.target_repo.repo_name
149 id_, params = build_data(
148 id_, params = build_data(
150 self.apikey, 'comment_pull_request',
149 self.apikey, 'comment_pull_request',
151 repoid=pull_request_repo,
150 repoid=pull_request_repo,
152 pullrequestid=pull_request_id)
151 pullrequestid=pull_request_id)
153 response = api_call(self.app, params)
152 response = api_call(self.app, params)
154
153
155 expected = 'Both message and status parameters are missing. At least one is required.'
154 expected = 'Both message and status parameters are missing. At least one is required.'
156 assert_error(id_, expected, given=response.body)
155 assert_error(id_, expected, given=response.body)
157
156
158 @pytest.mark.backends("git", "hg")
157 @pytest.mark.backends("git", "hg")
159 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
158 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
160 pull_request = pr_util.create_pull_request()
159 pull_request = pr_util.create_pull_request()
161 pull_request_id = pull_request.pull_request_id
160 pull_request_id = pull_request.pull_request_id
162 pull_request_repo = pull_request.target_repo.repo_name
161 pull_request_repo = pull_request.target_repo.repo_name
163 id_, params = build_data(
162 id_, params = build_data(
164 self.apikey, 'comment_pull_request',
163 self.apikey, 'comment_pull_request',
165 repoid=pull_request_repo,
164 repoid=pull_request_repo,
166 pullrequestid=pull_request_id,
165 pullrequestid=pull_request_id,
167 status='42')
166 status='42')
168 response = api_call(self.app, params)
167 response = api_call(self.app, params)
169
168
170 expected = 'Unknown comment status: `42`'
169 expected = 'Unknown comment status: `42`'
171 assert_error(id_, expected, given=response.body)
170 assert_error(id_, expected, given=response.body)
172
171
173 @pytest.mark.backends("git", "hg")
172 @pytest.mark.backends("git", "hg")
174 def test_api_comment_pull_request_repo_error(self):
173 def test_api_comment_pull_request_repo_error(self):
175 id_, params = build_data(
174 id_, params = build_data(
176 self.apikey, 'comment_pull_request',
175 self.apikey, 'comment_pull_request',
177 repoid=666, pullrequestid=1)
176 repoid=666, pullrequestid=1)
178 response = api_call(self.app, params)
177 response = api_call(self.app, params)
179
178
180 expected = 'repository `666` does not exist'
179 expected = 'repository `666` does not exist'
181 assert_error(id_, expected, given=response.body)
180 assert_error(id_, expected, given=response.body)
182
181
183 @pytest.mark.backends("git", "hg")
182 @pytest.mark.backends("git", "hg")
184 def test_api_comment_pull_request_non_admin_with_userid_error(
183 def test_api_comment_pull_request_non_admin_with_userid_error(
185 self, pr_util):
184 self, pr_util):
186 pull_request = pr_util.create_pull_request()
185 pull_request = pr_util.create_pull_request()
187 id_, params = build_data(
186 id_, params = build_data(
188 self.apikey_regular, 'comment_pull_request',
187 self.apikey_regular, 'comment_pull_request',
189 repoid=pull_request.target_repo.repo_name,
188 repoid=pull_request.target_repo.repo_name,
190 pullrequestid=pull_request.pull_request_id,
189 pullrequestid=pull_request.pull_request_id,
191 userid=TEST_USER_ADMIN_LOGIN)
190 userid=TEST_USER_ADMIN_LOGIN)
192 response = api_call(self.app, params)
191 response = api_call(self.app, params)
193
192
194 expected = 'userid is not the same as your user'
193 expected = 'userid is not the same as your user'
195 assert_error(id_, expected, given=response.body)
194 assert_error(id_, expected, given=response.body)
196
195
197 @pytest.mark.backends("git", "hg")
196 @pytest.mark.backends("git", "hg")
198 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
197 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
199 pull_request = pr_util.create_pull_request()
198 pull_request = pr_util.create_pull_request()
200 id_, params = build_data(
199 id_, params = build_data(
201 self.apikey_regular, 'comment_pull_request',
200 self.apikey_regular, 'comment_pull_request',
202 repoid=pull_request.target_repo.repo_name,
201 repoid=pull_request.target_repo.repo_name,
203 status='approved',
202 status='approved',
204 pullrequestid=pull_request.pull_request_id,
203 pullrequestid=pull_request.pull_request_id,
205 commit_id='XXX')
204 commit_id='XXX')
206 response = api_call(self.app, params)
205 response = api_call(self.app, params)
207
206
208 expected = 'Invalid commit_id `XXX` for this pull request.'
207 expected = 'Invalid commit_id `XXX` for this pull request.'
209 assert_error(id_, expected, given=response.body)
208 assert_error(id_, expected, given=response.body)
@@ -1,134 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21
21
22 import pytest
22 import pytest
23 import urlobject
23 import urlobject
24 from pylons import url
24 from pylons import url
25
25
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 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
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
32
32
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, http_host_stub, http_host_only_stub):
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(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47
47
48 url_obj = urlobject.URLObject(
48 url_obj = urlobject.URLObject(
49 url(
49 url(
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
53
54 pr_url = safe_unicode(
54 pr_url = safe_unicode(
55 url_obj.with_netloc(http_host_stub))
55 url_obj.with_netloc(http_host_only_stub))
56 source_url = safe_unicode(
56 source_url = safe_unicode(
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = safe_unicode(
58 target_url = safe_unicode(
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 shadow_url = safe_unicode(
60 shadow_url = safe_unicode(
61 PullRequestModel().get_shadow_clone_url(pull_request))
61 PullRequestModel().get_shadow_clone_url(pull_request))
62
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,
66 'title': pull_request.title,
66 'title': pull_request.title,
67 'description': pull_request.description,
67 'description': pull_request.description,
68 'status': pull_request.status,
68 'status': pull_request.status,
69 'created_on': pull_request.created_on,
69 'created_on': pull_request.created_on,
70 'updated_on': pull_request.updated_on,
70 'updated_on': pull_request.updated_on,
71 'commit_ids': pull_request.revisions,
71 'commit_ids': pull_request.revisions,
72 'review_status': pull_request.calculated_review_status(),
72 'review_status': pull_request.calculated_review_status(),
73 'mergeable': {
73 'mergeable': {
74 'status': True,
74 'status': True,
75 'message': 'This pull request can be automatically merged.',
75 'message': 'This pull request can be automatically merged.',
76 },
76 },
77 'source': {
77 'source': {
78 'clone_url': source_url,
78 'clone_url': source_url,
79 'repository': pull_request.source_repo.repo_name,
79 'repository': pull_request.source_repo.repo_name,
80 'reference': {
80 'reference': {
81 'name': pull_request.source_ref_parts.name,
81 'name': pull_request.source_ref_parts.name,
82 'type': pull_request.source_ref_parts.type,
82 'type': pull_request.source_ref_parts.type,
83 'commit_id': pull_request.source_ref_parts.commit_id,
83 'commit_id': pull_request.source_ref_parts.commit_id,
84 },
84 },
85 },
85 },
86 'target': {
86 'target': {
87 'clone_url': target_url,
87 'clone_url': target_url,
88 'repository': pull_request.target_repo.repo_name,
88 'repository': pull_request.target_repo.repo_name,
89 'reference': {
89 'reference': {
90 'name': pull_request.target_ref_parts.name,
90 'name': pull_request.target_ref_parts.name,
91 'type': pull_request.target_ref_parts.type,
91 'type': pull_request.target_ref_parts.type,
92 'commit_id': pull_request.target_ref_parts.commit_id,
92 'commit_id': pull_request.target_ref_parts.commit_id,
93 },
93 },
94 },
94 },
95 'merge': {
95 'merge': {
96 'clone_url': shadow_url,
96 'clone_url': shadow_url,
97 'reference': {
97 'reference': {
98 'name': pull_request.shadow_merge_ref.name,
98 'name': pull_request.shadow_merge_ref.name,
99 'type': pull_request.shadow_merge_ref.type,
99 'type': pull_request.shadow_merge_ref.type,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 },
101 },
102 },
102 },
103 'author': pull_request.author.get_api_data(include_secrets=False,
103 'author': pull_request.author.get_api_data(include_secrets=False,
104 details='basic'),
104 details='basic'),
105 'reviewers': [
105 'reviewers': [
106 {
106 {
107 'user': reviewer.get_api_data(include_secrets=False,
107 'user': reviewer.get_api_data(include_secrets=False,
108 details='basic'),
108 details='basic'),
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, mandatory, st in
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 pull_request.reviewers_statuses()
114 ]
114 ]
115 }
115 }
116 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
117
117
118 def test_api_get_pull_request_repo_error(self):
118 def test_api_get_pull_request_repo_error(self):
119 id_, params = build_data(
119 id_, params = build_data(
120 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
121 repoid=666, pullrequestid=1)
121 repoid=666, pullrequestid=1)
122 response = api_call(self.app, params)
122 response = api_call(self.app, params)
123
123
124 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
125 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
126
126
127 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey, 'get_pull_request',
129 self.apikey, 'get_pull_request',
130 repoid=1, pullrequestid=666)
130 repoid=1, pullrequestid=666)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132
132
133 expected = 'pull request `666` does not exist'
133 expected = 'pull request `666` does not exist'
134 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
@@ -1,136 +1,136 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import UserLog, PullRequest
23 from rhodecode.model.db import UserLog, PullRequest
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestMergePullRequest(object):
31 class TestMergePullRequest(object):
32 @pytest.mark.backends("git", "hg")
32 @pytest.mark.backends("git", "hg")
33 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
33 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
34 pull_request = pr_util.create_pull_request(mergeable=True)
34 pull_request = pr_util.create_pull_request(mergeable=True)
35 author = pull_request.user_id
35 author = pull_request.user_id
36 repo = pull_request.target_repo.repo_id
36 repo = pull_request.target_repo.repo_id
37 pull_request_id = pull_request.pull_request_id
37 pull_request_id = pull_request.pull_request_id
38 pull_request_repo = pull_request.target_repo.repo_name
38 pull_request_repo = pull_request.target_repo.repo_name
39
39
40 id_, params = build_data(
40 id_, params = build_data(
41 self.apikey, 'merge_pull_request',
41 self.apikey, 'merge_pull_request',
42 repoid=pull_request_repo,
42 repoid=pull_request_repo,
43 pullrequestid=pull_request_id)
43 pullrequestid=pull_request_id)
44
44
45 response = api_call(self.app, params)
45 response = api_call(self.app, params)
46
46
47 # The above api call detaches the pull request DB object from the
47 # The above api call detaches the pull request DB object from the
48 # session because of an unconditional transaction rollback in our
48 # session because of an unconditional transaction rollback in our
49 # middleware. Therefore we need to add it back here if we want to use
49 # middleware. Therefore we need to add it back here if we want to use
50 # it.
50 # it.
51 Session().add(pull_request)
51 Session().add(pull_request)
52
52
53 expected = 'merge not possible for following reasons: ' \
53 expected = 'merge not possible for following reasons: ' \
54 'Pull request reviewer approval is pending.'
54 'Pull request reviewer approval is pending.'
55 assert_error(id_, expected, given=response.body)
55 assert_error(id_, expected, given=response.body)
56
56
57 @pytest.mark.backends("git", "hg")
57 @pytest.mark.backends("git", "hg")
58 def test_api_merge_pull_request(self, pr_util, no_notifications):
58 def test_api_merge_pull_request(self, pr_util, no_notifications):
59 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
60 author = pull_request.user_id
60 author = pull_request.user_id
61 repo = pull_request.target_repo.repo_id
61 repo = pull_request.target_repo.repo_id
62 pull_request_id = pull_request.pull_request_id
62 pull_request_id = pull_request.pull_request_id
63 pull_request_repo = pull_request.target_repo.repo_name
63 pull_request_repo = pull_request.target_repo.repo_name
64
64
65 id_, params = build_data(
65 id_, params = build_data(
66 self.apikey, 'comment_pull_request',
66 self.apikey, 'comment_pull_request',
67 repoid=pull_request_repo,
67 repoid=pull_request_repo,
68 pullrequestid=pull_request_id,
68 pullrequestid=pull_request_id,
69 status='approved')
69 status='approved')
70
70
71 response = api_call(self.app, params)
71 response = api_call(self.app, params)
72 expected = {
72 expected = {
73 'comment_id': response.json.get('result', {}).get('comment_id'),
73 'comment_id': response.json.get('result', {}).get('comment_id'),
74 'pull_request_id': pull_request_id,
74 'pull_request_id': pull_request_id,
75 'status': {'given': 'approved', 'was_changed': True}
75 'status': {'given': 'approved', 'was_changed': True}
76 }
76 }
77 assert_ok(id_, expected, given=response.body)
77 assert_ok(id_, expected, given=response.body)
78
78
79 id_, params = build_data(
79 id_, params = build_data(
80 self.apikey, 'merge_pull_request',
80 self.apikey, 'merge_pull_request',
81 repoid=pull_request_repo,
81 repoid=pull_request_repo,
82 pullrequestid=pull_request_id)
82 pullrequestid=pull_request_id)
83
83
84 response = api_call(self.app, params)
84 response = api_call(self.app, params)
85
85
86 pull_request = PullRequest.get(pull_request_id)
86 pull_request = PullRequest.get(pull_request_id)
87
87
88 expected = {
88 expected = {
89 'executed': True,
89 'executed': True,
90 'failure_reason': 0,
90 'failure_reason': 0,
91 'possible': True,
91 'possible': True,
92 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
92 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
93 'merge_ref': pull_request.shadow_merge_ref._asdict()
93 'merge_ref': pull_request.shadow_merge_ref._asdict()
94 }
94 }
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',
108 repoid=pull_request_repo, pullrequestid=pull_request_id)
108 repoid=pull_request_repo, pullrequestid=pull_request_id)
109 response = api_call(self.app, params)
109 response = api_call(self.app, params)
110
110
111 expected = 'merge not possible for following reasons: This pull request is closed.'
111 expected = 'merge not possible for following reasons: This pull request is closed.'
112 assert_error(id_, expected, given=response.body)
112 assert_error(id_, expected, given=response.body)
113
113
114 @pytest.mark.backends("git", "hg")
114 @pytest.mark.backends("git", "hg")
115 def test_api_merge_pull_request_repo_error(self):
115 def test_api_merge_pull_request_repo_error(self):
116 id_, params = build_data(
116 id_, params = build_data(
117 self.apikey, 'merge_pull_request',
117 self.apikey, 'merge_pull_request',
118 repoid=666, pullrequestid=1)
118 repoid=666, pullrequestid=1)
119 response = api_call(self.app, params)
119 response = api_call(self.app, params)
120
120
121 expected = 'repository `666` does not exist'
121 expected = 'repository `666` does not exist'
122 assert_error(id_, expected, given=response.body)
122 assert_error(id_, expected, given=response.body)
123
123
124 @pytest.mark.backends("git", "hg")
124 @pytest.mark.backends("git", "hg")
125 def test_api_merge_pull_request_non_admin_with_userid_error(self,
125 def test_api_merge_pull_request_non_admin_with_userid_error(self,
126 pr_util):
126 pr_util):
127 pull_request = pr_util.create_pull_request(mergeable=True)
127 pull_request = pr_util.create_pull_request(mergeable=True)
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey_regular, 'merge_pull_request',
129 self.apikey_regular, 'merge_pull_request',
130 repoid=pull_request.target_repo.repo_name,
130 repoid=pull_request.target_repo.repo_name,
131 pullrequestid=pull_request.pull_request_id,
131 pullrequestid=pull_request.pull_request_id,
132 userid=TEST_USER_ADMIN_LOGIN)
132 userid=TEST_USER_ADMIN_LOGIN)
133 response = api_call(self.app, params)
133 response = api_call(self.app, params)
134
134
135 expected = 'userid is not the same as your user'
135 expected = 'userid is not the same as your user'
136 assert_error(id_, expected, given=response.body)
136 assert_error(id_, expected, given=response.body)
@@ -1,213 +1,212 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error)
28 build_data, api_call, assert_ok, assert_error)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdatePullRequest(object):
32 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(
40 self.apikey, 'update_pull_request',
40 self.apikey, 'update_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id,
42 pullrequestid=pull_request.pull_request_id,
43 title='New TITLE OF A PR',
43 title='New TITLE OF A PR',
44 description='New DESC OF A PR',
44 description='New DESC OF A PR',
45 )
45 )
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 expected = {
48 expected = {
49 "msg": "Updated pull request `{}`".format(
49 "msg": "Updated pull request `{}`".format(
50 pull_request.pull_request_id),
50 pull_request.pull_request_id),
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 }
54 }
55
55
56 response_json = response.json['result']
56 response_json = response.json['result']
57 assert response_json == expected
57 assert response_json == expected
58 pr = response_json['pull_request']
58 pr = response_json['pull_request']
59 assert pr['title'] == 'New TITLE OF A PR'
59 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
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)
68
68
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_pull_request',
70 self.apikey, 'update_pull_request',
71 repoid=pull_request.target_repo.repo_name,
71 repoid=pull_request.target_repo.repo_name,
72 pullrequestid=pull_request.pull_request_id)
72 pullrequestid=pull_request.pull_request_id)
73 response = api_call(self.app, params)
73 response = api_call(self.app, params)
74
74
75 expected = 'pull request `{}` update failed, pull request ' \
75 expected = 'pull request `{}` update failed, pull request ' \
76 'is closed'.format(pull_request.pull_request_id)
76 'is closed'.format(pull_request.pull_request_id)
77
77
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')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
85 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
87 ]
86 ]
88 pull_request = pr_util.create_pull_request(
87 pull_request = pr_util.create_pull_request(
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
88 commits=commits, target_head='a', source_head='b', revisions=['b'])
90 pr_util.update_source_repository(head='c')
89 pr_util.update_source_repository(head='c')
91 repo = pull_request.source_repo.scm_instance()
90 repo = pull_request.source_repo.scm_instance()
92 commits = [x for x in repo.get_commits()]
91 commits = [x for x in repo.get_commits()]
93 print commits
92 print commits
94
93
95 added_commit_id = commits[-1].raw_id # c commit
94 added_commit_id = commits[-1].raw_id # c commit
96 common_commit_id = commits[1].raw_id # b commit is common ancestor
95 common_commit_id = commits[1].raw_id # b commit is common ancestor
97 total_commits = [added_commit_id, common_commit_id]
96 total_commits = [added_commit_id, common_commit_id]
98
97
99 id_, params = build_data(
98 id_, params = build_data(
100 self.apikey, 'update_pull_request',
99 self.apikey, 'update_pull_request',
101 repoid=pull_request.target_repo.repo_name,
100 repoid=pull_request.target_repo.repo_name,
102 pullrequestid=pull_request.pull_request_id,
101 pullrequestid=pull_request.pull_request_id,
103 update_commits=True
102 update_commits=True
104 )
103 )
105 response = api_call(self.app, params)
104 response = api_call(self.app, params)
106
105
107 expected = {
106 expected = {
108 "msg": "Updated pull request `{}`".format(
107 "msg": "Updated pull request `{}`".format(
109 pull_request.pull_request_id),
108 pull_request.pull_request_id),
110 "pull_request": response.json['result']['pull_request'],
109 "pull_request": response.json['result']['pull_request'],
111 "updated_commits": {"added": [added_commit_id],
110 "updated_commits": {"added": [added_commit_id],
112 "common": [common_commit_id],
111 "common": [common_commit_id],
113 "total": total_commits,
112 "total": total_commits,
114 "removed": []},
113 "removed": []},
115 "updated_reviewers": {"added": [], "removed": []},
114 "updated_reviewers": {"added": [], "removed": []},
116 }
115 }
117
116
118 assert_ok(id_, expected, response.body)
117 assert_ok(id_, expected, response.body)
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, user_util, pr_util, silence_action_logger, no_notifications):
121 self, user_util, pr_util, no_notifications):
123 a = user_util.create_user()
122 a = user_util.create_user()
124 b = user_util.create_user()
123 b = user_util.create_user()
125 c = user_util.create_user()
124 c = user_util.create_user()
126 new_reviewers = [
125 new_reviewers = [
127 {'username': b.username,'reasons': ['updated via API'],
126 {'username': b.username,'reasons': ['updated via API'],
128 'mandatory':False},
127 'mandatory':False},
129 {'username': c.username, 'reasons': ['updated via API'],
128 {'username': c.username, 'reasons': ['updated via API'],
130 'mandatory':False},
129 'mandatory':False},
131 ]
130 ]
132
131
133 added = [b.username, c.username]
132 added = [b.username, c.username]
134 removed = [a.username]
133 removed = [a.username]
135
134
136 pull_request = pr_util.create_pull_request(
135 pull_request = pr_util.create_pull_request(
137 reviewers=[(a.username, ['added via API'], False)])
136 reviewers=[(a.username, ['added via API'], False)])
138
137
139 id_, params = build_data(
138 id_, params = build_data(
140 self.apikey, 'update_pull_request',
139 self.apikey, 'update_pull_request',
141 repoid=pull_request.target_repo.repo_name,
140 repoid=pull_request.target_repo.repo_name,
142 pullrequestid=pull_request.pull_request_id,
141 pullrequestid=pull_request.pull_request_id,
143 reviewers=new_reviewers)
142 reviewers=new_reviewers)
144 response = api_call(self.app, params)
143 response = api_call(self.app, params)
145 expected = {
144 expected = {
146 "msg": "Updated pull request `{}`".format(
145 "msg": "Updated pull request `{}`".format(
147 pull_request.pull_request_id),
146 pull_request.pull_request_id),
148 "pull_request": response.json['result']['pull_request'],
147 "pull_request": response.json['result']['pull_request'],
149 "updated_commits": {"added": [], "common": [], "removed": []},
148 "updated_commits": {"added": [], "common": [], "removed": []},
150 "updated_reviewers": {"added": added, "removed": removed},
149 "updated_reviewers": {"added": added, "removed": removed},
151 }
150 }
152
151
153 assert_ok(id_, expected, response.body)
152 assert_ok(id_, expected, response.body)
154
153
155 @pytest.mark.backends("git", "hg")
154 @pytest.mark.backends("git", "hg")
156 def test_api_update_bad_user_in_reviewers(self, pr_util):
155 def test_api_update_bad_user_in_reviewers(self, pr_util):
157 pull_request = pr_util.create_pull_request()
156 pull_request = pr_util.create_pull_request()
158
157
159 id_, params = build_data(
158 id_, params = build_data(
160 self.apikey, 'update_pull_request',
159 self.apikey, 'update_pull_request',
161 repoid=pull_request.target_repo.repo_name,
160 repoid=pull_request.target_repo.repo_name,
162 pullrequestid=pull_request.pull_request_id,
161 pullrequestid=pull_request.pull_request_id,
163 reviewers=[{'username': 'bad_name'}])
162 reviewers=[{'username': 'bad_name'}])
164 response = api_call(self.app, params)
163 response = api_call(self.app, params)
165
164
166 expected = 'user `bad_name` does not exist'
165 expected = 'user `bad_name` does not exist'
167
166
168 assert_error(id_, expected, response.body)
167 assert_error(id_, expected, response.body)
169
168
170 @pytest.mark.backends("git", "hg")
169 @pytest.mark.backends("git", "hg")
171 def test_api_update_repo_error(self, pr_util):
170 def test_api_update_repo_error(self, pr_util):
172 id_, params = build_data(
171 id_, params = build_data(
173 self.apikey, 'update_pull_request',
172 self.apikey, 'update_pull_request',
174 repoid='fake',
173 repoid='fake',
175 pullrequestid='fake',
174 pullrequestid='fake',
176 reviewers=[{'username': 'bad_name'}])
175 reviewers=[{'username': 'bad_name'}])
177 response = api_call(self.app, params)
176 response = api_call(self.app, params)
178
177
179 expected = 'repository `fake` does not exist'
178 expected = 'repository `fake` does not exist'
180
179
181 response_json = response.json['error']
180 response_json = response.json['error']
182 assert response_json == expected
181 assert response_json == expected
183
182
184 @pytest.mark.backends("git", "hg")
183 @pytest.mark.backends("git", "hg")
185 def test_api_update_pull_request_error(self, pr_util):
184 def test_api_update_pull_request_error(self, pr_util):
186 pull_request = pr_util.create_pull_request()
185 pull_request = pr_util.create_pull_request()
187
186
188 id_, params = build_data(
187 id_, params = build_data(
189 self.apikey, 'update_pull_request',
188 self.apikey, 'update_pull_request',
190 repoid=pull_request.target_repo.repo_name,
189 repoid=pull_request.target_repo.repo_name,
191 pullrequestid=999999,
190 pullrequestid=999999,
192 reviewers=[{'username': 'bad_name'}])
191 reviewers=[{'username': 'bad_name'}])
193 response = api_call(self.app, params)
192 response = api_call(self.app, params)
194
193
195 expected = 'pull request `999999` does not exist'
194 expected = 'pull request `999999` does not exist'
196 assert_error(id_, expected, response.body)
195 assert_error(id_, expected, response.body)
197
196
198 @pytest.mark.backends("git", "hg")
197 @pytest.mark.backends("git", "hg")
199 def test_api_update_pull_request_no_perms_to_update(
198 def test_api_update_pull_request_no_perms_to_update(
200 self, user_util, pr_util):
199 self, user_util, pr_util):
201 user = user_util.create_user()
200 user = user_util.create_user()
202 pull_request = pr_util.create_pull_request()
201 pull_request = pr_util.create_pull_request()
203
202
204 id_, params = build_data(
203 id_, params = build_data(
205 user.api_key, 'update_pull_request',
204 user.api_key, 'update_pull_request',
206 repoid=pull_request.target_repo.repo_name,
205 repoid=pull_request.target_repo.repo_name,
207 pullrequestid=pull_request.pull_request_id,)
206 pullrequestid=pull_request.pull_request_id,)
208 response = api_call(self.app, params)
207 response = api_call(self.app, params)
209
208
210 expected = ('pull request `%s` update failed, '
209 expected = ('pull request `%s` update failed, '
211 'no permission to update.') % pull_request.pull_request_id
210 'no permission to update.') % pull_request.pull_request_id
212
211
213 assert_error(id_, expected, response.body)
212 assert_error(id_, expected, response.body)
@@ -1,779 +1,779 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 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,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, repoid, pullrequestid):
46 def get_pull_request(request, apiuser, repoid, pullrequestid):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Repository name or repository ID from where the pull
52 :param repoid: Repository name or repository ID from where the pull
53 request was opened.
53 request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124 get_repo_or_error(repoid)
124 get_repo_or_error(repoid)
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if not PullRequestModel().check_user_read(
126 if not PullRequestModel().check_user_read(
127 pull_request, apiuser, api=True):
127 pull_request, apiuser, api=True):
128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
129 data = pull_request.get_api_data()
129 data = pull_request.get_api_data()
130 return data
130 return data
131
131
132
132
133 @jsonrpc_method()
133 @jsonrpc_method()
134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
135 """
135 """
136 Get all pull requests from the repository specified in `repoid`.
136 Get all pull requests from the repository specified in `repoid`.
137
137
138 :param apiuser: This is filled automatically from the |authtoken|.
138 :param apiuser: This is filled automatically from the |authtoken|.
139 :type apiuser: AuthUser
139 :type apiuser: AuthUser
140 :param repoid: Repository name or repository ID.
140 :param repoid: Repository name or repository ID.
141 :type repoid: str or int
141 :type repoid: str or int
142 :param status: Only return pull requests with the specified status.
142 :param status: Only return pull requests with the specified status.
143 Valid options are.
143 Valid options are.
144 * ``new`` (default)
144 * ``new`` (default)
145 * ``open``
145 * ``open``
146 * ``closed``
146 * ``closed``
147 :type status: str
147 :type status: str
148
148
149 Example output:
149 Example output:
150
150
151 .. code-block:: bash
151 .. code-block:: bash
152
152
153 "id": <id_given_in_input>,
153 "id": <id_given_in_input>,
154 "result":
154 "result":
155 [
155 [
156 ...
156 ...
157 {
157 {
158 "pull_request_id": "<pull_request_id>",
158 "pull_request_id": "<pull_request_id>",
159 "url": "<url>",
159 "url": "<url>",
160 "title" : "<title>",
160 "title" : "<title>",
161 "description": "<description>",
161 "description": "<description>",
162 "status": "<status>",
162 "status": "<status>",
163 "created_on": "<date_time_created>",
163 "created_on": "<date_time_created>",
164 "updated_on": "<date_time_updated>",
164 "updated_on": "<date_time_updated>",
165 "commit_ids": [
165 "commit_ids": [
166 ...
166 ...
167 "<commit_id>",
167 "<commit_id>",
168 "<commit_id>",
168 "<commit_id>",
169 ...
169 ...
170 ],
170 ],
171 "review_status": "<review_status>",
171 "review_status": "<review_status>",
172 "mergeable": {
172 "mergeable": {
173 "status": "<bool>",
173 "status": "<bool>",
174 "message: "<message>",
174 "message: "<message>",
175 },
175 },
176 "source": {
176 "source": {
177 "clone_url": "<clone_url>",
177 "clone_url": "<clone_url>",
178 "reference":
178 "reference":
179 {
179 {
180 "name": "<name>",
180 "name": "<name>",
181 "type": "<type>",
181 "type": "<type>",
182 "commit_id": "<commit_id>",
182 "commit_id": "<commit_id>",
183 }
183 }
184 },
184 },
185 "target": {
185 "target": {
186 "clone_url": "<clone_url>",
186 "clone_url": "<clone_url>",
187 "reference":
187 "reference":
188 {
188 {
189 "name": "<name>",
189 "name": "<name>",
190 "type": "<type>",
190 "type": "<type>",
191 "commit_id": "<commit_id>",
191 "commit_id": "<commit_id>",
192 }
192 }
193 },
193 },
194 "merge": {
194 "merge": {
195 "clone_url": "<clone_url>",
195 "clone_url": "<clone_url>",
196 "reference":
196 "reference":
197 {
197 {
198 "name": "<name>",
198 "name": "<name>",
199 "type": "<type>",
199 "type": "<type>",
200 "commit_id": "<commit_id>",
200 "commit_id": "<commit_id>",
201 }
201 }
202 },
202 },
203 "author": <user_obj>,
203 "author": <user_obj>,
204 "reviewers": [
204 "reviewers": [
205 ...
205 ...
206 {
206 {
207 "user": "<user_obj>",
207 "user": "<user_obj>",
208 "review_status": "<review_status>",
208 "review_status": "<review_status>",
209 }
209 }
210 ...
210 ...
211 ]
211 ]
212 }
212 }
213 ...
213 ...
214 ],
214 ],
215 "error": null
215 "error": null
216
216
217 """
217 """
218 repo = get_repo_or_error(repoid)
218 repo = get_repo_or_error(repoid)
219 if not has_superadmin_permission(apiuser):
219 if not has_superadmin_permission(apiuser):
220 _perms = (
220 _perms = (
221 'repository.admin', 'repository.write', 'repository.read',)
221 'repository.admin', 'repository.write', 'repository.read',)
222 validate_repo_permissions(apiuser, repoid, repo, _perms)
222 validate_repo_permissions(apiuser, repoid, repo, _perms)
223
223
224 status = Optional.extract(status)
224 status = Optional.extract(status)
225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
226 data = [pr.get_api_data() for pr in pull_requests]
226 data = [pr.get_api_data() for pr in pull_requests]
227 return data
227 return data
228
228
229
229
230 @jsonrpc_method()
230 @jsonrpc_method()
231 def merge_pull_request(
231 def merge_pull_request(
232 request, apiuser, repoid, pullrequestid,
232 request, apiuser, repoid, pullrequestid,
233 userid=Optional(OAttr('apiuser'))):
233 userid=Optional(OAttr('apiuser'))):
234 """
234 """
235 Merge the pull request specified by `pullrequestid` into its target
235 Merge the pull request specified by `pullrequestid` into its target
236 repository.
236 repository.
237
237
238 :param apiuser: This is filled automatically from the |authtoken|.
238 :param apiuser: This is filled automatically from the |authtoken|.
239 :type apiuser: AuthUser
239 :type apiuser: AuthUser
240 :param repoid: The Repository name or repository ID of the
240 :param repoid: The Repository name or repository ID of the
241 target repository to which the |pr| is to be merged.
241 target repository to which the |pr| is to be merged.
242 :type repoid: str or int
242 :type repoid: str or int
243 :param pullrequestid: ID of the pull request which shall be merged.
243 :param pullrequestid: ID of the pull request which shall be merged.
244 :type pullrequestid: int
244 :type pullrequestid: int
245 :param userid: Merge the pull request as this user.
245 :param userid: Merge the pull request as this user.
246 :type userid: Optional(str or int)
246 :type userid: Optional(str or int)
247
247
248 Example output:
248 Example output:
249
249
250 .. code-block:: bash
250 .. code-block:: bash
251
251
252 "id": <id_given_in_input>,
252 "id": <id_given_in_input>,
253 "result": {
253 "result": {
254 "executed": "<bool>",
254 "executed": "<bool>",
255 "failure_reason": "<int>",
255 "failure_reason": "<int>",
256 "merge_commit_id": "<merge_commit_id>",
256 "merge_commit_id": "<merge_commit_id>",
257 "possible": "<bool>",
257 "possible": "<bool>",
258 "merge_ref": {
258 "merge_ref": {
259 "commit_id": "<commit_id>",
259 "commit_id": "<commit_id>",
260 "type": "<type>",
260 "type": "<type>",
261 "name": "<name>"
261 "name": "<name>"
262 }
262 }
263 },
263 },
264 "error": null
264 "error": null
265 """
265 """
266 repo = get_repo_or_error(repoid)
266 repo = get_repo_or_error(repoid)
267 if not isinstance(userid, Optional):
267 if not isinstance(userid, Optional):
268 if (has_superadmin_permission(apiuser) or
268 if (has_superadmin_permission(apiuser) or
269 HasRepoPermissionAnyApi('repository.admin')(
269 HasRepoPermissionAnyApi('repository.admin')(
270 user=apiuser, repo_name=repo.repo_name)):
270 user=apiuser, repo_name=repo.repo_name)):
271 apiuser = get_user_or_error(userid)
271 apiuser = get_user_or_error(userid)
272 else:
272 else:
273 raise JSONRPCError('userid is not the same as your user')
273 raise JSONRPCError('userid is not the same as your user')
274
274
275 pull_request = get_pull_request_or_error(pullrequestid)
275 pull_request = get_pull_request_or_error(pullrequestid)
276
276
277 check = MergeCheck.validate(pull_request, user=apiuser)
277 check = MergeCheck.validate(pull_request, user=apiuser)
278 merge_possible = not check.failed
278 merge_possible = not check.failed
279
279
280 if not merge_possible:
280 if not merge_possible:
281 error_messages = []
281 error_messages = []
282 for err_type, error_msg in check.errors:
282 for err_type, error_msg in check.errors:
283 error_msg = request.translate(error_msg)
283 error_msg = request.translate(error_msg)
284 error_messages.append(error_msg)
284 error_messages.append(error_msg)
285
285
286 reasons = ','.join(error_messages)
286 reasons = ','.join(error_messages)
287 raise JSONRPCError(
287 raise JSONRPCError(
288 'merge not possible for following reasons: {}'.format(reasons))
288 'merge not possible for following reasons: {}'.format(reasons))
289
289
290 target_repo = pull_request.target_repo
290 target_repo = pull_request.target_repo
291 extras = vcs_operation_context(
291 extras = vcs_operation_context(
292 request.environ, repo_name=target_repo.repo_name,
292 request.environ, repo_name=target_repo.repo_name,
293 username=apiuser.username, action='push',
293 username=apiuser.username, action='push',
294 scm=target_repo.repo_type)
294 scm=target_repo.repo_type)
295 merge_response = PullRequestModel().merge(
295 merge_response = PullRequestModel().merge(
296 pull_request, apiuser, extras=extras)
296 pull_request, apiuser, extras=extras)
297 if merge_response.executed:
297 if merge_response.executed:
298 PullRequestModel().close_pull_request(
298 PullRequestModel().close_pull_request(
299 pull_request.pull_request_id, apiuser)
299 pull_request.pull_request_id, apiuser)
300
300
301 Session().commit()
301 Session().commit()
302
302
303 # In previous versions the merge response directly contained the merge
303 # In previous versions the merge response directly contained the merge
304 # commit id. It is now contained in the merge reference object. To be
304 # commit id. It is now contained in the merge reference object. To be
305 # backwards compatible we have to extract it again.
305 # backwards compatible we have to extract it again.
306 merge_response = merge_response._asdict()
306 merge_response = merge_response._asdict()
307 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
307 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
308
308
309 return merge_response
309 return merge_response
310
310
311
311
312 @jsonrpc_method()
312 @jsonrpc_method()
313 def comment_pull_request(
313 def comment_pull_request(
314 request, apiuser, repoid, pullrequestid, message=Optional(None),
314 request, apiuser, repoid, pullrequestid, message=Optional(None),
315 commit_id=Optional(None), status=Optional(None),
315 commit_id=Optional(None), status=Optional(None),
316 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
316 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
317 resolves_comment_id=Optional(None),
317 resolves_comment_id=Optional(None),
318 userid=Optional(OAttr('apiuser'))):
318 userid=Optional(OAttr('apiuser'))):
319 """
319 """
320 Comment on the pull request specified with the `pullrequestid`,
320 Comment on the pull request specified with the `pullrequestid`,
321 in the |repo| specified by the `repoid`, and optionally change the
321 in the |repo| specified by the `repoid`, and optionally change the
322 review status.
322 review status.
323
323
324 :param apiuser: This is filled automatically from the |authtoken|.
324 :param apiuser: This is filled automatically from the |authtoken|.
325 :type apiuser: AuthUser
325 :type apiuser: AuthUser
326 :param repoid: The repository name or repository ID.
326 :param repoid: The repository name or repository ID.
327 :type repoid: str or int
327 :type repoid: str or int
328 :param pullrequestid: The pull request ID.
328 :param pullrequestid: The pull request ID.
329 :type pullrequestid: int
329 :type pullrequestid: int
330 :param commit_id: Specify the commit_id for which to set a comment. If
330 :param commit_id: Specify the commit_id for which to set a comment. If
331 given commit_id is different than latest in the PR status
331 given commit_id is different than latest in the PR status
332 change won't be performed.
332 change won't be performed.
333 :type commit_id: str
333 :type commit_id: str
334 :param message: The text content of the comment.
334 :param message: The text content of the comment.
335 :type message: str
335 :type message: str
336 :param status: (**Optional**) Set the approval status of the pull
336 :param status: (**Optional**) Set the approval status of the pull
337 request. One of: 'not_reviewed', 'approved', 'rejected',
337 request. One of: 'not_reviewed', 'approved', 'rejected',
338 'under_review'
338 'under_review'
339 :type status: str
339 :type status: str
340 :param comment_type: Comment type, one of: 'note', 'todo'
340 :param comment_type: Comment type, one of: 'note', 'todo'
341 :type comment_type: Optional(str), default: 'note'
341 :type comment_type: Optional(str), default: 'note'
342 :param userid: Comment on the pull request as this user
342 :param userid: Comment on the pull request as this user
343 :type userid: Optional(str or int)
343 :type userid: Optional(str or int)
344
344
345 Example output:
345 Example output:
346
346
347 .. code-block:: bash
347 .. code-block:: bash
348
348
349 id : <id_given_in_input>
349 id : <id_given_in_input>
350 result : {
350 result : {
351 "pull_request_id": "<Integer>",
351 "pull_request_id": "<Integer>",
352 "comment_id": "<Integer>",
352 "comment_id": "<Integer>",
353 "status": {"given": <given_status>,
353 "status": {"given": <given_status>,
354 "was_changed": <bool status_was_actually_changed> },
354 "was_changed": <bool status_was_actually_changed> },
355 },
355 },
356 error : null
356 error : null
357 """
357 """
358 repo = get_repo_or_error(repoid)
358 repo = get_repo_or_error(repoid)
359 if not isinstance(userid, Optional):
359 if not isinstance(userid, Optional):
360 if (has_superadmin_permission(apiuser) or
360 if (has_superadmin_permission(apiuser) or
361 HasRepoPermissionAnyApi('repository.admin')(
361 HasRepoPermissionAnyApi('repository.admin')(
362 user=apiuser, repo_name=repo.repo_name)):
362 user=apiuser, repo_name=repo.repo_name)):
363 apiuser = get_user_or_error(userid)
363 apiuser = get_user_or_error(userid)
364 else:
364 else:
365 raise JSONRPCError('userid is not the same as your user')
365 raise JSONRPCError('userid is not the same as your user')
366
366
367 pull_request = get_pull_request_or_error(pullrequestid)
367 pull_request = get_pull_request_or_error(pullrequestid)
368 if not PullRequestModel().check_user_read(
368 if not PullRequestModel().check_user_read(
369 pull_request, apiuser, api=True):
369 pull_request, apiuser, api=True):
370 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
370 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
371 message = Optional.extract(message)
371 message = Optional.extract(message)
372 status = Optional.extract(status)
372 status = Optional.extract(status)
373 commit_id = Optional.extract(commit_id)
373 commit_id = Optional.extract(commit_id)
374 comment_type = Optional.extract(comment_type)
374 comment_type = Optional.extract(comment_type)
375 resolves_comment_id = Optional.extract(resolves_comment_id)
375 resolves_comment_id = Optional.extract(resolves_comment_id)
376
376
377 if not message and not status:
377 if not message and not status:
378 raise JSONRPCError(
378 raise JSONRPCError(
379 'Both message and status parameters are missing. '
379 'Both message and status parameters are missing. '
380 'At least one is required.')
380 'At least one is required.')
381
381
382 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
382 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
383 status is not None):
383 status is not None):
384 raise JSONRPCError('Unknown comment status: `%s`' % status)
384 raise JSONRPCError('Unknown comment status: `%s`' % status)
385
385
386 if commit_id and commit_id not in pull_request.revisions:
386 if commit_id and commit_id not in pull_request.revisions:
387 raise JSONRPCError(
387 raise JSONRPCError(
388 'Invalid commit_id `%s` for this pull request.' % commit_id)
388 'Invalid commit_id `%s` for this pull request.' % commit_id)
389
389
390 allowed_to_change_status = PullRequestModel().check_user_change_status(
390 allowed_to_change_status = PullRequestModel().check_user_change_status(
391 pull_request, apiuser)
391 pull_request, apiuser)
392
392
393 # if commit_id is passed re-validated if user is allowed to change status
393 # if commit_id is passed re-validated if user is allowed to change status
394 # based on latest commit_id from the PR
394 # based on latest commit_id from the PR
395 if commit_id:
395 if commit_id:
396 commit_idx = pull_request.revisions.index(commit_id)
396 commit_idx = pull_request.revisions.index(commit_id)
397 if commit_idx != 0:
397 if commit_idx != 0:
398 allowed_to_change_status = False
398 allowed_to_change_status = False
399
399
400 if resolves_comment_id:
400 if resolves_comment_id:
401 comment = ChangesetComment.get(resolves_comment_id)
401 comment = ChangesetComment.get(resolves_comment_id)
402 if not comment:
402 if not comment:
403 raise JSONRPCError(
403 raise JSONRPCError(
404 'Invalid resolves_comment_id `%s` for this pull request.'
404 'Invalid resolves_comment_id `%s` for this pull request.'
405 % resolves_comment_id)
405 % resolves_comment_id)
406 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
406 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
407 raise JSONRPCError(
407 raise JSONRPCError(
408 'Comment `%s` is wrong type for setting status to resolved.'
408 'Comment `%s` is wrong type for setting status to resolved.'
409 % resolves_comment_id)
409 % resolves_comment_id)
410
410
411 text = message
411 text = message
412 status_label = ChangesetStatus.get_status_lbl(status)
412 status_label = ChangesetStatus.get_status_lbl(status)
413 if status and allowed_to_change_status:
413 if status and allowed_to_change_status:
414 st_message = ('Status change %(transition_icon)s %(status)s'
414 st_message = ('Status change %(transition_icon)s %(status)s'
415 % {'transition_icon': '>', 'status': status_label})
415 % {'transition_icon': '>', 'status': status_label})
416 text = message or st_message
416 text = message or st_message
417
417
418 rc_config = SettingsModel().get_all_settings()
418 rc_config = SettingsModel().get_all_settings()
419 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
419 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
420
420
421 status_change = status and allowed_to_change_status
421 status_change = status and allowed_to_change_status
422 comment = CommentsModel().create(
422 comment = CommentsModel().create(
423 text=text,
423 text=text,
424 repo=pull_request.target_repo.repo_id,
424 repo=pull_request.target_repo.repo_id,
425 user=apiuser.user_id,
425 user=apiuser.user_id,
426 pull_request=pull_request.pull_request_id,
426 pull_request=pull_request.pull_request_id,
427 f_path=None,
427 f_path=None,
428 line_no=None,
428 line_no=None,
429 status_change=(status_label if status_change else None),
429 status_change=(status_label if status_change else None),
430 status_change_type=(status if status_change else None),
430 status_change_type=(status if status_change else None),
431 closing_pr=False,
431 closing_pr=False,
432 renderer=renderer,
432 renderer=renderer,
433 comment_type=comment_type,
433 comment_type=comment_type,
434 resolves_comment_id=resolves_comment_id
434 resolves_comment_id=resolves_comment_id
435 )
435 )
436
436
437 if allowed_to_change_status and status:
437 if allowed_to_change_status and status:
438 ChangesetStatusModel().set_status(
438 ChangesetStatusModel().set_status(
439 pull_request.target_repo.repo_id,
439 pull_request.target_repo.repo_id,
440 status,
440 status,
441 apiuser.user_id,
441 apiuser.user_id,
442 comment,
442 comment,
443 pull_request=pull_request.pull_request_id
443 pull_request=pull_request.pull_request_id
444 )
444 )
445 Session().flush()
445 Session().flush()
446
446
447 Session().commit()
447 Session().commit()
448 data = {
448 data = {
449 'pull_request_id': pull_request.pull_request_id,
449 'pull_request_id': pull_request.pull_request_id,
450 'comment_id': comment.comment_id if comment else None,
450 'comment_id': comment.comment_id if comment else None,
451 'status': {'given': status, 'was_changed': status_change},
451 'status': {'given': status, 'was_changed': status_change},
452 }
452 }
453 return data
453 return data
454
454
455
455
456 @jsonrpc_method()
456 @jsonrpc_method()
457 def create_pull_request(
457 def create_pull_request(
458 request, apiuser, source_repo, target_repo, source_ref, target_ref,
458 request, apiuser, source_repo, target_repo, source_ref, target_ref,
459 title, description=Optional(''), reviewers=Optional(None)):
459 title, description=Optional(''), reviewers=Optional(None)):
460 """
460 """
461 Creates a new pull request.
461 Creates a new pull request.
462
462
463 Accepts refs in the following formats:
463 Accepts refs in the following formats:
464
464
465 * branch:<branch_name>:<sha>
465 * branch:<branch_name>:<sha>
466 * branch:<branch_name>
466 * branch:<branch_name>
467 * bookmark:<bookmark_name>:<sha> (Mercurial only)
467 * bookmark:<bookmark_name>:<sha> (Mercurial only)
468 * bookmark:<bookmark_name> (Mercurial only)
468 * bookmark:<bookmark_name> (Mercurial only)
469
469
470 :param apiuser: This is filled automatically from the |authtoken|.
470 :param apiuser: This is filled automatically from the |authtoken|.
471 :type apiuser: AuthUser
471 :type apiuser: AuthUser
472 :param source_repo: Set the source repository name.
472 :param source_repo: Set the source repository name.
473 :type source_repo: str
473 :type source_repo: str
474 :param target_repo: Set the target repository name.
474 :param target_repo: Set the target repository name.
475 :type target_repo: str
475 :type target_repo: str
476 :param source_ref: Set the source ref name.
476 :param source_ref: Set the source ref name.
477 :type source_ref: str
477 :type source_ref: str
478 :param target_ref: Set the target ref name.
478 :param target_ref: Set the target ref name.
479 :type target_ref: str
479 :type target_ref: str
480 :param title: Set the pull request title.
480 :param title: Set the pull request title.
481 :type title: str
481 :type title: str
482 :param description: Set the pull request description.
482 :param description: Set the pull request description.
483 :type description: Optional(str)
483 :type description: Optional(str)
484 :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
485 Reviewer defined by review rules will be added automatically to the
486 defined list.
486 defined list.
487 :type reviewers: Optional(list)
487 :type reviewers: Optional(list)
488 Accepts username strings or objects of the format:
488 Accepts username strings or objects of the format:
489
489
490 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
490 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
491 """
491 """
492
492
493 source_db_repo = get_repo_or_error(source_repo)
493 source_db_repo = get_repo_or_error(source_repo)
494 target_db_repo = get_repo_or_error(target_repo)
494 target_db_repo = get_repo_or_error(target_repo)
495 if not has_superadmin_permission(apiuser):
495 if not has_superadmin_permission(apiuser):
496 _perms = ('repository.admin', 'repository.write', 'repository.read',)
496 _perms = ('repository.admin', 'repository.write', 'repository.read',)
497 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
497 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
498
498
499 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
499 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
500 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
500 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
501 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
501 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
502 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
502 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
503 source_scm = source_db_repo.scm_instance()
503 source_scm = source_db_repo.scm_instance()
504 target_scm = target_db_repo.scm_instance()
504 target_scm = target_db_repo.scm_instance()
505
505
506 commit_ranges = target_scm.compare(
506 commit_ranges = target_scm.compare(
507 target_commit.raw_id, source_commit.raw_id, source_scm,
507 target_commit.raw_id, source_commit.raw_id, source_scm,
508 merge=True, pre_load=[])
508 merge=True, pre_load=[])
509
509
510 ancestor = target_scm.get_common_ancestor(
510 ancestor = target_scm.get_common_ancestor(
511 target_commit.raw_id, source_commit.raw_id, source_scm)
511 target_commit.raw_id, source_commit.raw_id, source_scm)
512
512
513 if not commit_ranges:
513 if not commit_ranges:
514 raise JSONRPCError('no commits found')
514 raise JSONRPCError('no commits found')
515
515
516 if not ancestor:
516 if not ancestor:
517 raise JSONRPCError('no common ancestor found')
517 raise JSONRPCError('no common ancestor found')
518
518
519 reviewer_objects = Optional.extract(reviewers) or []
519 reviewer_objects = Optional.extract(reviewers) or []
520
520
521 if reviewer_objects:
521 if reviewer_objects:
522 schema = ReviewerListSchema()
522 schema = ReviewerListSchema()
523 try:
523 try:
524 reviewer_objects = schema.deserialize(reviewer_objects)
524 reviewer_objects = schema.deserialize(reviewer_objects)
525 except Invalid as err:
525 except Invalid as err:
526 raise JSONRPCValidationError(colander_exc=err)
526 raise JSONRPCValidationError(colander_exc=err)
527
527
528 # validate users
528 # validate users
529 for reviewer_object in reviewer_objects:
529 for reviewer_object in reviewer_objects:
530 user = get_user_or_error(reviewer_object['username'])
530 user = get_user_or_error(reviewer_object['username'])
531 reviewer_object['user_id'] = user.user_id
531 reviewer_object['user_id'] = user.user_id
532
532
533 get_default_reviewers_data, get_validated_reviewers = \
533 get_default_reviewers_data, get_validated_reviewers = \
534 PullRequestModel().get_reviewer_functions()
534 PullRequestModel().get_reviewer_functions()
535
535
536 reviewer_rules = get_default_reviewers_data(
536 reviewer_rules = get_default_reviewers_data(
537 apiuser.get_instance(), source_db_repo,
537 apiuser.get_instance(), source_db_repo,
538 source_commit, target_db_repo, target_commit)
538 source_commit, target_db_repo, target_commit)
539
539
540 # specified rules are later re-validated, thus we can assume users will
540 # specified rules are later re-validated, thus we can assume users will
541 # eventually provide those that meet the reviewer criteria.
541 # eventually provide those that meet the reviewer criteria.
542 if not reviewer_objects:
542 if not reviewer_objects:
543 reviewer_objects = reviewer_rules['reviewers']
543 reviewer_objects = reviewer_rules['reviewers']
544
544
545 try:
545 try:
546 reviewers = get_validated_reviewers(
546 reviewers = get_validated_reviewers(
547 reviewer_objects, reviewer_rules)
547 reviewer_objects, reviewer_rules)
548 except ValueError as e:
548 except ValueError as e:
549 raise JSONRPCError('Reviewers Validation: {}'.format(e))
549 raise JSONRPCError('Reviewers Validation: {}'.format(e))
550
550
551 pull_request_model = PullRequestModel()
551 pull_request_model = PullRequestModel()
552 pull_request = pull_request_model.create(
552 pull_request = pull_request_model.create(
553 created_by=apiuser.user_id,
553 created_by=apiuser.user_id,
554 source_repo=source_repo,
554 source_repo=source_repo,
555 source_ref=full_source_ref,
555 source_ref=full_source_ref,
556 target_repo=target_repo,
556 target_repo=target_repo,
557 target_ref=full_target_ref,
557 target_ref=full_target_ref,
558 revisions=reversed(
558 revisions=reversed(
559 [commit.raw_id for commit in reversed(commit_ranges)]),
559 [commit.raw_id for commit in reversed(commit_ranges)]),
560 reviewers=reviewers,
560 reviewers=reviewers,
561 title=title,
561 title=title,
562 description=Optional.extract(description)
562 description=Optional.extract(description)
563 )
563 )
564
564
565 Session().commit()
565 Session().commit()
566 data = {
566 data = {
567 'msg': 'Created new pull request `{}`'.format(title),
567 'msg': 'Created new pull request `{}`'.format(title),
568 'pull_request_id': pull_request.pull_request_id,
568 'pull_request_id': pull_request.pull_request_id,
569 }
569 }
570 return data
570 return data
571
571
572
572
573 @jsonrpc_method()
573 @jsonrpc_method()
574 def update_pull_request(
574 def update_pull_request(
575 request, apiuser, repoid, pullrequestid, title=Optional(''),
575 request, apiuser, repoid, pullrequestid, title=Optional(''),
576 description=Optional(''), reviewers=Optional(None),
576 description=Optional(''), reviewers=Optional(None),
577 update_commits=Optional(None)):
577 update_commits=Optional(None)):
578 """
578 """
579 Updates a pull request.
579 Updates a pull request.
580
580
581 :param apiuser: This is filled automatically from the |authtoken|.
581 :param apiuser: This is filled automatically from the |authtoken|.
582 :type apiuser: AuthUser
582 :type apiuser: AuthUser
583 :param repoid: The repository name or repository ID.
583 :param repoid: The repository name or repository ID.
584 :type repoid: str or int
584 :type repoid: str or int
585 :param pullrequestid: The pull request ID.
585 :param pullrequestid: The pull request ID.
586 :type pullrequestid: int
586 :type pullrequestid: int
587 :param title: Set the pull request title.
587 :param title: Set the pull request title.
588 :type title: str
588 :type title: str
589 :param description: Update pull request description.
589 :param description: Update pull request description.
590 :type description: Optional(str)
590 :type description: Optional(str)
591 :param reviewers: Update pull request reviewers list with new value.
591 :param reviewers: Update pull request reviewers list with new value.
592 :type reviewers: Optional(list)
592 :type reviewers: Optional(list)
593 Accepts username strings or objects of the format:
593 Accepts username strings or objects of the format:
594
594
595 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
595 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
596
596
597 :param update_commits: Trigger update of commits for this pull request
597 :param update_commits: Trigger update of commits for this pull request
598 :type: update_commits: Optional(bool)
598 :type: update_commits: Optional(bool)
599
599
600 Example output:
600 Example output:
601
601
602 .. code-block:: bash
602 .. code-block:: bash
603
603
604 id : <id_given_in_input>
604 id : <id_given_in_input>
605 result : {
605 result : {
606 "msg": "Updated pull request `63`",
606 "msg": "Updated pull request `63`",
607 "pull_request": <pull_request_object>,
607 "pull_request": <pull_request_object>,
608 "updated_reviewers": {
608 "updated_reviewers": {
609 "added": [
609 "added": [
610 "username"
610 "username"
611 ],
611 ],
612 "removed": []
612 "removed": []
613 },
613 },
614 "updated_commits": {
614 "updated_commits": {
615 "added": [
615 "added": [
616 "<sha1_hash>"
616 "<sha1_hash>"
617 ],
617 ],
618 "common": [
618 "common": [
619 "<sha1_hash>",
619 "<sha1_hash>",
620 "<sha1_hash>",
620 "<sha1_hash>",
621 ],
621 ],
622 "removed": []
622 "removed": []
623 }
623 }
624 }
624 }
625 error : null
625 error : null
626 """
626 """
627
627
628 repo = get_repo_or_error(repoid)
628 repo = get_repo_or_error(repoid)
629 pull_request = get_pull_request_or_error(pullrequestid)
629 pull_request = get_pull_request_or_error(pullrequestid)
630 if not PullRequestModel().check_user_update(
630 if not PullRequestModel().check_user_update(
631 pull_request, apiuser, api=True):
631 pull_request, apiuser, api=True):
632 raise JSONRPCError(
632 raise JSONRPCError(
633 'pull request `%s` update failed, no permission to update.' % (
633 'pull request `%s` update failed, no permission to update.' % (
634 pullrequestid,))
634 pullrequestid,))
635 if pull_request.is_closed():
635 if pull_request.is_closed():
636 raise JSONRPCError(
636 raise JSONRPCError(
637 'pull request `%s` update failed, pull request is closed' % (
637 'pull request `%s` update failed, pull request is closed' % (
638 pullrequestid,))
638 pullrequestid,))
639
639
640 reviewer_objects = Optional.extract(reviewers) or []
640 reviewer_objects = Optional.extract(reviewers) or []
641
641
642 if reviewer_objects:
642 if reviewer_objects:
643 schema = ReviewerListSchema()
643 schema = ReviewerListSchema()
644 try:
644 try:
645 reviewer_objects = schema.deserialize(reviewer_objects)
645 reviewer_objects = schema.deserialize(reviewer_objects)
646 except Invalid as err:
646 except Invalid as err:
647 raise JSONRPCValidationError(colander_exc=err)
647 raise JSONRPCValidationError(colander_exc=err)
648
648
649 # validate users
649 # validate users
650 for reviewer_object in reviewer_objects:
650 for reviewer_object in reviewer_objects:
651 user = get_user_or_error(reviewer_object['username'])
651 user = get_user_or_error(reviewer_object['username'])
652 reviewer_object['user_id'] = user.user_id
652 reviewer_object['user_id'] = user.user_id
653
653
654 get_default_reviewers_data, get_validated_reviewers = \
654 get_default_reviewers_data, get_validated_reviewers = \
655 PullRequestModel().get_reviewer_functions()
655 PullRequestModel().get_reviewer_functions()
656
656
657 # re-use stored rules
657 # re-use stored rules
658 reviewer_rules = pull_request.reviewer_data
658 reviewer_rules = pull_request.reviewer_data
659 try:
659 try:
660 reviewers = get_validated_reviewers(
660 reviewers = get_validated_reviewers(
661 reviewer_objects, reviewer_rules)
661 reviewer_objects, reviewer_rules)
662 except ValueError as e:
662 except ValueError as e:
663 raise JSONRPCError('Reviewers Validation: {}'.format(e))
663 raise JSONRPCError('Reviewers Validation: {}'.format(e))
664 else:
664 else:
665 reviewers = []
665 reviewers = []
666
666
667 title = Optional.extract(title)
667 title = Optional.extract(title)
668 description = Optional.extract(description)
668 description = Optional.extract(description)
669 if title or description:
669 if title or description:
670 PullRequestModel().edit(
670 PullRequestModel().edit(
671 pull_request, title or pull_request.title,
671 pull_request, title or pull_request.title,
672 description or pull_request.description)
672 description or pull_request.description, apiuser)
673 Session().commit()
673 Session().commit()
674
674
675 commit_changes = {"added": [], "common": [], "removed": []}
675 commit_changes = {"added": [], "common": [], "removed": []}
676 if str2bool(Optional.extract(update_commits)):
676 if str2bool(Optional.extract(update_commits)):
677 if PullRequestModel().has_valid_update_type(pull_request):
677 if PullRequestModel().has_valid_update_type(pull_request):
678 update_response = PullRequestModel().update_commits(
678 update_response = PullRequestModel().update_commits(
679 pull_request)
679 pull_request)
680 commit_changes = update_response.changes or commit_changes
680 commit_changes = update_response.changes or commit_changes
681 Session().commit()
681 Session().commit()
682
682
683 reviewers_changes = {"added": [], "removed": []}
683 reviewers_changes = {"added": [], "removed": []}
684 if reviewers:
684 if reviewers:
685 added_reviewers, removed_reviewers = \
685 added_reviewers, removed_reviewers = \
686 PullRequestModel().update_reviewers(pull_request, reviewers)
686 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
687
687
688 reviewers_changes['added'] = sorted(
688 reviewers_changes['added'] = sorted(
689 [get_user_or_error(n).username for n in added_reviewers])
689 [get_user_or_error(n).username for n in added_reviewers])
690 reviewers_changes['removed'] = sorted(
690 reviewers_changes['removed'] = sorted(
691 [get_user_or_error(n).username for n in removed_reviewers])
691 [get_user_or_error(n).username for n in removed_reviewers])
692 Session().commit()
692 Session().commit()
693
693
694 data = {
694 data = {
695 'msg': 'Updated pull request `{}`'.format(
695 'msg': 'Updated pull request `{}`'.format(
696 pull_request.pull_request_id),
696 pull_request.pull_request_id),
697 'pull_request': pull_request.get_api_data(),
697 'pull_request': pull_request.get_api_data(),
698 'updated_commits': commit_changes,
698 'updated_commits': commit_changes,
699 'updated_reviewers': reviewers_changes
699 'updated_reviewers': reviewers_changes
700 }
700 }
701
701
702 return data
702 return data
703
703
704
704
705 @jsonrpc_method()
705 @jsonrpc_method()
706 def close_pull_request(
706 def close_pull_request(
707 request, apiuser, repoid, pullrequestid,
707 request, apiuser, repoid, pullrequestid,
708 userid=Optional(OAttr('apiuser')), message=Optional('')):
708 userid=Optional(OAttr('apiuser')), message=Optional('')):
709 """
709 """
710 Close the pull request specified by `pullrequestid`.
710 Close the pull request specified by `pullrequestid`.
711
711
712 :param apiuser: This is filled automatically from the |authtoken|.
712 :param apiuser: This is filled automatically from the |authtoken|.
713 :type apiuser: AuthUser
713 :type apiuser: AuthUser
714 :param repoid: Repository name or repository ID to which the pull
714 :param repoid: Repository name or repository ID to which the pull
715 request belongs.
715 request belongs.
716 :type repoid: str or int
716 :type repoid: str or int
717 :param pullrequestid: ID of the pull request to be closed.
717 :param pullrequestid: ID of the pull request to be closed.
718 :type pullrequestid: int
718 :type pullrequestid: int
719 :param userid: Close the pull request as this user.
719 :param userid: Close the pull request as this user.
720 :type userid: Optional(str or int)
720 :type userid: Optional(str or int)
721 :param message: Optional message to close the Pull Request with. If not
721 :param message: Optional message to close the Pull Request with. If not
722 specified it will be generated automatically.
722 specified it will be generated automatically.
723 :type message: Optional(str)
723 :type message: Optional(str)
724
724
725 Example output:
725 Example output:
726
726
727 .. code-block:: bash
727 .. code-block:: bash
728
728
729 "id": <id_given_in_input>,
729 "id": <id_given_in_input>,
730 "result": {
730 "result": {
731 "pull_request_id": "<int>",
731 "pull_request_id": "<int>",
732 "close_status": "<str:status_lbl>,
732 "close_status": "<str:status_lbl>,
733 "closed": "<bool>"
733 "closed": "<bool>"
734 },
734 },
735 "error": null
735 "error": null
736
736
737 """
737 """
738 _ = request.translate
738 _ = request.translate
739
739
740 repo = get_repo_or_error(repoid)
740 repo = get_repo_or_error(repoid)
741 if not isinstance(userid, Optional):
741 if not isinstance(userid, Optional):
742 if (has_superadmin_permission(apiuser) or
742 if (has_superadmin_permission(apiuser) or
743 HasRepoPermissionAnyApi('repository.admin')(
743 HasRepoPermissionAnyApi('repository.admin')(
744 user=apiuser, repo_name=repo.repo_name)):
744 user=apiuser, repo_name=repo.repo_name)):
745 apiuser = get_user_or_error(userid)
745 apiuser = get_user_or_error(userid)
746 else:
746 else:
747 raise JSONRPCError('userid is not the same as your user')
747 raise JSONRPCError('userid is not the same as your user')
748
748
749 pull_request = get_pull_request_or_error(pullrequestid)
749 pull_request = get_pull_request_or_error(pullrequestid)
750
750
751 if pull_request.is_closed():
751 if pull_request.is_closed():
752 raise JSONRPCError(
752 raise JSONRPCError(
753 'pull request `%s` is already closed' % (pullrequestid,))
753 'pull request `%s` is already closed' % (pullrequestid,))
754
754
755 # only owner or admin or person with write permissions
755 # only owner or admin or person with write permissions
756 allowed_to_close = PullRequestModel().check_user_update(
756 allowed_to_close = PullRequestModel().check_user_update(
757 pull_request, apiuser, api=True)
757 pull_request, apiuser, api=True)
758
758
759 if not allowed_to_close:
759 if not allowed_to_close:
760 raise JSONRPCError(
760 raise JSONRPCError(
761 'pull request `%s` close failed, no permission to close.' % (
761 'pull request `%s` close failed, no permission to close.' % (
762 pullrequestid,))
762 pullrequestid,))
763
763
764 # message we're using to close the PR, else it's automatically generated
764 # message we're using to close the PR, else it's automatically generated
765 message = Optional.extract(message)
765 message = Optional.extract(message)
766
766
767 # finally close the PR, with proper message comment
767 # finally close the PR, with proper message comment
768 comment, status = PullRequestModel().close_pull_request_with_comment(
768 comment, status = PullRequestModel().close_pull_request_with_comment(
769 pull_request, apiuser, repo, message=message)
769 pull_request, apiuser, repo, message=message)
770 status_lbl = ChangesetStatus.get_status_lbl(status)
770 status_lbl = ChangesetStatus.get_status_lbl(status)
771
771
772 Session().commit()
772 Session().commit()
773
773
774 data = {
774 data = {
775 'pull_request_id': pull_request.pull_request_id,
775 'pull_request_id': pull_request.pull_request_id,
776 'close_status': status_lbl,
776 'close_status': status_lbl,
777 'closed': True,
777 'closed': True,
778 }
778 }
779 return data
779 return data
@@ -1,643 +1,643 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 Users crud controller for pylons
22 Users crud controller for pylons
23 """
23 """
24
24
25 import logging
25 import logging
26 import formencode
26 import formencode
27
27
28 from formencode import htmlfill
28 from formencode import htmlfill
29 from pylons import request, tmpl_context as c, url, config
29 from pylons import request, tmpl_context as c, url, config
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
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
34
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import auth
36 from rhodecode.lib import auth
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator, AuthUser)
39 LoginRequired, HasPermissionAllDecorator, AuthUser)
40 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
42 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
42 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
43 UserOwnsUserGroupsException, UserCreationError)
43 UserOwnsUserGroupsException, UserCreationError)
44 from rhodecode.lib.utils2 import safe_int, AttributeDict
44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45
45
46 from rhodecode.model.db import (
46 from rhodecode.model.db import (
47 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
47 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
48 from rhodecode.model.forms import (
48 from rhodecode.model.forms import (
49 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
49 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.user import UserModel
51 from rhodecode.model.user import UserModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class UsersController(BaseController):
58 class UsersController(BaseController):
59 """REST Controller styled on the Atom Publishing Protocol"""
59 """REST Controller styled on the Atom Publishing Protocol"""
60
60
61 @LoginRequired()
61 @LoginRequired()
62 def __before__(self):
62 def __before__(self):
63 super(UsersController, self).__before__()
63 super(UsersController, self).__before__()
64 c.available_permissions = config['available_permissions']
64 c.available_permissions = config['available_permissions']
65 c.allowed_languages = [
65 c.allowed_languages = [
66 ('en', 'English (en)'),
66 ('en', 'English (en)'),
67 ('de', 'German (de)'),
67 ('de', 'German (de)'),
68 ('fr', 'French (fr)'),
68 ('fr', 'French (fr)'),
69 ('it', 'Italian (it)'),
69 ('it', 'Italian (it)'),
70 ('ja', 'Japanese (ja)'),
70 ('ja', 'Japanese (ja)'),
71 ('pl', 'Polish (pl)'),
71 ('pl', 'Polish (pl)'),
72 ('pt', 'Portuguese (pt)'),
72 ('pt', 'Portuguese (pt)'),
73 ('ru', 'Russian (ru)'),
73 ('ru', 'Russian (ru)'),
74 ('zh', 'Chinese (zh)'),
74 ('zh', 'Chinese (zh)'),
75 ]
75 ]
76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
77
77
78 def _get_personal_repo_group_template_vars(self):
78 def _get_personal_repo_group_template_vars(self):
79 DummyUser = AttributeDict({
79 DummyUser = AttributeDict({
80 'username': '${username}',
80 'username': '${username}',
81 'user_id': '${user_id}',
81 'user_id': '${user_id}',
82 })
82 })
83 c.default_create_repo_group = RepoGroupModel() \
83 c.default_create_repo_group = RepoGroupModel() \
84 .get_default_create_personal_repo_group()
84 .get_default_create_personal_repo_group()
85 c.personal_repo_group_name = RepoGroupModel() \
85 c.personal_repo_group_name = RepoGroupModel() \
86 .get_personal_group_name(DummyUser)
86 .get_personal_group_name(DummyUser)
87
87
88 @HasPermissionAllDecorator('hg.admin')
88 @HasPermissionAllDecorator('hg.admin')
89 @auth.CSRFRequired()
89 @auth.CSRFRequired()
90 def create(self):
90 def create(self):
91 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
91 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
92 user_model = UserModel()
92 user_model = UserModel()
93 user_form = UserForm()()
93 user_form = UserForm()()
94 try:
94 try:
95 form_result = user_form.to_python(dict(request.POST))
95 form_result = user_form.to_python(dict(request.POST))
96 user = user_model.create(form_result)
96 user = user_model.create(form_result)
97 Session().flush()
97 Session().flush()
98 creation_data = user.get_api_data()
98 creation_data = user.get_api_data()
99 username = form_result['username']
99 username = form_result['username']
100
100
101 audit_logger.store_web(
101 audit_logger.store_web(
102 'user.create', action_data={'data': creation_data},
102 'user.create', action_data={'data': creation_data},
103 user=c.rhodecode_user)
103 user=c.rhodecode_user)
104
104
105 user_link = h.link_to(h.escape(username),
105 user_link = h.link_to(h.escape(username),
106 url('edit_user',
106 url('edit_user',
107 user_id=user.user_id))
107 user_id=user.user_id))
108 h.flash(h.literal(_('Created user %(user_link)s')
108 h.flash(h.literal(_('Created user %(user_link)s')
109 % {'user_link': user_link}), category='success')
109 % {'user_link': user_link}), category='success')
110 Session().commit()
110 Session().commit()
111 except formencode.Invalid as errors:
111 except formencode.Invalid as errors:
112 self._get_personal_repo_group_template_vars()
112 self._get_personal_repo_group_template_vars()
113 return htmlfill.render(
113 return htmlfill.render(
114 render('admin/users/user_add.mako'),
114 render('admin/users/user_add.mako'),
115 defaults=errors.value,
115 defaults=errors.value,
116 errors=errors.error_dict or {},
116 errors=errors.error_dict or {},
117 prefix_error=False,
117 prefix_error=False,
118 encoding="UTF-8",
118 encoding="UTF-8",
119 force_defaults=False)
119 force_defaults=False)
120 except UserCreationError as e:
120 except UserCreationError as e:
121 h.flash(e, 'error')
121 h.flash(e, 'error')
122 except Exception:
122 except Exception:
123 log.exception("Exception creation of user")
123 log.exception("Exception creation of user")
124 h.flash(_('Error occurred during creation of user %s')
124 h.flash(_('Error occurred during creation of user %s')
125 % request.POST.get('username'), category='error')
125 % request.POST.get('username'), category='error')
126 return redirect(h.route_path('users'))
126 return redirect(h.route_path('users'))
127
127
128 @HasPermissionAllDecorator('hg.admin')
128 @HasPermissionAllDecorator('hg.admin')
129 def new(self):
129 def new(self):
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')
133
133
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
137
138 user_id = safe_int(user_id)
138 user_id = safe_int(user_id)
139 c.user = User.get_or_404(user_id)
139 c.user = User.get_or_404(user_id)
140 c.active = 'profile'
140 c.active = 'profile'
141 c.extern_type = c.user.extern_type
141 c.extern_type = c.user.extern_type
142 c.extern_name = c.user.extern_name
142 c.extern_name = c.user.extern_name
143 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
143 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
144 available_languages = [x[0] for x in c.allowed_languages]
144 available_languages = [x[0] for x in c.allowed_languages]
145 _form = UserForm(edit=True, available_languages=available_languages,
145 _form = UserForm(edit=True, available_languages=available_languages,
146 old_data={'user_id': user_id,
146 old_data={'user_id': user_id,
147 'email': c.user.email})()
147 'email': c.user.email})()
148 form_result = {}
148 form_result = {}
149 old_values = c.user.get_api_data()
149 old_values = c.user.get_api_data()
150 try:
150 try:
151 form_result = _form.to_python(dict(request.POST))
151 form_result = _form.to_python(dict(request.POST))
152 skip_attrs = ['extern_type', 'extern_name']
152 skip_attrs = ['extern_type', 'extern_name']
153 # TODO: plugin should define if username can be updated
153 # TODO: plugin should define if username can be updated
154 if c.extern_type != "rhodecode":
154 if c.extern_type != "rhodecode":
155 # forbid updating username for external accounts
155 # forbid updating username for external accounts
156 skip_attrs.append('username')
156 skip_attrs.append('username')
157
157
158 UserModel().update_user(
158 UserModel().update_user(
159 user_id, skip_attrs=skip_attrs, **form_result)
159 user_id, skip_attrs=skip_attrs, **form_result)
160
160
161 audit_logger.store_web(
161 audit_logger.store_web(
162 'user.edit', action_data={'old_data': old_values},
162 'user.edit', action_data={'old_data': old_values},
163 user=c.rhodecode_user)
163 user=c.rhodecode_user)
164
164
165 Session().commit()
165 Session().commit()
166 h.flash(_('User updated successfully'), category='success')
166 h.flash(_('User updated successfully'), category='success')
167 except formencode.Invalid as errors:
167 except formencode.Invalid as errors:
168 defaults = errors.value
168 defaults = errors.value
169 e = errors.error_dict or {}
169 e = errors.error_dict or {}
170
170
171 return htmlfill.render(
171 return htmlfill.render(
172 render('admin/users/user_edit.mako'),
172 render('admin/users/user_edit.mako'),
173 defaults=defaults,
173 defaults=defaults,
174 errors=e,
174 errors=e,
175 prefix_error=False,
175 prefix_error=False,
176 encoding="UTF-8",
176 encoding="UTF-8",
177 force_defaults=False)
177 force_defaults=False)
178 except UserCreationError as e:
178 except UserCreationError as e:
179 h.flash(e, 'error')
179 h.flash(e, 'error')
180 except Exception:
180 except Exception:
181 log.exception("Exception updating user")
181 log.exception("Exception updating user")
182 h.flash(_('Error occurred during update of user %s')
182 h.flash(_('Error occurred during update of user %s')
183 % form_result.get('username'), category='error')
183 % form_result.get('username'), category='error')
184 return redirect(url('edit_user', user_id=user_id))
184 return redirect(url('edit_user', user_id=user_id))
185
185
186 @HasPermissionAllDecorator('hg.admin')
186 @HasPermissionAllDecorator('hg.admin')
187 @auth.CSRFRequired()
187 @auth.CSRFRequired()
188 def delete(self, user_id):
188 def delete(self, user_id):
189 user_id = safe_int(user_id)
189 user_id = safe_int(user_id)
190 c.user = User.get_or_404(user_id)
190 c.user = User.get_or_404(user_id)
191
191
192 _repos = c.user.repositories
192 _repos = c.user.repositories
193 _repo_groups = c.user.repository_groups
193 _repo_groups = c.user.repository_groups
194 _user_groups = c.user.user_groups
194 _user_groups = c.user.user_groups
195
195
196 handle_repos = None
196 handle_repos = None
197 handle_repo_groups = None
197 handle_repo_groups = None
198 handle_user_groups = None
198 handle_user_groups = None
199 # dummy call for flash of handle
199 # dummy call for flash of handle
200 set_handle_flash_repos = lambda: None
200 set_handle_flash_repos = lambda: None
201 set_handle_flash_repo_groups = lambda: None
201 set_handle_flash_repo_groups = lambda: None
202 set_handle_flash_user_groups = lambda: None
202 set_handle_flash_user_groups = lambda: None
203
203
204 if _repos and request.POST.get('user_repos'):
204 if _repos and request.POST.get('user_repos'):
205 do = request.POST['user_repos']
205 do = request.POST['user_repos']
206 if do == 'detach':
206 if do == 'detach':
207 handle_repos = 'detach'
207 handle_repos = 'detach'
208 set_handle_flash_repos = lambda: h.flash(
208 set_handle_flash_repos = lambda: h.flash(
209 _('Detached %s repositories') % len(_repos),
209 _('Detached %s repositories') % len(_repos),
210 category='success')
210 category='success')
211 elif do == 'delete':
211 elif do == 'delete':
212 handle_repos = 'delete'
212 handle_repos = 'delete'
213 set_handle_flash_repos = lambda: h.flash(
213 set_handle_flash_repos = lambda: h.flash(
214 _('Deleted %s repositories') % len(_repos),
214 _('Deleted %s repositories') % len(_repos),
215 category='success')
215 category='success')
216
216
217 if _repo_groups and request.POST.get('user_repo_groups'):
217 if _repo_groups and request.POST.get('user_repo_groups'):
218 do = request.POST['user_repo_groups']
218 do = request.POST['user_repo_groups']
219 if do == 'detach':
219 if do == 'detach':
220 handle_repo_groups = 'detach'
220 handle_repo_groups = 'detach'
221 set_handle_flash_repo_groups = lambda: h.flash(
221 set_handle_flash_repo_groups = lambda: h.flash(
222 _('Detached %s repository groups') % len(_repo_groups),
222 _('Detached %s repository groups') % len(_repo_groups),
223 category='success')
223 category='success')
224 elif do == 'delete':
224 elif do == 'delete':
225 handle_repo_groups = 'delete'
225 handle_repo_groups = 'delete'
226 set_handle_flash_repo_groups = lambda: h.flash(
226 set_handle_flash_repo_groups = lambda: h.flash(
227 _('Deleted %s repository groups') % len(_repo_groups),
227 _('Deleted %s repository groups') % len(_repo_groups),
228 category='success')
228 category='success')
229
229
230 if _user_groups and request.POST.get('user_user_groups'):
230 if _user_groups and request.POST.get('user_user_groups'):
231 do = request.POST['user_user_groups']
231 do = request.POST['user_user_groups']
232 if do == 'detach':
232 if do == 'detach':
233 handle_user_groups = 'detach'
233 handle_user_groups = 'detach'
234 set_handle_flash_user_groups = lambda: h.flash(
234 set_handle_flash_user_groups = lambda: h.flash(
235 _('Detached %s user groups') % len(_user_groups),
235 _('Detached %s user groups') % len(_user_groups),
236 category='success')
236 category='success')
237 elif do == 'delete':
237 elif do == 'delete':
238 handle_user_groups = 'delete'
238 handle_user_groups = 'delete'
239 set_handle_flash_user_groups = lambda: h.flash(
239 set_handle_flash_user_groups = lambda: h.flash(
240 _('Deleted %s user groups') % len(_user_groups),
240 _('Deleted %s user groups') % len(_user_groups),
241 category='success')
241 category='success')
242
242
243 old_values = c.user.get_api_data()
243 old_values = c.user.get_api_data()
244 try:
244 try:
245 UserModel().delete(c.user, handle_repos=handle_repos,
245 UserModel().delete(c.user, handle_repos=handle_repos,
246 handle_repo_groups=handle_repo_groups,
246 handle_repo_groups=handle_repo_groups,
247 handle_user_groups=handle_user_groups)
247 handle_user_groups=handle_user_groups)
248
248
249 audit_logger.store_web(
249 audit_logger.store_web(
250 'user.delete', action_data={'old_data': old_values},
250 'user.delete', action_data={'old_data': old_values},
251 user=c.rhodecode_user)
251 user=c.rhodecode_user)
252
252
253 Session().commit()
253 Session().commit()
254 set_handle_flash_repos()
254 set_handle_flash_repos()
255 set_handle_flash_repo_groups()
255 set_handle_flash_repo_groups()
256 set_handle_flash_user_groups()
256 set_handle_flash_user_groups()
257 h.flash(_('Successfully deleted user'), category='success')
257 h.flash(_('Successfully deleted user'), category='success')
258 except (UserOwnsReposException, UserOwnsRepoGroupsException,
258 except (UserOwnsReposException, UserOwnsRepoGroupsException,
259 UserOwnsUserGroupsException, DefaultUserException) as e:
259 UserOwnsUserGroupsException, DefaultUserException) as e:
260 h.flash(e, category='warning')
260 h.flash(e, category='warning')
261 except Exception:
261 except Exception:
262 log.exception("Exception during deletion of user")
262 log.exception("Exception during deletion of user")
263 h.flash(_('An error occurred during deletion of user'),
263 h.flash(_('An error occurred during deletion of user'),
264 category='error')
264 category='error')
265 return redirect(h.route_path('users'))
265 return redirect(h.route_path('users'))
266
266
267 @HasPermissionAllDecorator('hg.admin')
267 @HasPermissionAllDecorator('hg.admin')
268 @auth.CSRFRequired()
268 @auth.CSRFRequired()
269 def reset_password(self, user_id):
269 def reset_password(self, user_id):
270 """
270 """
271 toggle reset password flag for this user
271 toggle reset password flag for this user
272 """
272 """
273 user_id = safe_int(user_id)
273 user_id = safe_int(user_id)
274 c.user = User.get_or_404(user_id)
274 c.user = User.get_or_404(user_id)
275 try:
275 try:
276 old_value = c.user.user_data.get('force_password_change')
276 old_value = c.user.user_data.get('force_password_change')
277 c.user.update_userdata(force_password_change=not old_value)
277 c.user.update_userdata(force_password_change=not old_value)
278
278
279 if old_value:
279 if old_value:
280 msg = _('Force password change disabled for user')
280 msg = _('Force password change disabled for user')
281 audit_logger.store_web(
281 audit_logger.store_web(
282 'user.edit.password_reset.disabled',
282 'user.edit.password_reset.disabled',
283 user=c.rhodecode_user)
283 user=c.rhodecode_user)
284 else:
284 else:
285 msg = _('Force password change enabled for user')
285 msg = _('Force password change enabled for user')
286 audit_logger.store_web(
286 audit_logger.store_web(
287 'user.edit.password_reset.enabled',
287 'user.edit.password_reset.enabled',
288 user=c.rhodecode_user)
288 user=c.rhodecode_user)
289
289
290 Session().commit()
290 Session().commit()
291 h.flash(msg, category='success')
291 h.flash(msg, category='success')
292 except Exception:
292 except Exception:
293 log.exception("Exception during password reset for user")
293 log.exception("Exception during password reset for user")
294 h.flash(_('An error occurred during password reset for user'),
294 h.flash(_('An error occurred during password reset for user'),
295 category='error')
295 category='error')
296
296
297 return redirect(url('edit_user_advanced', user_id=user_id))
297 return redirect(url('edit_user_advanced', user_id=user_id))
298
298
299 @HasPermissionAllDecorator('hg.admin')
299 @HasPermissionAllDecorator('hg.admin')
300 @auth.CSRFRequired()
300 @auth.CSRFRequired()
301 def create_personal_repo_group(self, user_id):
301 def create_personal_repo_group(self, user_id):
302 """
302 """
303 Create personal repository group for this user
303 Create personal repository group for this user
304 """
304 """
305 from rhodecode.model.repo_group import RepoGroupModel
305 from rhodecode.model.repo_group import RepoGroupModel
306
306
307 user_id = safe_int(user_id)
307 user_id = safe_int(user_id)
308 c.user = User.get_or_404(user_id)
308 c.user = User.get_or_404(user_id)
309 personal_repo_group = RepoGroup.get_user_personal_repo_group(
309 personal_repo_group = RepoGroup.get_user_personal_repo_group(
310 c.user.user_id)
310 c.user.user_id)
311 if personal_repo_group:
311 if personal_repo_group:
312 return redirect(url('edit_user_advanced', user_id=user_id))
312 return redirect(url('edit_user_advanced', user_id=user_id))
313
313
314 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
314 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
315 c.user)
315 c.user)
316 named_personal_group = RepoGroup.get_by_group_name(
316 named_personal_group = RepoGroup.get_by_group_name(
317 personal_repo_group_name)
317 personal_repo_group_name)
318 try:
318 try:
319
319
320 if named_personal_group and named_personal_group.user_id == c.user.user_id:
320 if named_personal_group and named_personal_group.user_id == c.user.user_id:
321 # migrate the same named group, and mark it as personal
321 # migrate the same named group, and mark it as personal
322 named_personal_group.personal = True
322 named_personal_group.personal = True
323 Session().add(named_personal_group)
323 Session().add(named_personal_group)
324 Session().commit()
324 Session().commit()
325 msg = _('Linked repository group `%s` as personal' % (
325 msg = _('Linked repository group `%s` as personal' % (
326 personal_repo_group_name,))
326 personal_repo_group_name,))
327 h.flash(msg, category='success')
327 h.flash(msg, category='success')
328 elif not named_personal_group:
328 elif not named_personal_group:
329 RepoGroupModel().create_personal_repo_group(c.user)
329 RepoGroupModel().create_personal_repo_group(c.user)
330
330
331 msg = _('Created repository group `%s`' % (
331 msg = _('Created repository group `%s`' % (
332 personal_repo_group_name,))
332 personal_repo_group_name,))
333 h.flash(msg, category='success')
333 h.flash(msg, category='success')
334 else:
334 else:
335 msg = _('Repository group `%s` is already taken' % (
335 msg = _('Repository group `%s` is already taken' % (
336 personal_repo_group_name,))
336 personal_repo_group_name,))
337 h.flash(msg, category='warning')
337 h.flash(msg, category='warning')
338 except Exception:
338 except Exception:
339 log.exception("Exception during repository group creation")
339 log.exception("Exception during repository group creation")
340 msg = _(
340 msg = _(
341 'An error occurred during repository group creation for user')
341 'An error occurred during repository group creation for user')
342 h.flash(msg, category='error')
342 h.flash(msg, category='error')
343 Session().rollback()
343 Session().rollback()
344
344
345 return redirect(url('edit_user_advanced', user_id=user_id))
345 return redirect(url('edit_user_advanced', user_id=user_id))
346
346
347 @HasPermissionAllDecorator('hg.admin')
347 @HasPermissionAllDecorator('hg.admin')
348 def show(self, user_id):
348 def show(self, user_id):
349 """GET /users/user_id: Show a specific item"""
349 """GET /users/user_id: Show a specific item"""
350 # url('user', user_id=ID)
350 # url('user', user_id=ID)
351 User.get_or_404(-1)
351 User.get_or_404(-1)
352
352
353 @HasPermissionAllDecorator('hg.admin')
353 @HasPermissionAllDecorator('hg.admin')
354 def edit(self, user_id):
354 def edit(self, user_id):
355 """GET /users/user_id/edit: Form to edit an existing item"""
355 """GET /users/user_id/edit: Form to edit an existing item"""
356 # url('edit_user', user_id=ID)
356 # url('edit_user', user_id=ID)
357 user_id = safe_int(user_id)
357 user_id = safe_int(user_id)
358 c.user = User.get_or_404(user_id)
358 c.user = User.get_or_404(user_id)
359 if c.user.username == User.DEFAULT_USER:
359 if c.user.username == User.DEFAULT_USER:
360 h.flash(_("You can't edit this user"), category='warning')
360 h.flash(_("You can't edit this user"), category='warning')
361 return redirect(h.route_path('users'))
361 return redirect(h.route_path('users'))
362
362
363 c.active = 'profile'
363 c.active = 'profile'
364 c.extern_type = c.user.extern_type
364 c.extern_type = c.user.extern_type
365 c.extern_name = c.user.extern_name
365 c.extern_name = c.user.extern_name
366 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
366 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
367
367
368 defaults = c.user.get_dict()
368 defaults = c.user.get_dict()
369 defaults.update({'language': c.user.user_data.get('language')})
369 defaults.update({'language': c.user.user_data.get('language')})
370 return htmlfill.render(
370 return htmlfill.render(
371 render('admin/users/user_edit.mako'),
371 render('admin/users/user_edit.mako'),
372 defaults=defaults,
372 defaults=defaults,
373 encoding="UTF-8",
373 encoding="UTF-8",
374 force_defaults=False)
374 force_defaults=False)
375
375
376 @HasPermissionAllDecorator('hg.admin')
376 @HasPermissionAllDecorator('hg.admin')
377 def edit_advanced(self, user_id):
377 def edit_advanced(self, user_id):
378 user_id = safe_int(user_id)
378 user_id = safe_int(user_id)
379 user = c.user = User.get_or_404(user_id)
379 user = c.user = User.get_or_404(user_id)
380 if user.username == User.DEFAULT_USER:
380 if user.username == User.DEFAULT_USER:
381 h.flash(_("You can't edit this user"), category='warning')
381 h.flash(_("You can't edit this user"), category='warning')
382 return redirect(h.route_path('users'))
382 return redirect(h.route_path('users'))
383
383
384 c.active = 'advanced'
384 c.active = 'advanced'
385 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
385 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
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()
389 defaults = user.get_dict()
389 defaults = user.get_dict()
390
390
391 # Interim workaround if the user participated on any pull requests as a
391 # Interim workaround if the user participated on any pull requests as a
392 # reviewer.
392 # reviewer.
393 has_review = bool(PullRequestReviewers.query().filter(
393 has_review = bool(PullRequestReviewers.query().filter(
394 PullRequestReviewers.user_id == user_id).first())
394 PullRequestReviewers.user_id == user_id).first())
395 c.can_delete_user = not has_review
395 c.can_delete_user = not has_review
396 c.can_delete_user_message = _(
396 c.can_delete_user_message = _(
397 'The user participates as reviewer in pull requests and '
397 'The user participates as reviewer in pull requests and '
398 'cannot be deleted. You can set the user to '
398 'cannot be deleted. You can set the user to '
399 '"inactive" instead of deleting it.') if has_review else ''
399 '"inactive" instead of deleting it.') if has_review else ''
400
400
401 return htmlfill.render(
401 return htmlfill.render(
402 render('admin/users/user_edit.mako'),
402 render('admin/users/user_edit.mako'),
403 defaults=defaults,
403 defaults=defaults,
404 encoding="UTF-8",
404 encoding="UTF-8",
405 force_defaults=False)
405 force_defaults=False)
406
406
407 @HasPermissionAllDecorator('hg.admin')
407 @HasPermissionAllDecorator('hg.admin')
408 def edit_global_perms(self, user_id):
408 def edit_global_perms(self, user_id):
409 user_id = safe_int(user_id)
409 user_id = safe_int(user_id)
410 c.user = User.get_or_404(user_id)
410 c.user = User.get_or_404(user_id)
411 if c.user.username == User.DEFAULT_USER:
411 if c.user.username == User.DEFAULT_USER:
412 h.flash(_("You can't edit this user"), category='warning')
412 h.flash(_("You can't edit this user"), category='warning')
413 return redirect(h.route_path('users'))
413 return redirect(h.route_path('users'))
414
414
415 c.active = 'global_perms'
415 c.active = 'global_perms'
416
416
417 c.default_user = User.get_default_user()
417 c.default_user = User.get_default_user()
418 defaults = c.user.get_dict()
418 defaults = c.user.get_dict()
419 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
419 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
420 defaults.update(c.default_user.get_default_perms())
420 defaults.update(c.default_user.get_default_perms())
421 defaults.update(c.user.get_default_perms())
421 defaults.update(c.user.get_default_perms())
422
422
423 return htmlfill.render(
423 return htmlfill.render(
424 render('admin/users/user_edit.mako'),
424 render('admin/users/user_edit.mako'),
425 defaults=defaults,
425 defaults=defaults,
426 encoding="UTF-8",
426 encoding="UTF-8",
427 force_defaults=False)
427 force_defaults=False)
428
428
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 user_id = safe_int(user_id)
432 user_id = safe_int(user_id)
433 user = User.get_or_404(user_id)
433 user = User.get_or_404(user_id)
434 c.active = 'global_perms'
434 c.active = 'global_perms'
435 try:
435 try:
436 # first stage that verifies the checkbox
436 # first stage that verifies the checkbox
437 _form = UserIndividualPermissionsForm()
437 _form = UserIndividualPermissionsForm()
438 form_result = _form.to_python(dict(request.POST))
438 form_result = _form.to_python(dict(request.POST))
439 inherit_perms = form_result['inherit_default_permissions']
439 inherit_perms = form_result['inherit_default_permissions']
440 user.inherit_default_permissions = inherit_perms
440 user.inherit_default_permissions = inherit_perms
441 Session().add(user)
441 Session().add(user)
442
442
443 if not inherit_perms:
443 if not inherit_perms:
444 # only update the individual ones if we un check the flag
444 # only update the individual ones if we un check the flag
445 _form = UserPermissionsForm(
445 _form = UserPermissionsForm(
446 [x[0] for x in c.repo_create_choices],
446 [x[0] for x in c.repo_create_choices],
447 [x[0] for x in c.repo_create_on_write_choices],
447 [x[0] for x in c.repo_create_on_write_choices],
448 [x[0] for x in c.repo_group_create_choices],
448 [x[0] for x in c.repo_group_create_choices],
449 [x[0] for x in c.user_group_create_choices],
449 [x[0] for x in c.user_group_create_choices],
450 [x[0] for x in c.fork_choices],
450 [x[0] for x in c.fork_choices],
451 [x[0] for x in c.inherit_default_permission_choices])()
451 [x[0] for x in c.inherit_default_permission_choices])()
452
452
453 form_result = _form.to_python(dict(request.POST))
453 form_result = _form.to_python(dict(request.POST))
454 form_result.update({'perm_user_id': user.user_id})
454 form_result.update({'perm_user_id': user.user_id})
455
455
456 PermissionModel().update_user_permissions(form_result)
456 PermissionModel().update_user_permissions(form_result)
457
457
458 # TODO(marcink): implement global permissions
458 # TODO(marcink): implement global permissions
459 # audit_log.store_web('user.edit.permissions')
459 # audit_log.store_web('user.edit.permissions')
460
460
461 Session().commit()
461 Session().commit()
462 h.flash(_('User global permissions updated successfully'),
462 h.flash(_('User global permissions updated successfully'),
463 category='success')
463 category='success')
464
464
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
468 return htmlfill.render(
468 return htmlfill.render(
469 render('admin/users/user_edit.mako'),
469 render('admin/users/user_edit.mako'),
470 defaults=defaults,
470 defaults=defaults,
471 errors=errors.error_dict or {},
471 errors=errors.error_dict or {},
472 prefix_error=False,
472 prefix_error=False,
473 encoding="UTF-8",
473 encoding="UTF-8",
474 force_defaults=False)
474 force_defaults=False)
475 except Exception:
475 except Exception:
476 log.exception("Exception during permissions saving")
476 log.exception("Exception during permissions saving")
477 h.flash(_('An error occurred during permissions saving'),
477 h.flash(_('An error occurred during permissions saving'),
478 category='error')
478 category='error')
479 return redirect(url('edit_user_global_perms', user_id=user_id))
479 return redirect(url('edit_user_global_perms', user_id=user_id))
480
480
481 @HasPermissionAllDecorator('hg.admin')
481 @HasPermissionAllDecorator('hg.admin')
482 def edit_perms_summary(self, user_id):
482 def edit_perms_summary(self, user_id):
483 user_id = safe_int(user_id)
483 user_id = safe_int(user_id)
484 c.user = User.get_or_404(user_id)
484 c.user = User.get_or_404(user_id)
485 if c.user.username == User.DEFAULT_USER:
485 if c.user.username == User.DEFAULT_USER:
486 h.flash(_("You can't edit this user"), category='warning')
486 h.flash(_("You can't edit this user"), category='warning')
487 return redirect(h.route_path('users'))
487 return redirect(h.route_path('users'))
488
488
489 c.active = 'perms_summary'
489 c.active = 'perms_summary'
490 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
490 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
491
491
492 return render('admin/users/user_edit.mako')
492 return render('admin/users/user_edit.mako')
493
493
494 @HasPermissionAllDecorator('hg.admin')
494 @HasPermissionAllDecorator('hg.admin')
495 def edit_emails(self, user_id):
495 def edit_emails(self, user_id):
496 user_id = safe_int(user_id)
496 user_id = safe_int(user_id)
497 c.user = User.get_or_404(user_id)
497 c.user = User.get_or_404(user_id)
498 if c.user.username == User.DEFAULT_USER:
498 if c.user.username == User.DEFAULT_USER:
499 h.flash(_("You can't edit this user"), category='warning')
499 h.flash(_("You can't edit this user"), category='warning')
500 return redirect(h.route_path('users'))
500 return redirect(h.route_path('users'))
501
501
502 c.active = 'emails'
502 c.active = 'emails'
503 c.user_email_map = UserEmailMap.query() \
503 c.user_email_map = UserEmailMap.query() \
504 .filter(UserEmailMap.user == c.user).all()
504 .filter(UserEmailMap.user == c.user).all()
505
505
506 defaults = c.user.get_dict()
506 defaults = c.user.get_dict()
507 return htmlfill.render(
507 return htmlfill.render(
508 render('admin/users/user_edit.mako'),
508 render('admin/users/user_edit.mako'),
509 defaults=defaults,
509 defaults=defaults,
510 encoding="UTF-8",
510 encoding="UTF-8",
511 force_defaults=False)
511 force_defaults=False)
512
512
513 @HasPermissionAllDecorator('hg.admin')
513 @HasPermissionAllDecorator('hg.admin')
514 @auth.CSRFRequired()
514 @auth.CSRFRequired()
515 def add_email(self, user_id):
515 def add_email(self, user_id):
516 user_id = safe_int(user_id)
516 user_id = safe_int(user_id)
517 c.user = User.get_or_404(user_id)
517 c.user = User.get_or_404(user_id)
518
518
519 email = request.POST.get('new_email')
519 email = request.POST.get('new_email')
520 user_model = UserModel()
520 user_model = UserModel()
521 user_data = c.user.get_api_data()
521 user_data = c.user.get_api_data()
522 try:
522 try:
523 user_model.add_extra_email(user_id, email)
523 user_model.add_extra_email(user_id, email)
524 audit_logger.store_web(
524 audit_logger.store_web(
525 'user.edit.email.add',
525 'user.edit.email.add',
526 action_data={'email': email, 'user': user_data},
526 action_data={'email': email, 'user': user_data},
527 user=c.rhodecode_user)
527 user=c.rhodecode_user)
528 Session().commit()
528 Session().commit()
529 h.flash(_("Added new email address `%s` for user account") % email,
529 h.flash(_("Added new email address `%s` for user account") % email,
530 category='success')
530 category='success')
531 except formencode.Invalid as error:
531 except formencode.Invalid as error:
532 msg = error.error_dict['email']
532 msg = error.error_dict['email']
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 except Exception:
534 except Exception:
535 log.exception("Exception during email saving")
535 log.exception("Exception during email saving")
536 h.flash(_('An error occurred during email saving'),
536 h.flash(_('An error occurred during email saving'),
537 category='error')
537 category='error')
538 return redirect(url('edit_user_emails', user_id=user_id))
538 return redirect(url('edit_user_emails', user_id=user_id))
539
539
540 @HasPermissionAllDecorator('hg.admin')
540 @HasPermissionAllDecorator('hg.admin')
541 @auth.CSRFRequired()
541 @auth.CSRFRequired()
542 def delete_email(self, user_id):
542 def delete_email(self, user_id):
543 user_id = safe_int(user_id)
543 user_id = safe_int(user_id)
544 c.user = User.get_or_404(user_id)
544 c.user = User.get_or_404(user_id)
545 email_id = request.POST.get('del_email_id')
545 email_id = request.POST.get('del_email_id')
546 user_model = UserModel()
546 user_model = UserModel()
547
547
548 email = UserEmailMap.query().get(email_id).email
548 email = UserEmailMap.query().get(email_id).email
549 user_data = c.user.get_api_data()
549 user_data = c.user.get_api_data()
550 user_model.delete_extra_email(user_id, email_id)
550 user_model.delete_extra_email(user_id, email_id)
551 audit_logger.store_web(
551 audit_logger.store_web(
552 'user.edit.email.delete',
552 'user.edit.email.delete',
553 action_data={'email': email, 'user': user_data},
553 action_data={'email': email, 'user': user_data},
554 user=c.rhodecode_user)
554 user=c.rhodecode_user)
555 Session().commit()
555 Session().commit()
556 h.flash(_("Removed email address from user account"), category='success')
556 h.flash(_("Removed email address from user account"), category='success')
557 return redirect(url('edit_user_emails', user_id=user_id))
557 return redirect(url('edit_user_emails', user_id=user_id))
558
558
559 @HasPermissionAllDecorator('hg.admin')
559 @HasPermissionAllDecorator('hg.admin')
560 def edit_ips(self, user_id):
560 def edit_ips(self, user_id):
561 user_id = safe_int(user_id)
561 user_id = safe_int(user_id)
562 c.user = User.get_or_404(user_id)
562 c.user = User.get_or_404(user_id)
563 if c.user.username == User.DEFAULT_USER:
563 if c.user.username == User.DEFAULT_USER:
564 h.flash(_("You can't edit this user"), category='warning')
564 h.flash(_("You can't edit this user"), category='warning')
565 return redirect(h.route_path('users'))
565 return redirect(h.route_path('users'))
566
566
567 c.active = 'ips'
567 c.active = 'ips'
568 c.user_ip_map = UserIpMap.query() \
568 c.user_ip_map = UserIpMap.query() \
569 .filter(UserIpMap.user == c.user).all()
569 .filter(UserIpMap.user == c.user).all()
570
570
571 c.inherit_default_ips = c.user.inherit_default_permissions
571 c.inherit_default_ips = c.user.inherit_default_permissions
572 c.default_user_ip_map = UserIpMap.query() \
572 c.default_user_ip_map = UserIpMap.query() \
573 .filter(UserIpMap.user == User.get_default_user()).all()
573 .filter(UserIpMap.user == User.get_default_user()).all()
574
574
575 defaults = c.user.get_dict()
575 defaults = c.user.get_dict()
576 return htmlfill.render(
576 return htmlfill.render(
577 render('admin/users/user_edit.mako'),
577 render('admin/users/user_edit.mako'),
578 defaults=defaults,
578 defaults=defaults,
579 encoding="UTF-8",
579 encoding="UTF-8",
580 force_defaults=False)
580 force_defaults=False)
581
581
582 @HasPermissionAllDecorator('hg.admin')
582 @HasPermissionAllDecorator('hg.admin')
583 @auth.CSRFRequired()
583 @auth.CSRFRequired()
584 def add_ip(self, user_id):
584 def add_ip(self, user_id):
585 user_id = safe_int(user_id)
585 user_id = safe_int(user_id)
586 c.user = User.get_or_404(user_id)
586 c.user = User.get_or_404(user_id)
587 user_model = UserModel()
587 user_model = UserModel()
588 try:
588 try:
589 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
589 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
590 except Exception as e:
590 except Exception as e:
591 ip_list = []
591 ip_list = []
592 log.exception("Exception during ip saving")
592 log.exception("Exception during ip saving")
593 h.flash(_('An error occurred during ip saving:%s' % (e,)),
593 h.flash(_('An error occurred during ip saving:%s' % (e,)),
594 category='error')
594 category='error')
595
595
596 desc = request.POST.get('description')
596 desc = request.POST.get('description')
597 added = []
597 added = []
598 user_data = c.user.get_api_data()
598 user_data = c.user.get_api_data()
599 for ip in ip_list:
599 for ip in ip_list:
600 try:
600 try:
601 user_model.add_extra_ip(user_id, ip, desc)
601 user_model.add_extra_ip(user_id, ip, desc)
602 audit_logger.store_web(
602 audit_logger.store_web(
603 'user.edit.ip.add',
603 'user.edit.ip.add',
604 action_data={'ip': ip, 'user': user_data},
604 action_data={'ip': ip, 'user': user_data},
605 user=c.rhodecode_user)
605 user=c.rhodecode_user)
606 Session().commit()
606 Session().commit()
607 added.append(ip)
607 added.append(ip)
608 except formencode.Invalid as error:
608 except formencode.Invalid as error:
609 msg = error.error_dict['ip']
609 msg = error.error_dict['ip']
610 h.flash(msg, category='error')
610 h.flash(msg, category='error')
611 except Exception:
611 except Exception:
612 log.exception("Exception during ip saving")
612 log.exception("Exception during ip saving")
613 h.flash(_('An error occurred during ip saving'),
613 h.flash(_('An error occurred during ip saving'),
614 category='error')
614 category='error')
615 if added:
615 if added:
616 h.flash(
616 h.flash(
617 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
617 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
618 category='success')
618 category='success')
619 if 'default_user' in request.POST:
619 if 'default_user' in request.POST:
620 return redirect(url('admin_permissions_ips'))
620 return redirect(url('admin_permissions_ips'))
621 return redirect(url('edit_user_ips', user_id=user_id))
621 return redirect(url('edit_user_ips', user_id=user_id))
622
622
623 @HasPermissionAllDecorator('hg.admin')
623 @HasPermissionAllDecorator('hg.admin')
624 @auth.CSRFRequired()
624 @auth.CSRFRequired()
625 def delete_ip(self, user_id):
625 def delete_ip(self, user_id):
626 user_id = safe_int(user_id)
626 user_id = safe_int(user_id)
627 c.user = User.get_or_404(user_id)
627 c.user = User.get_or_404(user_id)
628
628
629 ip_id = request.POST.get('del_ip_id')
629 ip_id = request.POST.get('del_ip_id')
630 user_model = UserModel()
630 user_model = UserModel()
631 user_data = c.user.get_api_data()
631 ip = UserIpMap.query().get(ip_id).ip_addr
632 ip = UserIpMap.query().get(ip_id).ip_addr
632 user_data = c.user.get_api_data()
633 user_model.delete_extra_ip(user_id, ip_id)
633 user_model.delete_extra_ip(user_id, ip_id)
634 audit_logger.store_web(
634 audit_logger.store_web(
635 'user.edit.ip.delete',
635 'user.edit.ip.delete',
636 action_data={'ip': ip, 'user': user_data},
636 action_data={'ip': ip, 'user': user_data},
637 user=c.rhodecode_user)
637 user=c.rhodecode_user)
638 Session().commit()
638 Session().commit()
639 h.flash(_("Removed ip address from user whitelist"), category='success')
639 h.flash(_("Removed ip address from user whitelist"), category='success')
640
640
641 if 'default_user' in request.POST:
641 if 'default_user' in request.POST:
642 return redirect(url('admin_permissions_ips'))
642 return redirect(url('admin_permissions_ips'))
643 return redirect(url('edit_user_ips', user_id=user_id))
643 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,484 +1,484 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
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 jsonify
42 from rhodecode.lib.utils import jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
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)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
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
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 def _update_with_GET(params, GET):
56 def _update_with_GET(params, GET):
57 for k in ['diff1', 'diff2', 'diff']:
57 for k in ['diff1', 'diff2', 'diff']:
58 params[k] += GET.getall(k)
58 params[k] += GET.getall(k)
59
59
60
60
61 def get_ignore_ws(fid, GET):
61 def get_ignore_ws(fid, GET):
62 ig_ws_global = GET.get('ignorews')
62 ig_ws_global = GET.get('ignorews')
63 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
63 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 if ig_ws:
64 if ig_ws:
65 try:
65 try:
66 return int(ig_ws[0].split(':')[-1])
66 return int(ig_ws[0].split(':')[-1])
67 except Exception:
67 except Exception:
68 pass
68 pass
69 return ig_ws_global
69 return ig_ws_global
70
70
71
71
72 def _ignorews_url(GET, fileid=None):
72 def _ignorews_url(GET, fileid=None):
73 fileid = str(fileid) if fileid else None
73 fileid = str(fileid) if fileid else None
74 params = defaultdict(list)
74 params = defaultdict(list)
75 _update_with_GET(params, GET)
75 _update_with_GET(params, GET)
76 label = _('Show whitespace')
76 label = _('Show whitespace')
77 tooltiplbl = _('Show whitespace for all diffs')
77 tooltiplbl = _('Show whitespace for all diffs')
78 ig_ws = get_ignore_ws(fileid, GET)
78 ig_ws = get_ignore_ws(fileid, GET)
79 ln_ctx = get_line_ctx(fileid, GET)
79 ln_ctx = get_line_ctx(fileid, GET)
80
80
81 if ig_ws is None:
81 if ig_ws is None:
82 params['ignorews'] += [1]
82 params['ignorews'] += [1]
83 label = _('Ignore whitespace')
83 label = _('Ignore whitespace')
84 tooltiplbl = _('Ignore whitespace for all diffs')
84 tooltiplbl = _('Ignore whitespace for all diffs')
85 ctx_key = 'context'
85 ctx_key = 'context'
86 ctx_val = ln_ctx
86 ctx_val = ln_ctx
87
87
88 # if we have passed in ln_ctx pass it along to our params
88 # if we have passed in ln_ctx pass it along to our params
89 if ln_ctx:
89 if ln_ctx:
90 params[ctx_key] += [ctx_val]
90 params[ctx_key] += [ctx_val]
91
91
92 if fileid:
92 if fileid:
93 params['anchor'] = 'a_' + fileid
93 params['anchor'] = 'a_' + fileid
94 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
94 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95
95
96
96
97 def get_line_ctx(fid, GET):
97 def get_line_ctx(fid, GET):
98 ln_ctx_global = GET.get('context')
98 ln_ctx_global = GET.get('context')
99 if fid:
99 if fid:
100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
100 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 else:
101 else:
102 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
102 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
103 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 if ln_ctx:
104 if ln_ctx:
105 ln_ctx = [ln_ctx]
105 ln_ctx = [ln_ctx]
106
106
107 if ln_ctx:
107 if ln_ctx:
108 retval = ln_ctx[0].split(':')[-1]
108 retval = ln_ctx[0].split(':')[-1]
109 else:
109 else:
110 retval = ln_ctx_global
110 retval = ln_ctx_global
111
111
112 try:
112 try:
113 return int(retval)
113 return int(retval)
114 except Exception:
114 except Exception:
115 return 3
115 return 3
116
116
117
117
118 def _context_url(GET, fileid=None):
118 def _context_url(GET, fileid=None):
119 """
119 """
120 Generates a url for context lines.
120 Generates a url for context lines.
121
121
122 :param fileid:
122 :param fileid:
123 """
123 """
124
124
125 fileid = str(fileid) if fileid else None
125 fileid = str(fileid) if fileid else None
126 ig_ws = get_ignore_ws(fileid, GET)
126 ig_ws = get_ignore_ws(fileid, GET)
127 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
127 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128
128
129 params = defaultdict(list)
129 params = defaultdict(list)
130 _update_with_GET(params, GET)
130 _update_with_GET(params, GET)
131
131
132 if ln_ctx > 0:
132 if ln_ctx > 0:
133 params['context'] += [ln_ctx]
133 params['context'] += [ln_ctx]
134
134
135 if ig_ws:
135 if ig_ws:
136 ig_ws_key = 'ignorews'
136 ig_ws_key = 'ignorews'
137 ig_ws_val = 1
137 ig_ws_val = 1
138 params[ig_ws_key] += [ig_ws_val]
138 params[ig_ws_key] += [ig_ws_val]
139
139
140 lbl = _('Increase context')
140 lbl = _('Increase context')
141 tooltiplbl = _('Increase context for all diffs')
141 tooltiplbl = _('Increase context for all diffs')
142
142
143 if fileid:
143 if fileid:
144 params['anchor'] = 'a_' + fileid
144 params['anchor'] = 'a_' + fileid
145 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
145 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146
146
147
147
148 class ChangesetController(BaseRepoController):
148 class ChangesetController(BaseRepoController):
149
149
150 def __before__(self):
150 def __before__(self):
151 super(ChangesetController, self).__before__()
151 super(ChangesetController, self).__before__()
152 c.affected_files_cut_off = 60
152 c.affected_files_cut_off = 60
153
153
154 def _index(self, commit_id_range, method):
154 def _index(self, commit_id_range, method):
155 c.ignorews_url = _ignorews_url
155 c.ignorews_url = _ignorews_url
156 c.context_url = _context_url
156 c.context_url = _context_url
157 c.fulldiff = fulldiff = request.GET.get('fulldiff')
157 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158
158
159 # fetch global flags of ignore ws or context lines
159 # fetch global flags of ignore ws or context lines
160 context_lcl = get_line_ctx('', request.GET)
160 context_lcl = get_line_ctx('', request.GET)
161 ign_whitespace_lcl = get_ignore_ws('', request.GET)
161 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162
162
163 # diff_limit will cut off the whole diff if the limit is applied
163 # diff_limit will cut off the whole diff if the limit is applied
164 # otherwise it will just hide the big files from the front-end
164 # otherwise it will just hide the big files from the front-end
165 diff_limit = self.cut_off_limit_diff
165 diff_limit = self.cut_off_limit_diff
166 file_limit = self.cut_off_limit_file
166 file_limit = self.cut_off_limit_file
167
167
168 # get ranges of commit ids if preset
168 # get ranges of commit ids if preset
169 commit_range = commit_id_range.split('...')[:2]
169 commit_range = commit_id_range.split('...')[:2]
170
170
171 try:
171 try:
172 pre_load = ['affected_files', 'author', 'branch', 'date',
172 pre_load = ['affected_files', 'author', 'branch', 'date',
173 'message', 'parents']
173 'message', 'parents']
174
174
175 if len(commit_range) == 2:
175 if len(commit_range) == 2:
176 commits = c.rhodecode_repo.get_commits(
176 commits = c.rhodecode_repo.get_commits(
177 start_id=commit_range[0], end_id=commit_range[1],
177 start_id=commit_range[0], end_id=commit_range[1],
178 pre_load=pre_load)
178 pre_load=pre_load)
179 commits = list(commits)
179 commits = list(commits)
180 else:
180 else:
181 commits = [c.rhodecode_repo.get_commit(
181 commits = [c.rhodecode_repo.get_commit(
182 commit_id=commit_id_range, pre_load=pre_load)]
182 commit_id=commit_id_range, pre_load=pre_load)]
183
183
184 c.commit_ranges = commits
184 c.commit_ranges = commits
185 if not c.commit_ranges:
185 if not c.commit_ranges:
186 raise RepositoryError(
186 raise RepositoryError(
187 'The commit range returned an empty result')
187 'The commit range returned an empty result')
188 except CommitDoesNotExistError:
188 except CommitDoesNotExistError:
189 msg = _('No such commit exists for this repository')
189 msg = _('No such commit exists for this repository')
190 h.flash(msg, category='error')
190 h.flash(msg, category='error')
191 raise HTTPNotFound()
191 raise HTTPNotFound()
192 except Exception:
192 except Exception:
193 log.exception("General failure")
193 log.exception("General failure")
194 raise HTTPNotFound()
194 raise HTTPNotFound()
195
195
196 c.changes = OrderedDict()
196 c.changes = OrderedDict()
197 c.lines_added = 0
197 c.lines_added = 0
198 c.lines_deleted = 0
198 c.lines_deleted = 0
199
199
200 # auto collapse if we have more than limit
200 # auto collapse if we have more than limit
201 collapse_limit = diffs.DiffProcessor._collapse_commits_over
201 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
202 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203
203
204 c.commit_statuses = ChangesetStatus.STATUSES
204 c.commit_statuses = ChangesetStatus.STATUSES
205 c.inline_comments = []
205 c.inline_comments = []
206 c.files = []
206 c.files = []
207
207
208 c.statuses = []
208 c.statuses = []
209 c.comments = []
209 c.comments = []
210 c.unresolved_comments = []
210 c.unresolved_comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = CommentsModel().get_comments(
213 c.comments = CommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 c.unresolved_comments = CommentsModel()\
229 c.unresolved_comments = CommentsModel()\
230 .get_commit_unresolved_todos(commit.raw_id)
230 .get_commit_unresolved_todos(commit.raw_id)
231
231
232 # Iterate over ranges (default commit view is always one commit)
232 # Iterate over ranges (default commit view is always one commit)
233 for commit in c.commit_ranges:
233 for commit in c.commit_ranges:
234 c.changes[commit.raw_id] = []
234 c.changes[commit.raw_id] = []
235
235
236 commit2 = commit
236 commit2 = commit
237 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
237 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238
238
239 _diff = c.rhodecode_repo.get_diff(
239 _diff = c.rhodecode_repo.get_diff(
240 commit1, commit2,
240 commit1, commit2,
241 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
241 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 diff_processor = diffs.DiffProcessor(
242 diff_processor = diffs.DiffProcessor(
243 _diff, format='newdiff', diff_limit=diff_limit,
243 _diff, format='newdiff', diff_limit=diff_limit,
244 file_limit=file_limit, show_full_diff=fulldiff)
244 file_limit=file_limit, show_full_diff=fulldiff)
245
245
246 commit_changes = OrderedDict()
246 commit_changes = OrderedDict()
247 if method == 'show':
247 if method == 'show':
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
249 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250
250
251 _parsed = diff_processor.prepare()
251 _parsed = diff_processor.prepare()
252
252
253 def _node_getter(commit):
253 def _node_getter(commit):
254 def get_node(fname):
254 def get_node(fname):
255 try:
255 try:
256 return commit.get_node(fname)
256 return commit.get_node(fname)
257 except NodeDoesNotExistError:
257 except NodeDoesNotExistError:
258 return None
258 return None
259 return get_node
259 return get_node
260
260
261 inline_comments = CommentsModel().get_inline_comments(
261 inline_comments = CommentsModel().get_inline_comments(
262 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
262 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 c.inline_cnt = CommentsModel().get_inline_comments_count(
263 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 inline_comments)
264 inline_comments)
265
265
266 diffset = codeblocks.DiffSet(
266 diffset = codeblocks.DiffSet(
267 repo_name=c.repo_name,
267 repo_name=c.repo_name,
268 source_node_getter=_node_getter(commit1),
268 source_node_getter=_node_getter(commit1),
269 target_node_getter=_node_getter(commit2),
269 target_node_getter=_node_getter(commit2),
270 comments=inline_comments
270 comments=inline_comments
271 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
271 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
272 c.changes[commit.raw_id] = diffset
272 c.changes[commit.raw_id] = diffset
273 else:
273 else:
274 # downloads/raw we only need RAW diff nothing else
274 # downloads/raw we only need RAW diff nothing else
275 diff = diff_processor.as_raw()
275 diff = diff_processor.as_raw()
276 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
276 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
277
277
278 # sort comments by how they were generated
278 # sort comments by how they were generated
279 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
279 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
280
280
281 if len(c.commit_ranges) == 1:
281 if len(c.commit_ranges) == 1:
282 c.commit = c.commit_ranges[0]
282 c.commit = c.commit_ranges[0]
283 c.parent_tmpl = ''.join(
283 c.parent_tmpl = ''.join(
284 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
284 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 if method == 'download':
285 if method == 'download':
286 response.content_type = 'text/plain'
286 response.content_type = 'text/plain'
287 response.content_disposition = (
287 response.content_disposition = (
288 'attachment; filename=%s.diff' % commit_id_range[:12])
288 'attachment; filename=%s.diff' % commit_id_range[:12])
289 return diff
289 return diff
290 elif method == 'patch':
290 elif method == 'patch':
291 response.content_type = 'text/plain'
291 response.content_type = 'text/plain'
292 c.diff = safe_unicode(diff)
292 c.diff = safe_unicode(diff)
293 return render('changeset/patch_changeset.mako')
293 return render('changeset/patch_changeset.mako')
294 elif method == 'raw':
294 elif method == 'raw':
295 response.content_type = 'text/plain'
295 response.content_type = 'text/plain'
296 return diff
296 return diff
297 elif method == 'show':
297 elif method == 'show':
298 if len(c.commit_ranges) == 1:
298 if len(c.commit_ranges) == 1:
299 return render('changeset/changeset.mako')
299 return render('changeset/changeset.mako')
300 else:
300 else:
301 c.ancestor = None
301 c.ancestor = None
302 c.target_repo = c.rhodecode_db_repo
302 c.target_repo = c.rhodecode_db_repo
303 return render('changeset/changeset_range.mako')
303 return render('changeset/changeset_range.mako')
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 'repository.admin')
307 'repository.admin')
308 def index(self, revision, method='show'):
308 def index(self, revision, method='show'):
309 return self._index(revision, method=method)
309 return self._index(revision, method=method)
310
310
311 @LoginRequired()
311 @LoginRequired()
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 'repository.admin')
313 'repository.admin')
314 def changeset_raw(self, revision):
314 def changeset_raw(self, revision):
315 return self._index(revision, method='raw')
315 return self._index(revision, method='raw')
316
316
317 @LoginRequired()
317 @LoginRequired()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
319 'repository.admin')
320 def changeset_patch(self, revision):
320 def changeset_patch(self, revision):
321 return self._index(revision, method='patch')
321 return self._index(revision, method='patch')
322
322
323 @LoginRequired()
323 @LoginRequired()
324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 'repository.admin')
325 'repository.admin')
326 def changeset_download(self, revision):
326 def changeset_download(self, revision):
327 return self._index(revision, method='download')
327 return self._index(revision, method='download')
328
328
329 @LoginRequired()
329 @LoginRequired()
330 @NotAnonymous()
330 @NotAnonymous()
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 'repository.admin')
332 'repository.admin')
333 @auth.CSRFRequired()
333 @auth.CSRFRequired()
334 @jsonify
334 @jsonify
335 def comment(self, repo_name, revision):
335 def comment(self, repo_name, revision):
336 commit_id = revision
336 commit_id = revision
337 status = request.POST.get('changeset_status', None)
337 status = request.POST.get('changeset_status', None)
338 text = request.POST.get('text')
338 text = request.POST.get('text')
339 comment_type = request.POST.get('comment_type')
339 comment_type = request.POST.get('comment_type')
340 resolves_comment_id = request.POST.get('resolves_comment_id', None)
340 resolves_comment_id = request.POST.get('resolves_comment_id', None)
341
341
342 if status:
342 if status:
343 text = text or (_('Status change %(transition_icon)s %(status)s')
343 text = text or (_('Status change %(transition_icon)s %(status)s')
344 % {'transition_icon': '>',
344 % {'transition_icon': '>',
345 'status': ChangesetStatus.get_status_lbl(status)})
345 'status': ChangesetStatus.get_status_lbl(status)})
346
346
347 multi_commit_ids = []
347 multi_commit_ids = []
348 for _commit_id in request.POST.get('commit_ids', '').split(','):
348 for _commit_id in request.POST.get('commit_ids', '').split(','):
349 if _commit_id not in ['', None, EmptyCommit.raw_id]:
349 if _commit_id not in ['', None, EmptyCommit.raw_id]:
350 if _commit_id not in multi_commit_ids:
350 if _commit_id not in multi_commit_ids:
351 multi_commit_ids.append(_commit_id)
351 multi_commit_ids.append(_commit_id)
352
352
353 commit_ids = multi_commit_ids or [commit_id]
353 commit_ids = multi_commit_ids or [commit_id]
354
354
355 comment = None
355 comment = None
356 for current_id in filter(None, commit_ids):
356 for current_id in filter(None, commit_ids):
357 c.co = comment = CommentsModel().create(
357 c.co = comment = CommentsModel().create(
358 text=text,
358 text=text,
359 repo=c.rhodecode_db_repo.repo_id,
359 repo=c.rhodecode_db_repo.repo_id,
360 user=c.rhodecode_user.user_id,
360 user=c.rhodecode_user.user_id,
361 commit_id=current_id,
361 commit_id=current_id,
362 f_path=request.POST.get('f_path'),
362 f_path=request.POST.get('f_path'),
363 line_no=request.POST.get('line'),
363 line_no=request.POST.get('line'),
364 status_change=(ChangesetStatus.get_status_lbl(status)
364 status_change=(ChangesetStatus.get_status_lbl(status)
365 if status else None),
365 if status else None),
366 status_change_type=status,
366 status_change_type=status,
367 comment_type=comment_type,
367 comment_type=comment_type,
368 resolves_comment_id=resolves_comment_id
368 resolves_comment_id=resolves_comment_id
369 )
369 )
370
370
371 # get status if set !
371 # get status if set !
372 if status:
372 if status:
373 # if latest status was from pull request and it's closed
373 # if latest status was from pull request and it's closed
374 # disallow changing status !
374 # disallow changing status !
375 # dont_allow_on_closed_pull_request = True !
375 # dont_allow_on_closed_pull_request = True !
376
376
377 try:
377 try:
378 ChangesetStatusModel().set_status(
378 ChangesetStatusModel().set_status(
379 c.rhodecode_db_repo.repo_id,
379 c.rhodecode_db_repo.repo_id,
380 status,
380 status,
381 c.rhodecode_user.user_id,
381 c.rhodecode_user.user_id,
382 comment,
382 comment,
383 revision=current_id,
383 revision=current_id,
384 dont_allow_on_closed_pull_request=True
384 dont_allow_on_closed_pull_request=True
385 )
385 )
386 except StatusChangeOnClosedPullRequestError:
386 except StatusChangeOnClosedPullRequestError:
387 msg = _('Changing the status of a commit associated with '
387 msg = _('Changing the status of a commit associated with '
388 'a closed pull request is not allowed')
388 'a closed pull request is not allowed')
389 log.exception(msg)
389 log.exception(msg)
390 h.flash(msg, category='warning')
390 h.flash(msg, category='warning')
391 return redirect(h.url(
391 return redirect(h.url(
392 'changeset_home', repo_name=repo_name,
392 'changeset_home', repo_name=repo_name,
393 revision=current_id))
393 revision=current_id))
394
394
395 # finalize, commit and redirect
395 # finalize, commit and redirect
396 Session().commit()
396 Session().commit()
397
397
398 data = {
398 data = {
399 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
399 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 }
400 }
401 if comment:
401 if comment:
402 data.update(comment.get_dict())
402 data.update(comment.get_dict())
403 data.update({'rendered_text':
403 data.update({'rendered_text':
404 render('changeset/changeset_comment_block.mako')})
404 render('changeset/changeset_comment_block.mako')})
405
405
406 return data
406 return data
407
407
408 @LoginRequired()
408 @LoginRequired()
409 @NotAnonymous()
409 @NotAnonymous()
410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
410 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 'repository.admin')
411 'repository.admin')
412 @auth.CSRFRequired()
412 @auth.CSRFRequired()
413 def preview_comment(self):
413 def preview_comment(self):
414 # Technically a CSRF token is not needed as no state changes with this
414 # Technically a CSRF token is not needed as no state changes with this
415 # call. However, as this is a POST is better to have it, so automated
415 # call. However, as this is a POST is better to have it, so automated
416 # tools don't flag it as potential CSRF.
416 # tools don't flag it as potential CSRF.
417 # Post is required because the payload could be bigger than the maximum
417 # Post is required because the payload could be bigger than the maximum
418 # allowed by GET.
418 # allowed by GET.
419 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
419 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
420 raise HTTPBadRequest()
420 raise HTTPBadRequest()
421 text = request.POST.get('text')
421 text = request.POST.get('text')
422 renderer = request.POST.get('renderer') or 'rst'
422 renderer = request.POST.get('renderer') or 'rst'
423 if text:
423 if text:
424 return h.render(text, renderer=renderer, mentions=True)
424 return h.render(text, renderer=renderer, mentions=True)
425 return ''
425 return ''
426
426
427 @LoginRequired()
427 @LoginRequired()
428 @NotAnonymous()
428 @NotAnonymous()
429 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
429 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 'repository.admin')
430 'repository.admin')
431 @auth.CSRFRequired()
431 @auth.CSRFRequired()
432 @jsonify
432 @jsonify
433 def delete_comment(self, repo_name, comment_id):
433 def delete_comment(self, repo_name, comment_id):
434 comment = ChangesetComment.get(comment_id)
434 comment = ChangesetComment.get(comment_id)
435 if not comment:
435 if not comment:
436 log.debug('Comment with id:%s not found, skipping', comment_id)
436 log.debug('Comment with id:%s not found, skipping', comment_id)
437 # comment already deleted in another call probably
437 # comment already deleted in another call probably
438 return True
438 return True
439
439
440 owner = (comment.author.user_id == c.rhodecode_user.user_id)
440 owner = (comment.author.user_id == c.rhodecode_user.user_id)
441 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
441 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
442 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
442 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
443 CommentsModel().delete(comment=comment)
443 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
444 Session().commit()
444 Session().commit()
445 return True
445 return True
446 else:
446 else:
447 raise HTTPForbidden()
447 raise HTTPForbidden()
448
448
449 @LoginRequired()
449 @LoginRequired()
450 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
451 'repository.admin')
451 'repository.admin')
452 @jsonify
452 @jsonify
453 def changeset_info(self, repo_name, revision):
453 def changeset_info(self, repo_name, revision):
454 if request.is_xhr:
454 if request.is_xhr:
455 try:
455 try:
456 return c.rhodecode_repo.get_commit(commit_id=revision)
456 return c.rhodecode_repo.get_commit(commit_id=revision)
457 except CommitDoesNotExistError as e:
457 except CommitDoesNotExistError as e:
458 return EmptyCommit(message=str(e))
458 return EmptyCommit(message=str(e))
459 else:
459 else:
460 raise HTTPBadRequest()
460 raise HTTPBadRequest()
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
463 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 'repository.admin')
464 'repository.admin')
465 @jsonify
465 @jsonify
466 def changeset_children(self, repo_name, revision):
466 def changeset_children(self, repo_name, revision):
467 if request.is_xhr:
467 if request.is_xhr:
468 commit = c.rhodecode_repo.get_commit(commit_id=revision)
468 commit = c.rhodecode_repo.get_commit(commit_id=revision)
469 result = {"results": commit.children}
469 result = {"results": commit.children}
470 return result
470 return result
471 else:
471 else:
472 raise HTTPBadRequest()
472 raise HTTPBadRequest()
473
473
474 @LoginRequired()
474 @LoginRequired()
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
476 'repository.admin')
476 'repository.admin')
477 @jsonify
477 @jsonify
478 def changeset_parents(self, repo_name, revision):
478 def changeset_parents(self, repo_name, revision):
479 if request.is_xhr:
479 if request.is_xhr:
480 commit = c.rhodecode_repo.get_commit(commit_id=revision)
480 commit = c.rhodecode_repo.get_commit(commit_id=revision)
481 result = {"results": commit.parents}
481 result = {"results": commit.parents}
482 return result
482 return result
483 else:
483 else:
484 raise HTTPBadRequest()
484 raise HTTPBadRequest()
@@ -1,1110 +1,1110 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 Files controller for RhodeCode Enterprise
22 Files controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 import os
27 import os
28 import shutil
28 import shutil
29 import tempfile
29 import tempfile
30
30
31 from pylons import request, response, tmpl_context as c, url
31 from pylons import request, response, tmpl_context as c, url
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from webob.exc import HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPNotFound, HTTPBadRequest
35
35
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import diffs, helpers as h, caches
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.codeblocks import (
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
43 convert_line_endings, detect_mode, safe_str, str2bool)
43 convert_line_endings, detect_mode, safe_str, str2bool)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 from rhodecode.lib.base import BaseRepoController, render
46 from rhodecode.lib.base import BaseRepoController, render
47 from rhodecode.lib.vcs import path as vcspath
47 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.conf import settings
49 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 NodeDoesNotExistError, CommitError, NodeError)
53 NodeDoesNotExistError, CommitError, NodeError)
54 from rhodecode.lib.vcs.nodes import FileNode
54 from rhodecode.lib.vcs.nodes import FileNode
55
55
56 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
59
59
60 from rhodecode.controllers.changeset import (
60 from rhodecode.controllers.changeset import (
61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 from rhodecode.lib.exceptions import NonRelativePathError
62 from rhodecode.lib.exceptions import NonRelativePathError
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class FilesController(BaseRepoController):
67 class FilesController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(FilesController, self).__before__()
70 super(FilesController, self).__before__()
71 c.cut_off_limit = self.cut_off_limit_file
71 c.cut_off_limit = self.cut_off_limit_file
72
72
73 def _get_default_encoding(self):
73 def _get_default_encoding(self):
74 enc_list = getattr(c, 'default_encodings', [])
74 enc_list = getattr(c, 'default_encodings', [])
75 return enc_list[0] if enc_list else 'UTF-8'
75 return enc_list[0] if enc_list else 'UTF-8'
76
76
77 def __get_commit_or_redirect(self, commit_id, repo_name,
77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 redirect_after=True):
78 redirect_after=True):
79 """
79 """
80 This is a safe way to get commit. If an error occurs it redirects to
80 This is a safe way to get commit. If an error occurs it redirects to
81 tip with proper message
81 tip with proper message
82
82
83 :param commit_id: id of commit to fetch
83 :param commit_id: id of commit to fetch
84 :param repo_name: repo name to redirect after
84 :param repo_name: repo name to redirect after
85 :param redirect_after: toggle redirection
85 :param redirect_after: toggle redirection
86 """
86 """
87 try:
87 try:
88 return c.rhodecode_repo.get_commit(commit_id)
88 return c.rhodecode_repo.get_commit(commit_id)
89 except EmptyRepositoryError:
89 except EmptyRepositoryError:
90 if not redirect_after:
90 if not redirect_after:
91 return None
91 return None
92 url_ = url('files_add_home',
92 url_ = url('files_add_home',
93 repo_name=c.repo_name,
93 repo_name=c.repo_name,
94 revision=0, f_path='', anchor='edit')
94 revision=0, f_path='', anchor='edit')
95 if h.HasRepoPermissionAny(
95 if h.HasRepoPermissionAny(
96 'repository.write', 'repository.admin')(c.repo_name):
96 'repository.write', 'repository.admin')(c.repo_name):
97 add_new = h.link_to(
97 add_new = h.link_to(
98 _('Click here to add a new file.'),
98 _('Click here to add a new file.'),
99 url_, class_="alert-link")
99 url_, class_="alert-link")
100 else:
100 else:
101 add_new = ""
101 add_new = ""
102 h.flash(h.literal(
102 h.flash(h.literal(
103 _('There are no files yet. %s') % add_new), category='warning')
103 _('There are no files yet. %s') % add_new), category='warning')
104 redirect(h.route_path('repo_summary', repo_name=repo_name))
104 redirect(h.route_path('repo_summary', repo_name=repo_name))
105 except (CommitDoesNotExistError, LookupError):
105 except (CommitDoesNotExistError, LookupError):
106 msg = _('No such commit exists for this repository')
106 msg = _('No such commit exists for this repository')
107 h.flash(msg, category='error')
107 h.flash(msg, category='error')
108 raise HTTPNotFound()
108 raise HTTPNotFound()
109 except RepositoryError as e:
109 except RepositoryError as e:
110 h.flash(safe_str(e), category='error')
110 h.flash(safe_str(e), category='error')
111 raise HTTPNotFound()
111 raise HTTPNotFound()
112
112
113 def __get_filenode_or_redirect(self, repo_name, commit, path):
113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 """
114 """
115 Returns file_node, if error occurs or given path is directory,
115 Returns file_node, if error occurs or given path is directory,
116 it'll redirect to top level path
116 it'll redirect to top level path
117
117
118 :param repo_name: repo_name
118 :param repo_name: repo_name
119 :param commit: given commit
119 :param commit: given commit
120 :param path: path to lookup
120 :param path: path to lookup
121 """
121 """
122 try:
122 try:
123 file_node = commit.get_node(path)
123 file_node = commit.get_node(path)
124 if file_node.is_dir():
124 if file_node.is_dir():
125 raise RepositoryError('The given path is a directory')
125 raise RepositoryError('The given path is a directory')
126 except CommitDoesNotExistError:
126 except CommitDoesNotExistError:
127 msg = _('No such commit exists for this repository')
127 msg = _('No such commit exists for this repository')
128 log.exception(msg)
128 log.exception(msg)
129 h.flash(msg, category='error')
129 h.flash(msg, category='error')
130 raise HTTPNotFound()
130 raise HTTPNotFound()
131 except RepositoryError as e:
131 except RepositoryError as e:
132 h.flash(safe_str(e), category='error')
132 h.flash(safe_str(e), category='error')
133 raise HTTPNotFound()
133 raise HTTPNotFound()
134
134
135 return file_node
135 return file_node
136
136
137 def __get_tree_cache_manager(self, repo_name, namespace_type):
137 def __get_tree_cache_manager(self, repo_name, namespace_type):
138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
139 return caches.get_cache_manager('repo_cache_long', _namespace)
139 return caches.get_cache_manager('repo_cache_long', _namespace)
140
140
141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
142 full_load=False, force=False):
142 full_load=False, force=False):
143 def _cached_tree():
143 def _cached_tree():
144 log.debug('Generating cached file tree for %s, %s, %s',
144 log.debug('Generating cached file tree for %s, %s, %s',
145 repo_name, commit_id, f_path)
145 repo_name, commit_id, f_path)
146 c.full_load = full_load
146 c.full_load = full_load
147 return render('files/files_browser_tree.mako')
147 return render('files/files_browser_tree.mako')
148
148
149 cache_manager = self.__get_tree_cache_manager(
149 cache_manager = self.__get_tree_cache_manager(
150 repo_name, caches.FILE_TREE)
150 repo_name, caches.FILE_TREE)
151
151
152 cache_key = caches.compute_key_from_params(
152 cache_key = caches.compute_key_from_params(
153 repo_name, commit_id, f_path)
153 repo_name, commit_id, f_path)
154
154
155 if force:
155 if force:
156 # we want to force recompute of caches
156 # we want to force recompute of caches
157 cache_manager.remove_value(cache_key)
157 cache_manager.remove_value(cache_key)
158
158
159 return cache_manager.get(cache_key, createfunc=_cached_tree)
159 return cache_manager.get(cache_key, createfunc=_cached_tree)
160
160
161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
162 def _cached_nodes():
162 def _cached_nodes():
163 log.debug('Generating cached nodelist for %s, %s, %s',
163 log.debug('Generating cached nodelist for %s, %s, %s',
164 repo_name, commit_id, f_path)
164 repo_name, commit_id, f_path)
165 _d, _f = ScmModel().get_nodes(
165 _d, _f = ScmModel().get_nodes(
166 repo_name, commit_id, f_path, flat=False)
166 repo_name, commit_id, f_path, flat=False)
167 return _d + _f
167 return _d + _f
168
168
169 cache_manager = self.__get_tree_cache_manager(
169 cache_manager = self.__get_tree_cache_manager(
170 repo_name, caches.FILE_SEARCH_TREE_META)
170 repo_name, caches.FILE_SEARCH_TREE_META)
171
171
172 cache_key = caches.compute_key_from_params(
172 cache_key = caches.compute_key_from_params(
173 repo_name, commit_id, f_path)
173 repo_name, commit_id, f_path)
174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
175
175
176 @LoginRequired()
176 @LoginRequired()
177 @HasRepoPermissionAnyDecorator(
177 @HasRepoPermissionAnyDecorator(
178 'repository.read', 'repository.write', 'repository.admin')
178 'repository.read', 'repository.write', 'repository.admin')
179 def index(
179 def index(
180 self, repo_name, revision, f_path, annotate=False, rendered=False):
180 self, repo_name, revision, f_path, annotate=False, rendered=False):
181 commit_id = revision
181 commit_id = revision
182
182
183 # redirect to given commit_id from form if given
183 # redirect to given commit_id from form if given
184 get_commit_id = request.GET.get('at_rev', None)
184 get_commit_id = request.GET.get('at_rev', None)
185 if get_commit_id:
185 if get_commit_id:
186 self.__get_commit_or_redirect(get_commit_id, repo_name)
186 self.__get_commit_or_redirect(get_commit_id, repo_name)
187
187
188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
189 c.branch = request.GET.get('branch', None)
189 c.branch = request.GET.get('branch', None)
190 c.f_path = f_path
190 c.f_path = f_path
191 c.annotate = annotate
191 c.annotate = annotate
192 # default is false, but .rst/.md files later are autorendered, we can
192 # default is false, but .rst/.md files later are autorendered, we can
193 # overwrite autorendering by setting this GET flag
193 # overwrite autorendering by setting this GET flag
194 c.renderer = rendered or not request.GET.get('no-render', False)
194 c.renderer = rendered or not request.GET.get('no-render', False)
195
195
196 # prev link
196 # prev link
197 try:
197 try:
198 prev_commit = c.commit.prev(c.branch)
198 prev_commit = c.commit.prev(c.branch)
199 c.prev_commit = prev_commit
199 c.prev_commit = prev_commit
200 c.url_prev = url('files_home', repo_name=c.repo_name,
200 c.url_prev = url('files_home', repo_name=c.repo_name,
201 revision=prev_commit.raw_id, f_path=f_path)
201 revision=prev_commit.raw_id, f_path=f_path)
202 if c.branch:
202 if c.branch:
203 c.url_prev += '?branch=%s' % c.branch
203 c.url_prev += '?branch=%s' % c.branch
204 except (CommitDoesNotExistError, VCSError):
204 except (CommitDoesNotExistError, VCSError):
205 c.url_prev = '#'
205 c.url_prev = '#'
206 c.prev_commit = EmptyCommit()
206 c.prev_commit = EmptyCommit()
207
207
208 # next link
208 # next link
209 try:
209 try:
210 next_commit = c.commit.next(c.branch)
210 next_commit = c.commit.next(c.branch)
211 c.next_commit = next_commit
211 c.next_commit = next_commit
212 c.url_next = url('files_home', repo_name=c.repo_name,
212 c.url_next = url('files_home', repo_name=c.repo_name,
213 revision=next_commit.raw_id, f_path=f_path)
213 revision=next_commit.raw_id, f_path=f_path)
214 if c.branch:
214 if c.branch:
215 c.url_next += '?branch=%s' % c.branch
215 c.url_next += '?branch=%s' % c.branch
216 except (CommitDoesNotExistError, VCSError):
216 except (CommitDoesNotExistError, VCSError):
217 c.url_next = '#'
217 c.url_next = '#'
218 c.next_commit = EmptyCommit()
218 c.next_commit = EmptyCommit()
219
219
220 # files or dirs
220 # files or dirs
221 try:
221 try:
222 c.file = c.commit.get_node(f_path)
222 c.file = c.commit.get_node(f_path)
223 c.file_author = True
223 c.file_author = True
224 c.file_tree = ''
224 c.file_tree = ''
225 if c.file.is_file():
225 if c.file.is_file():
226 c.lf_node = c.file.get_largefile_node()
226 c.lf_node = c.file.get_largefile_node()
227
227
228 c.file_source_page = 'true'
228 c.file_source_page = 'true'
229 c.file_last_commit = c.file.last_commit
229 c.file_last_commit = c.file.last_commit
230 if c.file.size < self.cut_off_limit_file:
230 if c.file.size < self.cut_off_limit_file:
231 if c.annotate: # annotation has precedence over renderer
231 if c.annotate: # annotation has precedence over renderer
232 c.annotated_lines = filenode_as_annotated_lines_tokens(
232 c.annotated_lines = filenode_as_annotated_lines_tokens(
233 c.file
233 c.file
234 )
234 )
235 else:
235 else:
236 c.renderer = (
236 c.renderer = (
237 c.renderer and h.renderer_from_filename(c.file.path)
237 c.renderer and h.renderer_from_filename(c.file.path)
238 )
238 )
239 if not c.renderer:
239 if not c.renderer:
240 c.lines = filenode_as_lines_tokens(c.file)
240 c.lines = filenode_as_lines_tokens(c.file)
241
241
242 c.on_branch_head = self._is_valid_head(
242 c.on_branch_head = self._is_valid_head(
243 commit_id, c.rhodecode_repo)
243 commit_id, c.rhodecode_repo)
244
244
245 branch = c.commit.branch if (
245 branch = c.commit.branch if (
246 c.commit.branch and '/' not in c.commit.branch) else None
246 c.commit.branch and '/' not in c.commit.branch) else None
247 c.branch_or_raw_id = branch or c.commit.raw_id
247 c.branch_or_raw_id = branch or c.commit.raw_id
248 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
248 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
249
249
250 author = c.file_last_commit.author
250 author = c.file_last_commit.author
251 c.authors = [(h.email(author),
251 c.authors = [(h.email(author),
252 h.person(author, 'username_or_name_or_email'))]
252 h.person(author, 'username_or_name_or_email'))]
253 else:
253 else:
254 c.file_source_page = 'false'
254 c.file_source_page = 'false'
255 c.authors = []
255 c.authors = []
256 c.file_tree = self._get_tree_at_commit(
256 c.file_tree = self._get_tree_at_commit(
257 repo_name, c.commit.raw_id, f_path)
257 repo_name, c.commit.raw_id, f_path)
258
258
259 except RepositoryError as e:
259 except RepositoryError as e:
260 h.flash(safe_str(e), category='error')
260 h.flash(safe_str(e), category='error')
261 raise HTTPNotFound()
261 raise HTTPNotFound()
262
262
263 if request.environ.get('HTTP_X_PJAX'):
263 if request.environ.get('HTTP_X_PJAX'):
264 return render('files/files_pjax.mako')
264 return render('files/files_pjax.mako')
265
265
266 return render('files/files.mako')
266 return render('files/files.mako')
267
267
268 @LoginRequired()
268 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
269 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
270 'repository.read', 'repository.write', 'repository.admin')
271 def annotate_previous(self, repo_name, revision, f_path):
271 def annotate_previous(self, repo_name, revision, f_path):
272
272
273 commit_id = revision
273 commit_id = revision
274 commit = self.__get_commit_or_redirect(commit_id, repo_name)
274 commit = self.__get_commit_or_redirect(commit_id, repo_name)
275 prev_commit_id = commit.raw_id
275 prev_commit_id = commit.raw_id
276
276
277 f_path = f_path
277 f_path = f_path
278 is_file = False
278 is_file = False
279 try:
279 try:
280 _file = commit.get_node(f_path)
280 _file = commit.get_node(f_path)
281 is_file = _file.is_file()
281 is_file = _file.is_file()
282 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
282 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
283 pass
283 pass
284
284
285 if is_file:
285 if is_file:
286 history = commit.get_file_history(f_path)
286 history = commit.get_file_history(f_path)
287 prev_commit_id = history[1].raw_id \
287 prev_commit_id = history[1].raw_id \
288 if len(history) > 1 else prev_commit_id
288 if len(history) > 1 else prev_commit_id
289
289
290 return redirect(h.url(
290 return redirect(h.url(
291 'files_annotate_home', repo_name=repo_name,
291 'files_annotate_home', repo_name=repo_name,
292 revision=prev_commit_id, f_path=f_path))
292 revision=prev_commit_id, f_path=f_path))
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
295 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 'repository.admin')
296 'repository.admin')
297 @jsonify
297 @jsonify
298 def history(self, repo_name, revision, f_path):
298 def history(self, repo_name, revision, f_path):
299 commit = self.__get_commit_or_redirect(revision, repo_name)
299 commit = self.__get_commit_or_redirect(revision, repo_name)
300 f_path = f_path
300 f_path = f_path
301 _file = commit.get_node(f_path)
301 _file = commit.get_node(f_path)
302 if _file.is_file():
302 if _file.is_file():
303 file_history, _hist = self._get_node_history(commit, f_path)
303 file_history, _hist = self._get_node_history(commit, f_path)
304
304
305 res = []
305 res = []
306 for obj in file_history:
306 for obj in file_history:
307 res.append({
307 res.append({
308 'text': obj[1],
308 'text': obj[1],
309 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
309 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
310 })
310 })
311
311
312 data = {
312 data = {
313 'more': False,
313 'more': False,
314 'results': res
314 'results': res
315 }
315 }
316 return data
316 return data
317
317
318 @LoginRequired()
318 @LoginRequired()
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 'repository.admin')
320 'repository.admin')
321 def authors(self, repo_name, revision, f_path):
321 def authors(self, repo_name, revision, f_path):
322 commit = self.__get_commit_or_redirect(revision, repo_name)
322 commit = self.__get_commit_or_redirect(revision, repo_name)
323 file_node = commit.get_node(f_path)
323 file_node = commit.get_node(f_path)
324 if file_node.is_file():
324 if file_node.is_file():
325 c.file_last_commit = file_node.last_commit
325 c.file_last_commit = file_node.last_commit
326 if request.GET.get('annotate') == '1':
326 if request.GET.get('annotate') == '1':
327 # use _hist from annotation if annotation mode is on
327 # use _hist from annotation if annotation mode is on
328 commit_ids = set(x[1] for x in file_node.annotate)
328 commit_ids = set(x[1] for x in file_node.annotate)
329 _hist = (
329 _hist = (
330 c.rhodecode_repo.get_commit(commit_id)
330 c.rhodecode_repo.get_commit(commit_id)
331 for commit_id in commit_ids)
331 for commit_id in commit_ids)
332 else:
332 else:
333 _f_history, _hist = self._get_node_history(commit, f_path)
333 _f_history, _hist = self._get_node_history(commit, f_path)
334 c.file_author = False
334 c.file_author = False
335 c.authors = []
335 c.authors = []
336 for author in set(commit.author for commit in _hist):
336 for author in set(commit.author for commit in _hist):
337 c.authors.append((
337 c.authors.append((
338 h.email(author),
338 h.email(author),
339 h.person(author, 'username_or_name_or_email')))
339 h.person(author, 'username_or_name_or_email')))
340 return render('files/file_authors_box.mako')
340 return render('files/file_authors_box.mako')
341
341
342 @LoginRequired()
342 @LoginRequired()
343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
344 'repository.admin')
344 'repository.admin')
345 def rawfile(self, repo_name, revision, f_path):
345 def rawfile(self, repo_name, revision, f_path):
346 """
346 """
347 Action for download as raw
347 Action for download as raw
348 """
348 """
349 commit = self.__get_commit_or_redirect(revision, repo_name)
349 commit = self.__get_commit_or_redirect(revision, repo_name)
350 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
350 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
351
351
352 if request.GET.get('lf'):
352 if request.GET.get('lf'):
353 # only if lf get flag is passed, we download this file
353 # only if lf get flag is passed, we download this file
354 # as LFS/Largefile
354 # as LFS/Largefile
355 lf_node = file_node.get_largefile_node()
355 lf_node = file_node.get_largefile_node()
356 if lf_node:
356 if lf_node:
357 # overwrite our pointer with the REAL large-file
357 # overwrite our pointer with the REAL large-file
358 file_node = lf_node
358 file_node = lf_node
359
359
360 response.content_disposition = 'attachment; filename=%s' % \
360 response.content_disposition = 'attachment; filename=%s' % \
361 safe_str(f_path.split(Repository.NAME_SEP)[-1])
361 safe_str(f_path.split(Repository.NAME_SEP)[-1])
362
362
363 response.content_type = file_node.mimetype
363 response.content_type = file_node.mimetype
364 charset = self._get_default_encoding()
364 charset = self._get_default_encoding()
365 if charset:
365 if charset:
366 response.charset = charset
366 response.charset = charset
367
367
368 return file_node.content
368 return file_node.content
369
369
370 @LoginRequired()
370 @LoginRequired()
371 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
371 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
372 'repository.admin')
372 'repository.admin')
373 def raw(self, repo_name, revision, f_path):
373 def raw(self, repo_name, revision, f_path):
374 """
374 """
375 Action for show as raw, some mimetypes are "rendered",
375 Action for show as raw, some mimetypes are "rendered",
376 those include images, icons.
376 those include images, icons.
377 """
377 """
378 commit = self.__get_commit_or_redirect(revision, repo_name)
378 commit = self.__get_commit_or_redirect(revision, repo_name)
379 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
379 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
380
380
381 raw_mimetype_mapping = {
381 raw_mimetype_mapping = {
382 # map original mimetype to a mimetype used for "show as raw"
382 # map original mimetype to a mimetype used for "show as raw"
383 # you can also provide a content-disposition to override the
383 # you can also provide a content-disposition to override the
384 # default "attachment" disposition.
384 # default "attachment" disposition.
385 # orig_type: (new_type, new_dispo)
385 # orig_type: (new_type, new_dispo)
386
386
387 # show images inline:
387 # show images inline:
388 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
388 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
389 # for example render an SVG with javascript inside or even render
389 # for example render an SVG with javascript inside or even render
390 # HTML.
390 # HTML.
391 'image/x-icon': ('image/x-icon', 'inline'),
391 'image/x-icon': ('image/x-icon', 'inline'),
392 'image/png': ('image/png', 'inline'),
392 'image/png': ('image/png', 'inline'),
393 'image/gif': ('image/gif', 'inline'),
393 'image/gif': ('image/gif', 'inline'),
394 'image/jpeg': ('image/jpeg', 'inline'),
394 'image/jpeg': ('image/jpeg', 'inline'),
395 'application/pdf': ('application/pdf', 'inline'),
395 'application/pdf': ('application/pdf', 'inline'),
396 }
396 }
397
397
398 mimetype = file_node.mimetype
398 mimetype = file_node.mimetype
399 try:
399 try:
400 mimetype, dispo = raw_mimetype_mapping[mimetype]
400 mimetype, dispo = raw_mimetype_mapping[mimetype]
401 except KeyError:
401 except KeyError:
402 # we don't know anything special about this, handle it safely
402 # we don't know anything special about this, handle it safely
403 if file_node.is_binary:
403 if file_node.is_binary:
404 # do same as download raw for binary files
404 # do same as download raw for binary files
405 mimetype, dispo = 'application/octet-stream', 'attachment'
405 mimetype, dispo = 'application/octet-stream', 'attachment'
406 else:
406 else:
407 # do not just use the original mimetype, but force text/plain,
407 # do not just use the original mimetype, but force text/plain,
408 # otherwise it would serve text/html and that might be unsafe.
408 # otherwise it would serve text/html and that might be unsafe.
409 # Note: underlying vcs library fakes text/plain mimetype if the
409 # Note: underlying vcs library fakes text/plain mimetype if the
410 # mimetype can not be determined and it thinks it is not
410 # mimetype can not be determined and it thinks it is not
411 # binary.This might lead to erroneous text display in some
411 # binary.This might lead to erroneous text display in some
412 # cases, but helps in other cases, like with text files
412 # cases, but helps in other cases, like with text files
413 # without extension.
413 # without extension.
414 mimetype, dispo = 'text/plain', 'inline'
414 mimetype, dispo = 'text/plain', 'inline'
415
415
416 if dispo == 'attachment':
416 if dispo == 'attachment':
417 dispo = 'attachment; filename=%s' % safe_str(
417 dispo = 'attachment; filename=%s' % safe_str(
418 f_path.split(os.sep)[-1])
418 f_path.split(os.sep)[-1])
419
419
420 response.content_disposition = dispo
420 response.content_disposition = dispo
421 response.content_type = mimetype
421 response.content_type = mimetype
422 charset = self._get_default_encoding()
422 charset = self._get_default_encoding()
423 if charset:
423 if charset:
424 response.charset = charset
424 response.charset = charset
425 return file_node.content
425 return file_node.content
426
426
427 @CSRFRequired()
427 @CSRFRequired()
428 @LoginRequired()
428 @LoginRequired()
429 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
429 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
430 def delete(self, repo_name, revision, f_path):
430 def delete(self, repo_name, revision, f_path):
431 commit_id = revision
431 commit_id = revision
432
432
433 repo = c.rhodecode_db_repo
433 repo = c.rhodecode_db_repo
434 if repo.enable_locking and repo.locked[0]:
434 if repo.enable_locking and repo.locked[0]:
435 h.flash(_('This repository has been locked by %s on %s')
435 h.flash(_('This repository has been locked by %s on %s')
436 % (h.person_by_id(repo.locked[0]),
436 % (h.person_by_id(repo.locked[0]),
437 h.format_date(h.time_to_datetime(repo.locked[1]))),
437 h.format_date(h.time_to_datetime(repo.locked[1]))),
438 'warning')
438 'warning')
439 return redirect(h.url('files_home',
439 return redirect(h.url('files_home',
440 repo_name=repo_name, revision='tip'))
440 repo_name=repo_name, revision='tip'))
441
441
442 if not self._is_valid_head(commit_id, repo.scm_instance()):
442 if not self._is_valid_head(commit_id, repo.scm_instance()):
443 h.flash(_('You can only delete files with revision '
443 h.flash(_('You can only delete files with revision '
444 'being a valid branch '), category='warning')
444 'being a valid branch '), category='warning')
445 return redirect(h.url('files_home',
445 return redirect(h.url('files_home',
446 repo_name=repo_name, revision='tip',
446 repo_name=repo_name, revision='tip',
447 f_path=f_path))
447 f_path=f_path))
448
448
449 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
449 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
450 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
450 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
451
451
452 c.default_message = _(
452 c.default_message = _(
453 'Deleted file %s via RhodeCode Enterprise') % (f_path)
453 'Deleted file %s via RhodeCode Enterprise') % (f_path)
454 c.f_path = f_path
454 c.f_path = f_path
455 node_path = f_path
455 node_path = f_path
456 author = c.rhodecode_user.full_contact
456 author = c.rhodecode_user.full_contact
457 message = request.POST.get('message') or c.default_message
457 message = request.POST.get('message') or c.default_message
458 try:
458 try:
459 nodes = {
459 nodes = {
460 node_path: {
460 node_path: {
461 'content': ''
461 'content': ''
462 }
462 }
463 }
463 }
464 self.scm_model.delete_nodes(
464 self.scm_model.delete_nodes(
465 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
465 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
466 message=message,
466 message=message,
467 nodes=nodes,
467 nodes=nodes,
468 parent_commit=c.commit,
468 parent_commit=c.commit,
469 author=author,
469 author=author,
470 )
470 )
471
471
472 h.flash(_('Successfully deleted file %s') % f_path,
472 h.flash(_('Successfully deleted file %s') % f_path,
473 category='success')
473 category='success')
474 except Exception:
474 except Exception:
475 msg = _('Error occurred during commit')
475 msg = _('Error occurred during commit')
476 log.exception(msg)
476 log.exception(msg)
477 h.flash(msg, category='error')
477 h.flash(msg, category='error')
478 return redirect(url('changeset_home',
478 return redirect(url('changeset_home',
479 repo_name=c.repo_name, revision='tip'))
479 repo_name=c.repo_name, revision='tip'))
480
480
481 @LoginRequired()
481 @LoginRequired()
482 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
482 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
483 def delete_home(self, repo_name, revision, f_path):
483 def delete_home(self, repo_name, revision, f_path):
484 commit_id = revision
484 commit_id = revision
485
485
486 repo = c.rhodecode_db_repo
486 repo = c.rhodecode_db_repo
487 if repo.enable_locking and repo.locked[0]:
487 if repo.enable_locking and repo.locked[0]:
488 h.flash(_('This repository has been locked by %s on %s')
488 h.flash(_('This repository has been locked by %s on %s')
489 % (h.person_by_id(repo.locked[0]),
489 % (h.person_by_id(repo.locked[0]),
490 h.format_date(h.time_to_datetime(repo.locked[1]))),
490 h.format_date(h.time_to_datetime(repo.locked[1]))),
491 'warning')
491 'warning')
492 return redirect(h.url('files_home',
492 return redirect(h.url('files_home',
493 repo_name=repo_name, revision='tip'))
493 repo_name=repo_name, revision='tip'))
494
494
495 if not self._is_valid_head(commit_id, repo.scm_instance()):
495 if not self._is_valid_head(commit_id, repo.scm_instance()):
496 h.flash(_('You can only delete files with revision '
496 h.flash(_('You can only delete files with revision '
497 'being a valid branch '), category='warning')
497 'being a valid branch '), category='warning')
498 return redirect(h.url('files_home',
498 return redirect(h.url('files_home',
499 repo_name=repo_name, revision='tip',
499 repo_name=repo_name, revision='tip',
500 f_path=f_path))
500 f_path=f_path))
501
501
502 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
502 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
503 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
503 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
504
504
505 c.default_message = _(
505 c.default_message = _(
506 'Deleted file %s via RhodeCode Enterprise') % (f_path)
506 'Deleted file %s via RhodeCode Enterprise') % (f_path)
507 c.f_path = f_path
507 c.f_path = f_path
508
508
509 return render('files/files_delete.mako')
509 return render('files/files_delete.mako')
510
510
511 @CSRFRequired()
511 @CSRFRequired()
512 @LoginRequired()
512 @LoginRequired()
513 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
513 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
514 def edit(self, repo_name, revision, f_path):
514 def edit(self, repo_name, revision, f_path):
515 commit_id = revision
515 commit_id = revision
516
516
517 repo = c.rhodecode_db_repo
517 repo = c.rhodecode_db_repo
518 if repo.enable_locking and repo.locked[0]:
518 if repo.enable_locking and repo.locked[0]:
519 h.flash(_('This repository has been locked by %s on %s')
519 h.flash(_('This repository has been locked by %s on %s')
520 % (h.person_by_id(repo.locked[0]),
520 % (h.person_by_id(repo.locked[0]),
521 h.format_date(h.time_to_datetime(repo.locked[1]))),
521 h.format_date(h.time_to_datetime(repo.locked[1]))),
522 'warning')
522 'warning')
523 return redirect(h.url('files_home',
523 return redirect(h.url('files_home',
524 repo_name=repo_name, revision='tip'))
524 repo_name=repo_name, revision='tip'))
525
525
526 if not self._is_valid_head(commit_id, repo.scm_instance()):
526 if not self._is_valid_head(commit_id, repo.scm_instance()):
527 h.flash(_('You can only edit files with revision '
527 h.flash(_('You can only edit files with revision '
528 'being a valid branch '), category='warning')
528 'being a valid branch '), category='warning')
529 return redirect(h.url('files_home',
529 return redirect(h.url('files_home',
530 repo_name=repo_name, revision='tip',
530 repo_name=repo_name, revision='tip',
531 f_path=f_path))
531 f_path=f_path))
532
532
533 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
533 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
534 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
534 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
535
535
536 if c.file.is_binary:
536 if c.file.is_binary:
537 return redirect(url('files_home', repo_name=c.repo_name,
537 return redirect(url('files_home', repo_name=c.repo_name,
538 revision=c.commit.raw_id, f_path=f_path))
538 revision=c.commit.raw_id, f_path=f_path))
539 c.default_message = _(
539 c.default_message = _(
540 'Edited file %s via RhodeCode Enterprise') % (f_path)
540 'Edited file %s via RhodeCode Enterprise') % (f_path)
541 c.f_path = f_path
541 c.f_path = f_path
542 old_content = c.file.content
542 old_content = c.file.content
543 sl = old_content.splitlines(1)
543 sl = old_content.splitlines(1)
544 first_line = sl[0] if sl else ''
544 first_line = sl[0] if sl else ''
545
545
546 # modes: 0 - Unix, 1 - Mac, 2 - DOS
546 # modes: 0 - Unix, 1 - Mac, 2 - DOS
547 mode = detect_mode(first_line, 0)
547 mode = detect_mode(first_line, 0)
548 content = convert_line_endings(request.POST.get('content', ''), mode)
548 content = convert_line_endings(request.POST.get('content', ''), mode)
549
549
550 message = request.POST.get('message') or c.default_message
550 message = request.POST.get('message') or c.default_message
551 org_f_path = c.file.unicode_path
551 org_f_path = c.file.unicode_path
552 filename = request.POST['filename']
552 filename = request.POST['filename']
553 org_filename = c.file.name
553 org_filename = c.file.name
554
554
555 if content == old_content and filename == org_filename:
555 if content == old_content and filename == org_filename:
556 h.flash(_('No changes'), category='warning')
556 h.flash(_('No changes'), category='warning')
557 return redirect(url('changeset_home', repo_name=c.repo_name,
557 return redirect(url('changeset_home', repo_name=c.repo_name,
558 revision='tip'))
558 revision='tip'))
559 try:
559 try:
560 mapping = {
560 mapping = {
561 org_f_path: {
561 org_f_path: {
562 'org_filename': org_f_path,
562 'org_filename': org_f_path,
563 'filename': os.path.join(c.file.dir_path, filename),
563 'filename': os.path.join(c.file.dir_path, filename),
564 'content': content,
564 'content': content,
565 'lexer': '',
565 'lexer': '',
566 'op': 'mod',
566 'op': 'mod',
567 }
567 }
568 }
568 }
569
569
570 ScmModel().update_nodes(
570 ScmModel().update_nodes(
571 user=c.rhodecode_user.user_id,
571 user=c.rhodecode_user.user_id,
572 repo=c.rhodecode_db_repo,
572 repo=c.rhodecode_db_repo,
573 message=message,
573 message=message,
574 nodes=mapping,
574 nodes=mapping,
575 parent_commit=c.commit,
575 parent_commit=c.commit,
576 )
576 )
577
577
578 h.flash(_('Successfully committed to %s') % f_path,
578 h.flash(_('Successfully committed to %s') % f_path,
579 category='success')
579 category='success')
580 except Exception:
580 except Exception:
581 msg = _('Error occurred during commit')
581 msg = _('Error occurred during commit')
582 log.exception(msg)
582 log.exception(msg)
583 h.flash(msg, category='error')
583 h.flash(msg, category='error')
584 return redirect(url('changeset_home',
584 return redirect(url('changeset_home',
585 repo_name=c.repo_name, revision='tip'))
585 repo_name=c.repo_name, revision='tip'))
586
586
587 @LoginRequired()
587 @LoginRequired()
588 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
588 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
589 def edit_home(self, repo_name, revision, f_path):
589 def edit_home(self, repo_name, revision, f_path):
590 commit_id = revision
590 commit_id = revision
591
591
592 repo = c.rhodecode_db_repo
592 repo = c.rhodecode_db_repo
593 if repo.enable_locking and repo.locked[0]:
593 if repo.enable_locking and repo.locked[0]:
594 h.flash(_('This repository has been locked by %s on %s')
594 h.flash(_('This repository has been locked by %s on %s')
595 % (h.person_by_id(repo.locked[0]),
595 % (h.person_by_id(repo.locked[0]),
596 h.format_date(h.time_to_datetime(repo.locked[1]))),
596 h.format_date(h.time_to_datetime(repo.locked[1]))),
597 'warning')
597 'warning')
598 return redirect(h.url('files_home',
598 return redirect(h.url('files_home',
599 repo_name=repo_name, revision='tip'))
599 repo_name=repo_name, revision='tip'))
600
600
601 if not self._is_valid_head(commit_id, repo.scm_instance()):
601 if not self._is_valid_head(commit_id, repo.scm_instance()):
602 h.flash(_('You can only edit files with revision '
602 h.flash(_('You can only edit files with revision '
603 'being a valid branch '), category='warning')
603 'being a valid branch '), category='warning')
604 return redirect(h.url('files_home',
604 return redirect(h.url('files_home',
605 repo_name=repo_name, revision='tip',
605 repo_name=repo_name, revision='tip',
606 f_path=f_path))
606 f_path=f_path))
607
607
608 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
608 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
609 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
609 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
610
610
611 if c.file.is_binary:
611 if c.file.is_binary:
612 return redirect(url('files_home', repo_name=c.repo_name,
612 return redirect(url('files_home', repo_name=c.repo_name,
613 revision=c.commit.raw_id, f_path=f_path))
613 revision=c.commit.raw_id, f_path=f_path))
614 c.default_message = _(
614 c.default_message = _(
615 'Edited file %s via RhodeCode Enterprise') % (f_path)
615 'Edited file %s via RhodeCode Enterprise') % (f_path)
616 c.f_path = f_path
616 c.f_path = f_path
617
617
618 return render('files/files_edit.mako')
618 return render('files/files_edit.mako')
619
619
620 def _is_valid_head(self, commit_id, repo):
620 def _is_valid_head(self, commit_id, repo):
621 # check if commit is a branch identifier- basically we cannot
621 # check if commit is a branch identifier- basically we cannot
622 # create multiple heads via file editing
622 # create multiple heads via file editing
623 valid_heads = repo.branches.keys() + repo.branches.values()
623 valid_heads = repo.branches.keys() + repo.branches.values()
624
624
625 if h.is_svn(repo) and not repo.is_empty():
625 if h.is_svn(repo) and not repo.is_empty():
626 # Note: Subversion only has one head, we add it here in case there
626 # Note: Subversion only has one head, we add it here in case there
627 # is no branch matched.
627 # is no branch matched.
628 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
628 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
629
629
630 # check if commit is a branch name or branch hash
630 # check if commit is a branch name or branch hash
631 return commit_id in valid_heads
631 return commit_id in valid_heads
632
632
633 @CSRFRequired()
633 @CSRFRequired()
634 @LoginRequired()
634 @LoginRequired()
635 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
635 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
636 def add(self, repo_name, revision, f_path):
636 def add(self, repo_name, revision, f_path):
637 repo = Repository.get_by_repo_name(repo_name)
637 repo = Repository.get_by_repo_name(repo_name)
638 if repo.enable_locking and repo.locked[0]:
638 if repo.enable_locking and repo.locked[0]:
639 h.flash(_('This repository has been locked by %s on %s')
639 h.flash(_('This repository has been locked by %s on %s')
640 % (h.person_by_id(repo.locked[0]),
640 % (h.person_by_id(repo.locked[0]),
641 h.format_date(h.time_to_datetime(repo.locked[1]))),
641 h.format_date(h.time_to_datetime(repo.locked[1]))),
642 'warning')
642 'warning')
643 return redirect(h.url('files_home',
643 return redirect(h.url('files_home',
644 repo_name=repo_name, revision='tip'))
644 repo_name=repo_name, revision='tip'))
645
645
646 r_post = request.POST
646 r_post = request.POST
647
647
648 c.commit = self.__get_commit_or_redirect(
648 c.commit = self.__get_commit_or_redirect(
649 revision, repo_name, redirect_after=False)
649 revision, repo_name, redirect_after=False)
650 if c.commit is None:
650 if c.commit is None:
651 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
651 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
652 c.default_message = (_('Added file via RhodeCode Enterprise'))
652 c.default_message = (_('Added file via RhodeCode Enterprise'))
653 c.f_path = f_path
653 c.f_path = f_path
654 unix_mode = 0
654 unix_mode = 0
655 content = convert_line_endings(r_post.get('content', ''), unix_mode)
655 content = convert_line_endings(r_post.get('content', ''), unix_mode)
656
656
657 message = r_post.get('message') or c.default_message
657 message = r_post.get('message') or c.default_message
658 filename = r_post.get('filename')
658 filename = r_post.get('filename')
659 location = r_post.get('location', '') # dir location
659 location = r_post.get('location', '') # dir location
660 file_obj = r_post.get('upload_file', None)
660 file_obj = r_post.get('upload_file', None)
661
661
662 if file_obj is not None and hasattr(file_obj, 'filename'):
662 if file_obj is not None and hasattr(file_obj, 'filename'):
663 filename = r_post.get('filename_upload')
663 filename = r_post.get('filename_upload')
664 content = file_obj.file
664 content = file_obj.file
665
665
666 if hasattr(content, 'file'):
666 if hasattr(content, 'file'):
667 # non posix systems store real file under file attr
667 # non posix systems store real file under file attr
668 content = content.file
668 content = content.file
669
669
670 # If there's no commit, redirect to repo summary
670 # If there's no commit, redirect to repo summary
671 if type(c.commit) is EmptyCommit:
671 if type(c.commit) is EmptyCommit:
672 redirect_url = h.route_path('repo_summary', repo_name=c.repo_name)
672 redirect_url = h.route_path('repo_summary', repo_name=c.repo_name)
673 else:
673 else:
674 redirect_url = url("changeset_home", repo_name=c.repo_name,
674 redirect_url = url("changeset_home", repo_name=c.repo_name,
675 revision='tip')
675 revision='tip')
676
676
677 if not filename:
677 if not filename:
678 h.flash(_('No filename'), category='warning')
678 h.flash(_('No filename'), category='warning')
679 return redirect(redirect_url)
679 return redirect(redirect_url)
680
680
681 # extract the location from filename,
681 # extract the location from filename,
682 # allows using foo/bar.txt syntax to create subdirectories
682 # allows using foo/bar.txt syntax to create subdirectories
683 subdir_loc = filename.rsplit('/', 1)
683 subdir_loc = filename.rsplit('/', 1)
684 if len(subdir_loc) == 2:
684 if len(subdir_loc) == 2:
685 location = os.path.join(location, subdir_loc[0])
685 location = os.path.join(location, subdir_loc[0])
686
686
687 # strip all crap out of file, just leave the basename
687 # strip all crap out of file, just leave the basename
688 filename = os.path.basename(filename)
688 filename = os.path.basename(filename)
689 node_path = os.path.join(location, filename)
689 node_path = os.path.join(location, filename)
690 author = c.rhodecode_user.full_contact
690 author = c.rhodecode_user.full_contact
691
691
692 try:
692 try:
693 nodes = {
693 nodes = {
694 node_path: {
694 node_path: {
695 'content': content
695 'content': content
696 }
696 }
697 }
697 }
698 self.scm_model.create_nodes(
698 self.scm_model.create_nodes(
699 user=c.rhodecode_user.user_id,
699 user=c.rhodecode_user.user_id,
700 repo=c.rhodecode_db_repo,
700 repo=c.rhodecode_db_repo,
701 message=message,
701 message=message,
702 nodes=nodes,
702 nodes=nodes,
703 parent_commit=c.commit,
703 parent_commit=c.commit,
704 author=author,
704 author=author,
705 )
705 )
706
706
707 h.flash(_('Successfully committed to %s') % node_path,
707 h.flash(_('Successfully committed to %s') % node_path,
708 category='success')
708 category='success')
709 except NonRelativePathError as e:
709 except NonRelativePathError as e:
710 h.flash(_(
710 h.flash(_(
711 'The location specified must be a relative path and must not '
711 'The location specified must be a relative path and must not '
712 'contain .. in the path'), category='warning')
712 'contain .. in the path'), category='warning')
713 return redirect(url('changeset_home', repo_name=c.repo_name,
713 return redirect(url('changeset_home', repo_name=c.repo_name,
714 revision='tip'))
714 revision='tip'))
715 except (NodeError, NodeAlreadyExistsError) as e:
715 except (NodeError, NodeAlreadyExistsError) as e:
716 h.flash(_(e), category='error')
716 h.flash(_(e), category='error')
717 except Exception:
717 except Exception:
718 msg = _('Error occurred during commit')
718 msg = _('Error occurred during commit')
719 log.exception(msg)
719 log.exception(msg)
720 h.flash(msg, category='error')
720 h.flash(msg, category='error')
721 return redirect(url('changeset_home',
721 return redirect(url('changeset_home',
722 repo_name=c.repo_name, revision='tip'))
722 repo_name=c.repo_name, revision='tip'))
723
723
724 @LoginRequired()
724 @LoginRequired()
725 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
725 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
726 def add_home(self, repo_name, revision, f_path):
726 def add_home(self, repo_name, revision, f_path):
727
727
728 repo = Repository.get_by_repo_name(repo_name)
728 repo = Repository.get_by_repo_name(repo_name)
729 if repo.enable_locking and repo.locked[0]:
729 if repo.enable_locking and repo.locked[0]:
730 h.flash(_('This repository has been locked by %s on %s')
730 h.flash(_('This repository has been locked by %s on %s')
731 % (h.person_by_id(repo.locked[0]),
731 % (h.person_by_id(repo.locked[0]),
732 h.format_date(h.time_to_datetime(repo.locked[1]))),
732 h.format_date(h.time_to_datetime(repo.locked[1]))),
733 'warning')
733 'warning')
734 return redirect(h.url('files_home',
734 return redirect(h.url('files_home',
735 repo_name=repo_name, revision='tip'))
735 repo_name=repo_name, revision='tip'))
736
736
737 c.commit = self.__get_commit_or_redirect(
737 c.commit = self.__get_commit_or_redirect(
738 revision, repo_name, redirect_after=False)
738 revision, repo_name, redirect_after=False)
739 if c.commit is None:
739 if c.commit is None:
740 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
740 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
741 c.default_message = (_('Added file via RhodeCode Enterprise'))
741 c.default_message = (_('Added file via RhodeCode Enterprise'))
742 c.f_path = f_path
742 c.f_path = f_path
743
743
744 return render('files/files_add.mako')
744 return render('files/files_add.mako')
745
745
746 @LoginRequired()
746 @LoginRequired()
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 'repository.admin')
748 'repository.admin')
749 def archivefile(self, repo_name, fname):
749 def archivefile(self, repo_name, fname):
750 fileformat = None
750 fileformat = None
751 commit_id = None
751 commit_id = None
752 ext = None
752 ext = None
753 subrepos = request.GET.get('subrepos') == 'true'
753 subrepos = request.GET.get('subrepos') == 'true'
754
754
755 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
755 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
756 archive_spec = fname.split(ext_data[1])
756 archive_spec = fname.split(ext_data[1])
757 if len(archive_spec) == 2 and archive_spec[1] == '':
757 if len(archive_spec) == 2 and archive_spec[1] == '':
758 fileformat = a_type or ext_data[1]
758 fileformat = a_type or ext_data[1]
759 commit_id = archive_spec[0]
759 commit_id = archive_spec[0]
760 ext = ext_data[1]
760 ext = ext_data[1]
761
761
762 dbrepo = RepoModel().get_by_repo_name(repo_name)
762 dbrepo = RepoModel().get_by_repo_name(repo_name)
763 if not dbrepo.enable_downloads:
763 if not dbrepo.enable_downloads:
764 return _('Downloads disabled')
764 return _('Downloads disabled')
765
765
766 try:
766 try:
767 commit = c.rhodecode_repo.get_commit(commit_id)
767 commit = c.rhodecode_repo.get_commit(commit_id)
768 content_type = settings.ARCHIVE_SPECS[fileformat][0]
768 content_type = settings.ARCHIVE_SPECS[fileformat][0]
769 except CommitDoesNotExistError:
769 except CommitDoesNotExistError:
770 return _('Unknown revision %s') % commit_id
770 return _('Unknown revision %s') % commit_id
771 except EmptyRepositoryError:
771 except EmptyRepositoryError:
772 return _('Empty repository')
772 return _('Empty repository')
773 except KeyError:
773 except KeyError:
774 return _('Unknown archive type')
774 return _('Unknown archive type')
775
775
776 # archive cache
776 # archive cache
777 from rhodecode import CONFIG
777 from rhodecode import CONFIG
778
778
779 archive_name = '%s-%s%s%s' % (
779 archive_name = '%s-%s%s%s' % (
780 safe_str(repo_name.replace('/', '_')),
780 safe_str(repo_name.replace('/', '_')),
781 '-sub' if subrepos else '',
781 '-sub' if subrepos else '',
782 safe_str(commit.short_id), ext)
782 safe_str(commit.short_id), ext)
783
783
784 use_cached_archive = False
784 use_cached_archive = False
785 archive_cache_enabled = CONFIG.get(
785 archive_cache_enabled = CONFIG.get(
786 'archive_cache_dir') and not request.GET.get('no_cache')
786 'archive_cache_dir') and not request.GET.get('no_cache')
787
787
788 if archive_cache_enabled:
788 if archive_cache_enabled:
789 # check if we it's ok to write
789 # check if we it's ok to write
790 if not os.path.isdir(CONFIG['archive_cache_dir']):
790 if not os.path.isdir(CONFIG['archive_cache_dir']):
791 os.makedirs(CONFIG['archive_cache_dir'])
791 os.makedirs(CONFIG['archive_cache_dir'])
792 cached_archive_path = os.path.join(
792 cached_archive_path = os.path.join(
793 CONFIG['archive_cache_dir'], archive_name)
793 CONFIG['archive_cache_dir'], archive_name)
794 if os.path.isfile(cached_archive_path):
794 if os.path.isfile(cached_archive_path):
795 log.debug('Found cached archive in %s', cached_archive_path)
795 log.debug('Found cached archive in %s', cached_archive_path)
796 fd, archive = None, cached_archive_path
796 fd, archive = None, cached_archive_path
797 use_cached_archive = True
797 use_cached_archive = True
798 else:
798 else:
799 log.debug('Archive %s is not yet cached', archive_name)
799 log.debug('Archive %s is not yet cached', archive_name)
800
800
801 if not use_cached_archive:
801 if not use_cached_archive:
802 # generate new archive
802 # generate new archive
803 fd, archive = tempfile.mkstemp()
803 fd, archive = tempfile.mkstemp()
804 log.debug('Creating new temp archive in %s' % (archive,))
804 log.debug('Creating new temp archive in %s' % (archive,))
805 try:
805 try:
806 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
806 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
807 except ImproperArchiveTypeError:
807 except ImproperArchiveTypeError:
808 return _('Unknown archive type')
808 return _('Unknown archive type')
809 if archive_cache_enabled:
809 if archive_cache_enabled:
810 # if we generated the archive and we have cache enabled
810 # if we generated the archive and we have cache enabled
811 # let's use this for future
811 # let's use this for future
812 log.debug('Storing new archive in %s' % (cached_archive_path,))
812 log.debug('Storing new archive in %s' % (cached_archive_path,))
813 shutil.move(archive, cached_archive_path)
813 shutil.move(archive, cached_archive_path)
814 archive = cached_archive_path
814 archive = cached_archive_path
815
815
816 # store download action
816 # store download action
817 audit_logger.store_web(
817 audit_logger.store_web(
818 action='repo.archive.download',
818 action='repo.archive.download',
819 action_data={'user_agent': request.user_agent,
819 action_data={'user_agent': request.user_agent,
820 'archive_name': archive_name,
820 'archive_name': archive_name,
821 'archive_spec': fname,
821 'archive_spec': fname,
822 'archive_cached': use_cached_archive},
822 'archive_cached': use_cached_archive},
823 user=c.rhodecode_user,
823 user=c.rhodecode_user,
824 repo=dbrepo,
824 repo=dbrepo,
825 commit=True
825 commit=True
826 )
826 )
827
827
828 response.content_disposition = str(
828 response.content_disposition = str(
829 'attachment; filename=%s' % archive_name)
829 'attachment; filename=%s' % archive_name)
830 response.content_type = str(content_type)
830 response.content_type = str(content_type)
831
831
832 def get_chunked_archive(archive):
832 def get_chunked_archive(archive):
833 with open(archive, 'rb') as stream:
833 with open(archive, 'rb') as stream:
834 while True:
834 while True:
835 data = stream.read(16 * 1024)
835 data = stream.read(16 * 1024)
836 if not data:
836 if not data:
837 if fd: # fd means we used temporary file
837 if fd: # fd means we used temporary file
838 os.close(fd)
838 os.close(fd)
839 if not archive_cache_enabled:
839 if not archive_cache_enabled:
840 log.debug('Destroying temp archive %s', archive)
840 log.debug('Destroying temp archive %s', archive)
841 os.remove(archive)
841 os.remove(archive)
842 break
842 break
843 yield data
843 yield data
844
844
845 return get_chunked_archive(archive)
845 return get_chunked_archive(archive)
846
846
847 @LoginRequired()
847 @LoginRequired()
848 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
848 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
849 'repository.admin')
849 'repository.admin')
850 def diff(self, repo_name, f_path):
850 def diff(self, repo_name, f_path):
851
851
852 c.action = request.GET.get('diff')
852 c.action = request.GET.get('diff')
853 diff1 = request.GET.get('diff1', '')
853 diff1 = request.GET.get('diff1', '')
854 diff2 = request.GET.get('diff2', '')
854 diff2 = request.GET.get('diff2', '')
855
855
856 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
856 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
857
857
858 ignore_whitespace = str2bool(request.GET.get('ignorews'))
858 ignore_whitespace = str2bool(request.GET.get('ignorews'))
859 line_context = request.GET.get('context', 3)
859 line_context = request.GET.get('context', 3)
860
860
861 if not any((diff1, diff2)):
861 if not any((diff1, diff2)):
862 h.flash(
862 h.flash(
863 'Need query parameter "diff1" or "diff2" to generate a diff.',
863 'Need query parameter "diff1" or "diff2" to generate a diff.',
864 category='error')
864 category='error')
865 raise HTTPBadRequest()
865 raise HTTPBadRequest()
866
866
867 if c.action not in ['download', 'raw']:
867 if c.action not in ['download', 'raw']:
868 # redirect to new view if we render diff
868 # redirect to new view if we render diff
869 return redirect(
869 return redirect(
870 url('compare_url', repo_name=repo_name,
870 url('compare_url', repo_name=repo_name,
871 source_ref_type='rev',
871 source_ref_type='rev',
872 source_ref=diff1,
872 source_ref=diff1,
873 target_repo=c.repo_name,
873 target_repo=c.repo_name,
874 target_ref_type='rev',
874 target_ref_type='rev',
875 target_ref=diff2,
875 target_ref=diff2,
876 f_path=f_path))
876 f_path=f_path))
877
877
878 try:
878 try:
879 node1 = self._get_file_node(diff1, path1)
879 node1 = self._get_file_node(diff1, path1)
880 node2 = self._get_file_node(diff2, f_path)
880 node2 = self._get_file_node(diff2, f_path)
881 except (RepositoryError, NodeError):
881 except (RepositoryError, NodeError):
882 log.exception("Exception while trying to get node from repository")
882 log.exception("Exception while trying to get node from repository")
883 return redirect(url(
883 return redirect(url(
884 'files_home', repo_name=c.repo_name, f_path=f_path))
884 'files_home', repo_name=c.repo_name, f_path=f_path))
885
885
886 if all(isinstance(node.commit, EmptyCommit)
886 if all(isinstance(node.commit, EmptyCommit)
887 for node in (node1, node2)):
887 for node in (node1, node2)):
888 raise HTTPNotFound
888 raise HTTPNotFound
889
889
890 c.commit_1 = node1.commit
890 c.commit_1 = node1.commit
891 c.commit_2 = node2.commit
891 c.commit_2 = node2.commit
892
892
893 if c.action == 'download':
893 if c.action == 'download':
894 _diff = diffs.get_gitdiff(node1, node2,
894 _diff = diffs.get_gitdiff(node1, node2,
895 ignore_whitespace=ignore_whitespace,
895 ignore_whitespace=ignore_whitespace,
896 context=line_context)
896 context=line_context)
897 diff = diffs.DiffProcessor(_diff, format='gitdiff')
897 diff = diffs.DiffProcessor(_diff, format='gitdiff')
898
898
899 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
899 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
900 response.content_type = 'text/plain'
900 response.content_type = 'text/plain'
901 response.content_disposition = (
901 response.content_disposition = (
902 'attachment; filename=%s' % (diff_name,)
902 'attachment; filename=%s' % (diff_name,)
903 )
903 )
904 charset = self._get_default_encoding()
904 charset = self._get_default_encoding()
905 if charset:
905 if charset:
906 response.charset = charset
906 response.charset = charset
907 return diff.as_raw()
907 return diff.as_raw()
908
908
909 elif c.action == 'raw':
909 elif c.action == 'raw':
910 _diff = diffs.get_gitdiff(node1, node2,
910 _diff = diffs.get_gitdiff(node1, node2,
911 ignore_whitespace=ignore_whitespace,
911 ignore_whitespace=ignore_whitespace,
912 context=line_context)
912 context=line_context)
913 diff = diffs.DiffProcessor(_diff, format='gitdiff')
913 diff = diffs.DiffProcessor(_diff, format='gitdiff')
914 response.content_type = 'text/plain'
914 response.content_type = 'text/plain'
915 charset = self._get_default_encoding()
915 charset = self._get_default_encoding()
916 if charset:
916 if charset:
917 response.charset = charset
917 response.charset = charset
918 return diff.as_raw()
918 return diff.as_raw()
919
919
920 else:
920 else:
921 return redirect(
921 return redirect(
922 url('compare_url', repo_name=repo_name,
922 url('compare_url', repo_name=repo_name,
923 source_ref_type='rev',
923 source_ref_type='rev',
924 source_ref=diff1,
924 source_ref=diff1,
925 target_repo=c.repo_name,
925 target_repo=c.repo_name,
926 target_ref_type='rev',
926 target_ref_type='rev',
927 target_ref=diff2,
927 target_ref=diff2,
928 f_path=f_path))
928 f_path=f_path))
929
929
930 @LoginRequired()
930 @LoginRequired()
931 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
931 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
932 'repository.admin')
932 'repository.admin')
933 def diff_2way(self, repo_name, f_path):
933 def diff_2way(self, repo_name, f_path):
934 """
934 """
935 Kept only to make OLD links work
935 Kept only to make OLD links work
936 """
936 """
937 diff1 = request.GET.get('diff1', '')
937 diff1 = request.GET.get('diff1', '')
938 diff2 = request.GET.get('diff2', '')
938 diff2 = request.GET.get('diff2', '')
939
939
940 if not any((diff1, diff2)):
940 if not any((diff1, diff2)):
941 h.flash(
941 h.flash(
942 'Need query parameter "diff1" or "diff2" to generate a diff.',
942 'Need query parameter "diff1" or "diff2" to generate a diff.',
943 category='error')
943 category='error')
944 raise HTTPBadRequest()
944 raise HTTPBadRequest()
945
945
946 return redirect(
946 return redirect(
947 url('compare_url', repo_name=repo_name,
947 url('compare_url', repo_name=repo_name,
948 source_ref_type='rev',
948 source_ref_type='rev',
949 source_ref=diff1,
949 source_ref=diff1,
950 target_repo=c.repo_name,
950 target_repo=c.repo_name,
951 target_ref_type='rev',
951 target_ref_type='rev',
952 target_ref=diff2,
952 target_ref=diff2,
953 f_path=f_path,
953 f_path=f_path,
954 diffmode='sideside'))
954 diffmode='sideside'))
955
955
956 def _get_file_node(self, commit_id, f_path):
956 def _get_file_node(self, commit_id, f_path):
957 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
957 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
958 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
958 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
959 try:
959 try:
960 node = commit.get_node(f_path)
960 node = commit.get_node(f_path)
961 if node.is_dir():
961 if node.is_dir():
962 raise NodeError('%s path is a %s not a file'
962 raise NodeError('%s path is a %s not a file'
963 % (node, type(node)))
963 % (node, type(node)))
964 except NodeDoesNotExistError:
964 except NodeDoesNotExistError:
965 commit = EmptyCommit(
965 commit = EmptyCommit(
966 commit_id=commit_id,
966 commit_id=commit_id,
967 idx=commit.idx,
967 idx=commit.idx,
968 repo=commit.repository,
968 repo=commit.repository,
969 alias=commit.repository.alias,
969 alias=commit.repository.alias,
970 message=commit.message,
970 message=commit.message,
971 author=commit.author,
971 author=commit.author,
972 date=commit.date)
972 date=commit.date)
973 node = FileNode(f_path, '', commit=commit)
973 node = FileNode(f_path, '', commit=commit)
974 else:
974 else:
975 commit = EmptyCommit(
975 commit = EmptyCommit(
976 repo=c.rhodecode_repo,
976 repo=c.rhodecode_repo,
977 alias=c.rhodecode_repo.alias)
977 alias=c.rhodecode_repo.alias)
978 node = FileNode(f_path, '', commit=commit)
978 node = FileNode(f_path, '', commit=commit)
979 return node
979 return node
980
980
981 def _get_node_history(self, commit, f_path, commits=None):
981 def _get_node_history(self, commit, f_path, commits=None):
982 """
982 """
983 get commit history for given node
983 get commit history for given node
984
984
985 :param commit: commit to calculate history
985 :param commit: commit to calculate history
986 :param f_path: path for node to calculate history for
986 :param f_path: path for node to calculate history for
987 :param commits: if passed don't calculate history and take
987 :param commits: if passed don't calculate history and take
988 commits defined in this list
988 commits defined in this list
989 """
989 """
990 # calculate history based on tip
990 # calculate history based on tip
991 tip = c.rhodecode_repo.get_commit()
991 tip = c.rhodecode_repo.get_commit()
992 if commits is None:
992 if commits is None:
993 pre_load = ["author", "branch"]
993 pre_load = ["author", "branch"]
994 try:
994 try:
995 commits = tip.get_file_history(f_path, pre_load=pre_load)
995 commits = tip.get_file_history(f_path, pre_load=pre_load)
996 except (NodeDoesNotExistError, CommitError):
996 except (NodeDoesNotExistError, CommitError):
997 # this node is not present at tip!
997 # this node is not present at tip!
998 commits = commit.get_file_history(f_path, pre_load=pre_load)
998 commits = commit.get_file_history(f_path, pre_load=pre_load)
999
999
1000 history = []
1000 history = []
1001 commits_group = ([], _("Changesets"))
1001 commits_group = ([], _("Changesets"))
1002 for commit in commits:
1002 for commit in commits:
1003 branch = ' (%s)' % commit.branch if commit.branch else ''
1003 branch = ' (%s)' % commit.branch if commit.branch else ''
1004 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1004 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1005 commits_group[0].append((commit.raw_id, n_desc,))
1005 commits_group[0].append((commit.raw_id, n_desc,))
1006 history.append(commits_group)
1006 history.append(commits_group)
1007
1007
1008 symbolic_reference = self._symbolic_reference
1008 symbolic_reference = self._symbolic_reference
1009
1009
1010 if c.rhodecode_repo.alias == 'svn':
1010 if c.rhodecode_repo.alias == 'svn':
1011 adjusted_f_path = self._adjust_file_path_for_svn(
1011 adjusted_f_path = self._adjust_file_path_for_svn(
1012 f_path, c.rhodecode_repo)
1012 f_path, c.rhodecode_repo)
1013 if adjusted_f_path != f_path:
1013 if adjusted_f_path != f_path:
1014 log.debug(
1014 log.debug(
1015 'Recognized svn tag or branch in file "%s", using svn '
1015 'Recognized svn tag or branch in file "%s", using svn '
1016 'specific symbolic references', f_path)
1016 'specific symbolic references', f_path)
1017 f_path = adjusted_f_path
1017 f_path = adjusted_f_path
1018 symbolic_reference = self._symbolic_reference_svn
1018 symbolic_reference = self._symbolic_reference_svn
1019
1019
1020 branches = self._create_references(
1020 branches = self._create_references(
1021 c.rhodecode_repo.branches, symbolic_reference, f_path)
1021 c.rhodecode_repo.branches, symbolic_reference, f_path)
1022 branches_group = (branches, _("Branches"))
1022 branches_group = (branches, _("Branches"))
1023
1023
1024 tags = self._create_references(
1024 tags = self._create_references(
1025 c.rhodecode_repo.tags, symbolic_reference, f_path)
1025 c.rhodecode_repo.tags, symbolic_reference, f_path)
1026 tags_group = (tags, _("Tags"))
1026 tags_group = (tags, _("Tags"))
1027
1027
1028 history.append(branches_group)
1028 history.append(branches_group)
1029 history.append(tags_group)
1029 history.append(tags_group)
1030
1030
1031 return history, commits
1031 return history, commits
1032
1032
1033 def _adjust_file_path_for_svn(self, f_path, repo):
1033 def _adjust_file_path_for_svn(self, f_path, repo):
1034 """
1034 """
1035 Computes the relative path of `f_path`.
1035 Computes the relative path of `f_path`.
1036
1036
1037 This is mainly based on prefix matching of the recognized tags and
1037 This is mainly based on prefix matching of the recognized tags and
1038 branches in the underlying repository.
1038 branches in the underlying repository.
1039 """
1039 """
1040 tags_and_branches = itertools.chain(
1040 tags_and_branches = itertools.chain(
1041 repo.branches.iterkeys(),
1041 repo.branches.iterkeys(),
1042 repo.tags.iterkeys())
1042 repo.tags.iterkeys())
1043 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1043 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1044
1044
1045 for name in tags_and_branches:
1045 for name in tags_and_branches:
1046 if f_path.startswith(name + '/'):
1046 if f_path.startswith(name + '/'):
1047 f_path = vcspath.relpath(f_path, name)
1047 f_path = vcspath.relpath(f_path, name)
1048 break
1048 break
1049 return f_path
1049 return f_path
1050
1050
1051 def _create_references(
1051 def _create_references(
1052 self, branches_or_tags, symbolic_reference, f_path):
1052 self, branches_or_tags, symbolic_reference, f_path):
1053 items = []
1053 items = []
1054 for name, commit_id in branches_or_tags.items():
1054 for name, commit_id in branches_or_tags.items():
1055 sym_ref = symbolic_reference(commit_id, name, f_path)
1055 sym_ref = symbolic_reference(commit_id, name, f_path)
1056 items.append((sym_ref, name))
1056 items.append((sym_ref, name))
1057 return items
1057 return items
1058
1058
1059 def _symbolic_reference(self, commit_id, name, f_path):
1059 def _symbolic_reference(self, commit_id, name, f_path):
1060 return commit_id
1060 return commit_id
1061
1061
1062 def _symbolic_reference_svn(self, commit_id, name, f_path):
1062 def _symbolic_reference_svn(self, commit_id, name, f_path):
1063 new_f_path = vcspath.join(name, f_path)
1063 new_f_path = vcspath.join(name, f_path)
1064 return u'%s@%s' % (new_f_path, commit_id)
1064 return u'%s@%s' % (new_f_path, commit_id)
1065
1065
1066 @LoginRequired()
1066 @LoginRequired()
1067 @XHRRequired()
1067 @XHRRequired()
1068 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1069 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1070 @jsonify
1070 @jsonify
1071 def nodelist(self, repo_name, revision, f_path):
1071 def nodelist(self, repo_name, revision, f_path):
1072 commit = self.__get_commit_or_redirect(revision, repo_name)
1072 commit = self.__get_commit_or_redirect(revision, repo_name)
1073
1073
1074 metadata = self._get_nodelist_at_commit(
1074 metadata = self._get_nodelist_at_commit(
1075 repo_name, commit.raw_id, f_path)
1075 repo_name, commit.raw_id, f_path)
1076 return {'nodes': metadata}
1076 return {'nodes': metadata}
1077
1077
1078 @LoginRequired()
1078 @LoginRequired()
1079 @XHRRequired()
1079 @XHRRequired()
1080 @HasRepoPermissionAnyDecorator(
1080 @HasRepoPermissionAnyDecorator(
1081 'repository.read', 'repository.write', 'repository.admin')
1081 'repository.read', 'repository.write', 'repository.admin')
1082 def nodetree_full(self, repo_name, commit_id, f_path):
1082 def nodetree_full(self, repo_name, commit_id, f_path):
1083 """
1083 """
1084 Returns rendered html of file tree that contains commit date,
1084 Returns rendered html of file tree that contains commit date,
1085 author, revision for the specified combination of
1085 author, revision for the specified combination of
1086 repo, commit_id and file path
1086 repo, commit_id and file path
1087
1087
1088 :param repo_name: name of the repository
1088 :param repo_name: name of the repository
1089 :param commit_id: commit_id of file tree
1089 :param commit_id: commit_id of file tree
1090 :param f_path: file path of the requested directory
1090 :param f_path: file path of the requested directory
1091 """
1091 """
1092
1092
1093 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1093 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1094 try:
1094 try:
1095 dir_node = commit.get_node(f_path)
1095 dir_node = commit.get_node(f_path)
1096 except RepositoryError as e:
1096 except RepositoryError as e:
1097 return 'error {}'.format(safe_str(e))
1097 return 'error {}'.format(safe_str(e))
1098
1098
1099 if dir_node.is_file():
1099 if dir_node.is_file():
1100 return ''
1100 return ''
1101
1101
1102 c.file = dir_node
1102 c.file = dir_node
1103 c.commit = commit
1103 c.commit = commit
1104
1104
1105 # using force=True here, make a little trick. We flush the cache and
1105 # using force=True here, make a little trick. We flush the cache and
1106 # compute it using the same key as without full_load, so the fully
1106 # compute it using the same key as without full_load, so the fully
1107 # loaded cached tree is now returned instead of partial
1107 # loaded cached tree is now returned instead of partial
1108 return self._get_tree_at_commit(
1108 return self._get_tree_at_commit(
1109 repo_name, commit.raw_id, dir_node.path, full_load=True,
1109 repo_name, commit.raw_id, dir_node.path, full_load=True,
1110 force=True)
1110 force=True)
@@ -1,1008 +1,1009 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231
231
232 # find the ancestor for this pr
232 # find the ancestor for this pr
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
235
235
236 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
238
238
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
241
241
242 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
243 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
244
244
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
247
247
248 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
249 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
250 if not pullrequest_title:
250 if not pullrequest_title:
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
252 source=source_repo,
252 source=source_repo,
253 source_ref=title_source_ref,
253 source_ref=title_source_ref,
254 target=target_repo
254 target=target_repo
255 )
255 )
256
256
257 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
258
259 get_default_reviewers_data, validate_default_reviewers = \
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
260 PullRequestModel().get_reviewer_functions()
261
261
262 # recalculate reviewers logic, to make sure we can validate this
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user.get_instance(), source_db_repo,
264 c.rhodecode_user.get_instance(), source_db_repo,
265 source_commit, target_db_repo, target_commit)
265 source_commit, target_db_repo, target_commit)
266
266
267 given_reviewers = _form['review_members']
267 given_reviewers = _form['review_members']
268 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
269
269
270 try:
270 try:
271 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
273 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
274 description, reviewer_rules
274 description, reviewer_rules
275 )
275 )
276 Session().commit()
276 Session().commit()
277 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
278 category='success')
278 category='success')
279 except Exception as e:
279 except Exception as e:
280 msg = _('Error occurred during creation of this pull request.')
280 msg = _('Error occurred during creation of this pull request.')
281 log.exception(msg)
281 log.exception(msg)
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
284
284
285 return redirect(url('pullrequest_show', repo_name=target_repo,
285 return redirect(url('pullrequest_show', repo_name=target_repo,
286 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
287
287
288 @LoginRequired()
288 @LoginRequired()
289 @NotAnonymous()
289 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
291 'repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 @jsonify
293 @jsonify
294 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
297 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
300 if allowed_to_update:
300 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
302
302
303 if 'review_members' in controls:
303 if 'review_members' in controls:
304 self._update_reviewers(
304 self._update_reviewers(
305 pull_request_id, controls['review_members'],
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
306 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
308 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 self._edit_pull_request(pull_request)
310 self._edit_pull_request(pull_request)
311 else:
311 else:
312 raise HTTPBadRequest()
312 raise HTTPBadRequest()
313 return True
313 return True
314 raise HTTPForbidden()
314 raise HTTPForbidden()
315
315
316 def _edit_pull_request(self, pull_request):
316 def _edit_pull_request(self, pull_request):
317 try:
317 try:
318 PullRequestModel().edit(
318 PullRequestModel().edit(
319 pull_request, request.POST.get('title'),
319 pull_request, request.POST.get('title'),
320 request.POST.get('description'))
320 request.POST.get('description'), c.rhodecode_user)
321 except ValueError:
321 except ValueError:
322 msg = _(u'Cannot update closed pull requests.')
322 msg = _(u'Cannot update closed pull requests.')
323 h.flash(msg, category='error')
323 h.flash(msg, category='error')
324 return
324 return
325 else:
325 else:
326 Session().commit()
326 Session().commit()
327
327
328 msg = _(u'Pull request title & description updated.')
328 msg = _(u'Pull request title & description updated.')
329 h.flash(msg, category='success')
329 h.flash(msg, category='success')
330 return
330 return
331
331
332 def _update_commits(self, pull_request):
332 def _update_commits(self, pull_request):
333 resp = PullRequestModel().update_commits(pull_request)
333 resp = PullRequestModel().update_commits(pull_request)
334
334
335 if resp.executed:
335 if resp.executed:
336
336
337 if resp.target_changed and resp.source_changed:
337 if resp.target_changed and resp.source_changed:
338 changed = 'target and source repositories'
338 changed = 'target and source repositories'
339 elif resp.target_changed and not resp.source_changed:
339 elif resp.target_changed and not resp.source_changed:
340 changed = 'target repository'
340 changed = 'target repository'
341 elif not resp.target_changed and resp.source_changed:
341 elif not resp.target_changed and resp.source_changed:
342 changed = 'source repository'
342 changed = 'source repository'
343 else:
343 else:
344 changed = 'nothing'
344 changed = 'nothing'
345
345
346 msg = _(
346 msg = _(
347 u'Pull request updated to "{source_commit_id}" with '
347 u'Pull request updated to "{source_commit_id}" with '
348 u'{count_added} added, {count_removed} removed commits. '
348 u'{count_added} added, {count_removed} removed commits. '
349 u'Source of changes: {change_source}')
349 u'Source of changes: {change_source}')
350 msg = msg.format(
350 msg = msg.format(
351 source_commit_id=pull_request.source_ref_parts.commit_id,
351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 count_added=len(resp.changes.added),
352 count_added=len(resp.changes.added),
353 count_removed=len(resp.changes.removed),
353 count_removed=len(resp.changes.removed),
354 change_source=changed)
354 change_source=changed)
355 h.flash(msg, category='success')
355 h.flash(msg, category='success')
356
356
357 registry = get_current_registry()
357 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 if channelstream_config.get('enabled'):
360 if channelstream_config.get('enabled'):
361 message = msg + (
361 message = msg + (
362 ' - <a onclick="window.location.reload()">'
362 ' - <a onclick="window.location.reload()">'
363 '<strong>{}</strong></a>'.format(_('Reload page')))
363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 channel = '/repo${}$/pr/{}'.format(
364 channel = '/repo${}$/pr/{}'.format(
365 pull_request.target_repo.repo_name,
365 pull_request.target_repo.repo_name,
366 pull_request.pull_request_id
366 pull_request.pull_request_id
367 )
367 )
368 payload = {
368 payload = {
369 'type': 'message',
369 'type': 'message',
370 'user': 'system',
370 'user': 'system',
371 'exclude_users': [request.user.username],
371 'exclude_users': [request.user.username],
372 'channel': channel,
372 'channel': channel,
373 'message': {
373 'message': {
374 'message': message,
374 'message': message,
375 'level': 'success',
375 'level': 'success',
376 'topic': '/notifications'
376 'topic': '/notifications'
377 }
377 }
378 }
378 }
379 channelstream_request(
379 channelstream_request(
380 channelstream_config, [payload], '/message',
380 channelstream_config, [payload], '/message',
381 raise_exc=False)
381 raise_exc=False)
382 else:
382 else:
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 warning_reasons = [
384 warning_reasons = [
385 UpdateFailureReason.NO_CHANGE,
385 UpdateFailureReason.NO_CHANGE,
386 UpdateFailureReason.WRONG_REF_TYPE,
386 UpdateFailureReason.WRONG_REF_TYPE,
387 ]
387 ]
388 category = 'warning' if resp.reason in warning_reasons else 'error'
388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 h.flash(msg, category=category)
389 h.flash(msg, category=category)
390
390
391 @auth.CSRFRequired()
391 @auth.CSRFRequired()
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 'repository.admin')
395 'repository.admin')
396 def merge(self, repo_name, pull_request_id):
396 def merge(self, repo_name, pull_request_id):
397 """
397 """
398 POST /{repo_name}/pull-request/{pull_request_id}
398 POST /{repo_name}/pull-request/{pull_request_id}
399
399
400 Merge will perform a server-side merge of the specified
400 Merge will perform a server-side merge of the specified
401 pull request, if the pull request is approved and mergeable.
401 pull request, if the pull request is approved and mergeable.
402 After successful merging, the pull request is automatically
402 After successful merging, the pull request is automatically
403 closed, with a relevant comment.
403 closed, with a relevant comment.
404 """
404 """
405 pull_request_id = safe_int(pull_request_id)
405 pull_request_id = safe_int(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
407 user = c.rhodecode_user
407 user = c.rhodecode_user
408
408
409 check = MergeCheck.validate(pull_request, user)
409 check = MergeCheck.validate(pull_request, user)
410 merge_possible = not check.failed
410 merge_possible = not check.failed
411
411
412 for err_type, error_msg in check.errors:
412 for err_type, error_msg in check.errors:
413 h.flash(error_msg, category=err_type)
413 h.flash(error_msg, category=err_type)
414
414
415 if merge_possible:
415 if merge_possible:
416 log.debug("Pre-conditions checked, trying to merge.")
416 log.debug("Pre-conditions checked, trying to merge.")
417 extras = vcs_operation_context(
417 extras = vcs_operation_context(
418 request.environ, repo_name=pull_request.target_repo.repo_name,
418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 username=user.username, action='push',
419 username=user.username, action='push',
420 scm=pull_request.target_repo.repo_type)
420 scm=pull_request.target_repo.repo_type)
421 self._merge_pull_request(pull_request, user, extras)
421 self._merge_pull_request(pull_request, user, extras)
422
422
423 return redirect(url(
423 return redirect(url(
424 'pullrequest_show',
424 'pullrequest_show',
425 repo_name=pull_request.target_repo.repo_name,
425 repo_name=pull_request.target_repo.repo_name,
426 pull_request_id=pull_request.pull_request_id))
426 pull_request_id=pull_request.pull_request_id))
427
427
428 def _merge_pull_request(self, pull_request, user, extras):
428 def _merge_pull_request(self, pull_request, user, extras):
429 merge_resp = PullRequestModel().merge(
429 merge_resp = PullRequestModel().merge(
430 pull_request, user, extras=extras)
430 pull_request, user, extras=extras)
431
431
432 if merge_resp.executed:
432 if merge_resp.executed:
433 log.debug("The merge was successful, closing the pull request.")
433 log.debug("The merge was successful, closing the pull request.")
434 PullRequestModel().close_pull_request(
434 PullRequestModel().close_pull_request(
435 pull_request.pull_request_id, user)
435 pull_request.pull_request_id, user)
436 Session().commit()
436 Session().commit()
437 msg = _('Pull request was successfully merged and closed.')
437 msg = _('Pull request was successfully merged and closed.')
438 h.flash(msg, category='success')
438 h.flash(msg, category='success')
439 else:
439 else:
440 log.debug(
440 log.debug(
441 "The merge was not successful. Merge response: %s",
441 "The merge was not successful. Merge response: %s",
442 merge_resp)
442 merge_resp)
443 msg = PullRequestModel().merge_status_message(
443 msg = PullRequestModel().merge_status_message(
444 merge_resp.failure_reason)
444 merge_resp.failure_reason)
445 h.flash(msg, category='error')
445 h.flash(msg, category='error')
446
446
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448
448
449 get_default_reviewers_data, validate_default_reviewers = \
449 get_default_reviewers_data, validate_default_reviewers = \
450 PullRequestModel().get_reviewer_functions()
450 PullRequestModel().get_reviewer_functions()
451
451
452 try:
452 try:
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 except ValueError as e:
454 except ValueError as e:
455 log.error('Reviewers Validation: {}'.format(e))
455 log.error('Reviewers Validation: {}'.format(e))
456 h.flash(e, category='error')
456 h.flash(e, category='error')
457 return
457 return
458
458
459 PullRequestModel().update_reviewers(pull_request_id, reviewers)
459 PullRequestModel().update_reviewers(
460 pull_request_id, reviewers, c.rhodecode_user)
460 h.flash(_('Pull request reviewers updated.'), category='success')
461 h.flash(_('Pull request reviewers updated.'), category='success')
461 Session().commit()
462 Session().commit()
462
463
463 @LoginRequired()
464 @LoginRequired()
464 @NotAnonymous()
465 @NotAnonymous()
465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 'repository.admin')
467 'repository.admin')
467 @auth.CSRFRequired()
468 @auth.CSRFRequired()
468 @jsonify
469 @jsonify
469 def delete(self, repo_name, pull_request_id):
470 def delete(self, repo_name, pull_request_id):
470 pull_request_id = safe_int(pull_request_id)
471 pull_request_id = safe_int(pull_request_id)
471 pull_request = PullRequest.get_or_404(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
472
473
473 pr_closed = pull_request.is_closed()
474 pr_closed = pull_request.is_closed()
474 allowed_to_delete = PullRequestModel().check_user_delete(
475 allowed_to_delete = PullRequestModel().check_user_delete(
475 pull_request, c.rhodecode_user) and not pr_closed
476 pull_request, c.rhodecode_user) and not pr_closed
476
477
477 # only owner can delete it !
478 # only owner can delete it !
478 if allowed_to_delete:
479 if allowed_to_delete:
479 PullRequestModel().delete(pull_request)
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
480 Session().commit()
481 Session().commit()
481 h.flash(_('Successfully deleted pull request'),
482 h.flash(_('Successfully deleted pull request'),
482 category='success')
483 category='success')
483 return redirect(url('my_account_pullrequests'))
484 return redirect(url('my_account_pullrequests'))
484
485
485 h.flash(_('Your are not allowed to delete this pull request'),
486 h.flash(_('Your are not allowed to delete this pull request'),
486 category='error')
487 category='error')
487 raise HTTPForbidden()
488 raise HTTPForbidden()
488
489
489 def _get_pr_version(self, pull_request_id, version=None):
490 def _get_pr_version(self, pull_request_id, version=None):
490 pull_request_id = safe_int(pull_request_id)
491 pull_request_id = safe_int(pull_request_id)
491 at_version = None
492 at_version = None
492
493
493 if version and version == 'latest':
494 if version and version == 'latest':
494 pull_request_ver = PullRequest.get(pull_request_id)
495 pull_request_ver = PullRequest.get(pull_request_id)
495 pull_request_obj = pull_request_ver
496 pull_request_obj = pull_request_ver
496 _org_pull_request_obj = pull_request_obj
497 _org_pull_request_obj = pull_request_obj
497 at_version = 'latest'
498 at_version = 'latest'
498 elif version:
499 elif version:
499 pull_request_ver = PullRequestVersion.get_or_404(version)
500 pull_request_ver = PullRequestVersion.get_or_404(version)
500 pull_request_obj = pull_request_ver
501 pull_request_obj = pull_request_ver
501 _org_pull_request_obj = pull_request_ver.pull_request
502 _org_pull_request_obj = pull_request_ver.pull_request
502 at_version = pull_request_ver.pull_request_version_id
503 at_version = pull_request_ver.pull_request_version_id
503 else:
504 else:
504 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
505 pull_request_id)
506 pull_request_id)
506
507
507 pull_request_display_obj = PullRequest.get_pr_display_object(
508 pull_request_display_obj = PullRequest.get_pr_display_object(
508 pull_request_obj, _org_pull_request_obj)
509 pull_request_obj, _org_pull_request_obj)
509
510
510 return _org_pull_request_obj, pull_request_obj, \
511 return _org_pull_request_obj, pull_request_obj, \
511 pull_request_display_obj, at_version
512 pull_request_display_obj, at_version
512
513
513 def _get_diffset(
514 def _get_diffset(
514 self, source_repo, source_ref_id, target_ref_id, target_commit,
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
515 source_commit, diff_limit, file_limit, display_inline_comments):
516 source_commit, diff_limit, file_limit, display_inline_comments):
516 vcs_diff = PullRequestModel().get_diff(
517 vcs_diff = PullRequestModel().get_diff(
517 source_repo, source_ref_id, target_ref_id)
518 source_repo, source_ref_id, target_ref_id)
518
519
519 diff_processor = diffs.DiffProcessor(
520 diff_processor = diffs.DiffProcessor(
520 vcs_diff, format='newdiff', diff_limit=diff_limit,
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
521 file_limit=file_limit, show_full_diff=c.fulldiff)
522 file_limit=file_limit, show_full_diff=c.fulldiff)
522
523
523 _parsed = diff_processor.prepare()
524 _parsed = diff_processor.prepare()
524
525
525 def _node_getter(commit):
526 def _node_getter(commit):
526 def get_node(fname):
527 def get_node(fname):
527 try:
528 try:
528 return commit.get_node(fname)
529 return commit.get_node(fname)
529 except NodeDoesNotExistError:
530 except NodeDoesNotExistError:
530 return None
531 return None
531
532
532 return get_node
533 return get_node
533
534
534 diffset = codeblocks.DiffSet(
535 diffset = codeblocks.DiffSet(
535 repo_name=c.repo_name,
536 repo_name=c.repo_name,
536 source_repo_name=c.source_repo.repo_name,
537 source_repo_name=c.source_repo.repo_name,
537 source_node_getter=_node_getter(target_commit),
538 source_node_getter=_node_getter(target_commit),
538 target_node_getter=_node_getter(source_commit),
539 target_node_getter=_node_getter(source_commit),
539 comments=display_inline_comments
540 comments=display_inline_comments
540 )
541 )
541 diffset = diffset.render_patchset(
542 diffset = diffset.render_patchset(
542 _parsed, target_commit.raw_id, source_commit.raw_id)
543 _parsed, target_commit.raw_id, source_commit.raw_id)
543
544
544 return diffset
545 return diffset
545
546
546 @LoginRequired()
547 @LoginRequired()
547 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
548 'repository.admin')
549 'repository.admin')
549 def show(self, repo_name, pull_request_id):
550 def show(self, repo_name, pull_request_id):
550 pull_request_id = safe_int(pull_request_id)
551 pull_request_id = safe_int(pull_request_id)
551 version = request.GET.get('version')
552 version = request.GET.get('version')
552 from_version = request.GET.get('from_version') or version
553 from_version = request.GET.get('from_version') or version
553 merge_checks = request.GET.get('merge_checks')
554 merge_checks = request.GET.get('merge_checks')
554 c.fulldiff = str2bool(request.GET.get('fulldiff'))
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
555
556
556 (pull_request_latest,
557 (pull_request_latest,
557 pull_request_at_ver,
558 pull_request_at_ver,
558 pull_request_display_obj,
559 pull_request_display_obj,
559 at_version) = self._get_pr_version(
560 at_version) = self._get_pr_version(
560 pull_request_id, version=version)
561 pull_request_id, version=version)
561 pr_closed = pull_request_latest.is_closed()
562 pr_closed = pull_request_latest.is_closed()
562
563
563 if pr_closed and (version or from_version):
564 if pr_closed and (version or from_version):
564 # not allow to browse versions
565 # not allow to browse versions
565 return redirect(h.url('pullrequest_show', repo_name=repo_name,
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
566 pull_request_id=pull_request_id))
567 pull_request_id=pull_request_id))
567
568
568 versions = pull_request_display_obj.versions()
569 versions = pull_request_display_obj.versions()
569
570
570 c.at_version = at_version
571 c.at_version = at_version
571 c.at_version_num = (at_version
572 c.at_version_num = (at_version
572 if at_version and at_version != 'latest'
573 if at_version and at_version != 'latest'
573 else None)
574 else None)
574 c.at_version_pos = ChangesetComment.get_index_from_version(
575 c.at_version_pos = ChangesetComment.get_index_from_version(
575 c.at_version_num, versions)
576 c.at_version_num, versions)
576
577
577 (prev_pull_request_latest,
578 (prev_pull_request_latest,
578 prev_pull_request_at_ver,
579 prev_pull_request_at_ver,
579 prev_pull_request_display_obj,
580 prev_pull_request_display_obj,
580 prev_at_version) = self._get_pr_version(
581 prev_at_version) = self._get_pr_version(
581 pull_request_id, version=from_version)
582 pull_request_id, version=from_version)
582
583
583 c.from_version = prev_at_version
584 c.from_version = prev_at_version
584 c.from_version_num = (prev_at_version
585 c.from_version_num = (prev_at_version
585 if prev_at_version and prev_at_version != 'latest'
586 if prev_at_version and prev_at_version != 'latest'
586 else None)
587 else None)
587 c.from_version_pos = ChangesetComment.get_index_from_version(
588 c.from_version_pos = ChangesetComment.get_index_from_version(
588 c.from_version_num, versions)
589 c.from_version_num, versions)
589
590
590 # define if we're in COMPARE mode or VIEW at version mode
591 # define if we're in COMPARE mode or VIEW at version mode
591 compare = at_version != prev_at_version
592 compare = at_version != prev_at_version
592
593
593 # pull_requests repo_name we opened it against
594 # pull_requests repo_name we opened it against
594 # ie. target_repo must match
595 # ie. target_repo must match
595 if repo_name != pull_request_at_ver.target_repo.repo_name:
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
596 raise HTTPNotFound
597 raise HTTPNotFound
597
598
598 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
599 pull_request_at_ver)
600 pull_request_at_ver)
600
601
601 c.pull_request = pull_request_display_obj
602 c.pull_request = pull_request_display_obj
602 c.pull_request_latest = pull_request_latest
603 c.pull_request_latest = pull_request_latest
603
604
604 if compare or (at_version and not at_version == 'latest'):
605 if compare or (at_version and not at_version == 'latest'):
605 c.allowed_to_change_status = False
606 c.allowed_to_change_status = False
606 c.allowed_to_update = False
607 c.allowed_to_update = False
607 c.allowed_to_merge = False
608 c.allowed_to_merge = False
608 c.allowed_to_delete = False
609 c.allowed_to_delete = False
609 c.allowed_to_comment = False
610 c.allowed_to_comment = False
610 c.allowed_to_close = False
611 c.allowed_to_close = False
611 else:
612 else:
612 can_change_status = PullRequestModel().check_user_change_status(
613 can_change_status = PullRequestModel().check_user_change_status(
613 pull_request_at_ver, c.rhodecode_user)
614 pull_request_at_ver, c.rhodecode_user)
614 c.allowed_to_change_status = can_change_status and not pr_closed
615 c.allowed_to_change_status = can_change_status and not pr_closed
615
616
616 c.allowed_to_update = PullRequestModel().check_user_update(
617 c.allowed_to_update = PullRequestModel().check_user_update(
617 pull_request_latest, c.rhodecode_user) and not pr_closed
618 pull_request_latest, c.rhodecode_user) and not pr_closed
618 c.allowed_to_merge = PullRequestModel().check_user_merge(
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
619 pull_request_latest, c.rhodecode_user) and not pr_closed
620 pull_request_latest, c.rhodecode_user) and not pr_closed
620 c.allowed_to_delete = PullRequestModel().check_user_delete(
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
621 pull_request_latest, c.rhodecode_user) and not pr_closed
622 pull_request_latest, c.rhodecode_user) and not pr_closed
622 c.allowed_to_comment = not pr_closed
623 c.allowed_to_comment = not pr_closed
623 c.allowed_to_close = c.allowed_to_merge and not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
624
625
625 c.forbid_adding_reviewers = False
626 c.forbid_adding_reviewers = False
626 c.forbid_author_to_review = False
627 c.forbid_author_to_review = False
627 c.forbid_commit_author_to_review = False
628 c.forbid_commit_author_to_review = False
628
629
629 if pull_request_latest.reviewer_data and \
630 if pull_request_latest.reviewer_data and \
630 'rules' in pull_request_latest.reviewer_data:
631 'rules' in pull_request_latest.reviewer_data:
631 rules = pull_request_latest.reviewer_data['rules'] or {}
632 rules = pull_request_latest.reviewer_data['rules'] or {}
632 try:
633 try:
633 c.forbid_adding_reviewers = rules.get(
634 c.forbid_adding_reviewers = rules.get(
634 'forbid_adding_reviewers')
635 'forbid_adding_reviewers')
635 c.forbid_author_to_review = rules.get(
636 c.forbid_author_to_review = rules.get(
636 'forbid_author_to_review')
637 'forbid_author_to_review')
637 c.forbid_commit_author_to_review = rules.get(
638 c.forbid_commit_author_to_review = rules.get(
638 'forbid_commit_author_to_review')
639 'forbid_commit_author_to_review')
639 except Exception:
640 except Exception:
640 pass
641 pass
641
642
642 # check merge capabilities
643 # check merge capabilities
643 _merge_check = MergeCheck.validate(
644 _merge_check = MergeCheck.validate(
644 pull_request_latest, user=c.rhodecode_user)
645 pull_request_latest, user=c.rhodecode_user)
645 c.pr_merge_errors = _merge_check.error_details
646 c.pr_merge_errors = _merge_check.error_details
646 c.pr_merge_possible = not _merge_check.failed
647 c.pr_merge_possible = not _merge_check.failed
647 c.pr_merge_message = _merge_check.merge_msg
648 c.pr_merge_message = _merge_check.merge_msg
648
649
649 c.pull_request_review_status = _merge_check.review_status
650 c.pull_request_review_status = _merge_check.review_status
650 if merge_checks:
651 if merge_checks:
651 return render('/pullrequests/pullrequest_merge_checks.mako')
652 return render('/pullrequests/pullrequest_merge_checks.mako')
652
653
653 comments_model = CommentsModel()
654 comments_model = CommentsModel()
654
655
655 # reviewers and statuses
656 # reviewers and statuses
656 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
657 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
658
659
659 # GENERAL COMMENTS with versions #
660 # GENERAL COMMENTS with versions #
660 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
661 q = q.order_by(ChangesetComment.comment_id.asc())
662 q = q.order_by(ChangesetComment.comment_id.asc())
662 general_comments = q
663 general_comments = q
663
664
664 # pick comments we want to render at current version
665 # pick comments we want to render at current version
665 c.comment_versions = comments_model.aggregate_comments(
666 c.comment_versions = comments_model.aggregate_comments(
666 general_comments, versions, c.at_version_num)
667 general_comments, versions, c.at_version_num)
667 c.comments = c.comment_versions[c.at_version_num]['until']
668 c.comments = c.comment_versions[c.at_version_num]['until']
668
669
669 # INLINE COMMENTS with versions #
670 # INLINE COMMENTS with versions #
670 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
671 q = q.order_by(ChangesetComment.comment_id.asc())
672 q = q.order_by(ChangesetComment.comment_id.asc())
672 inline_comments = q
673 inline_comments = q
673
674
674 c.inline_versions = comments_model.aggregate_comments(
675 c.inline_versions = comments_model.aggregate_comments(
675 inline_comments, versions, c.at_version_num, inline=True)
676 inline_comments, versions, c.at_version_num, inline=True)
676
677
677 # inject latest version
678 # inject latest version
678 latest_ver = PullRequest.get_pr_display_object(
679 latest_ver = PullRequest.get_pr_display_object(
679 pull_request_latest, pull_request_latest)
680 pull_request_latest, pull_request_latest)
680
681
681 c.versions = versions + [latest_ver]
682 c.versions = versions + [latest_ver]
682
683
683 # if we use version, then do not show later comments
684 # if we use version, then do not show later comments
684 # than current version
685 # than current version
685 display_inline_comments = collections.defaultdict(
686 display_inline_comments = collections.defaultdict(
686 lambda: collections.defaultdict(list))
687 lambda: collections.defaultdict(list))
687 for co in inline_comments:
688 for co in inline_comments:
688 if c.at_version_num:
689 if c.at_version_num:
689 # pick comments that are at least UPTO given version, so we
690 # pick comments that are at least UPTO given version, so we
690 # don't render comments for higher version
691 # don't render comments for higher version
691 should_render = co.pull_request_version_id and \
692 should_render = co.pull_request_version_id and \
692 co.pull_request_version_id <= c.at_version_num
693 co.pull_request_version_id <= c.at_version_num
693 else:
694 else:
694 # showing all, for 'latest'
695 # showing all, for 'latest'
695 should_render = True
696 should_render = True
696
697
697 if should_render:
698 if should_render:
698 display_inline_comments[co.f_path][co.line_no].append(co)
699 display_inline_comments[co.f_path][co.line_no].append(co)
699
700
700 # load diff data into template context, if we use compare mode then
701 # load diff data into template context, if we use compare mode then
701 # diff is calculated based on changes between versions of PR
702 # diff is calculated based on changes between versions of PR
702
703
703 source_repo = pull_request_at_ver.source_repo
704 source_repo = pull_request_at_ver.source_repo
704 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
705
706
706 target_repo = pull_request_at_ver.target_repo
707 target_repo = pull_request_at_ver.target_repo
707 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
708
709
709 if compare:
710 if compare:
710 # in compare switch the diff base to latest commit from prev version
711 # in compare switch the diff base to latest commit from prev version
711 target_ref_id = prev_pull_request_display_obj.revisions[0]
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
712
713
713 # despite opening commits for bookmarks/branches/tags, we always
714 # despite opening commits for bookmarks/branches/tags, we always
714 # convert this to rev to prevent changes after bookmark or branch change
715 # convert this to rev to prevent changes after bookmark or branch change
715 c.source_ref_type = 'rev'
716 c.source_ref_type = 'rev'
716 c.source_ref = source_ref_id
717 c.source_ref = source_ref_id
717
718
718 c.target_ref_type = 'rev'
719 c.target_ref_type = 'rev'
719 c.target_ref = target_ref_id
720 c.target_ref = target_ref_id
720
721
721 c.source_repo = source_repo
722 c.source_repo = source_repo
722 c.target_repo = target_repo
723 c.target_repo = target_repo
723
724
724 # diff_limit is the old behavior, will cut off the whole diff
725 # diff_limit is the old behavior, will cut off the whole diff
725 # if the limit is applied otherwise will just hide the
726 # if the limit is applied otherwise will just hide the
726 # big files from the front-end
727 # big files from the front-end
727 diff_limit = self.cut_off_limit_diff
728 diff_limit = self.cut_off_limit_diff
728 file_limit = self.cut_off_limit_file
729 file_limit = self.cut_off_limit_file
729
730
730 c.commit_ranges = []
731 c.commit_ranges = []
731 source_commit = EmptyCommit()
732 source_commit = EmptyCommit()
732 target_commit = EmptyCommit()
733 target_commit = EmptyCommit()
733 c.missing_requirements = False
734 c.missing_requirements = False
734
735
735 source_scm = source_repo.scm_instance()
736 source_scm = source_repo.scm_instance()
736 target_scm = target_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
737
738
738 # try first shadow repo, fallback to regular repo
739 # try first shadow repo, fallback to regular repo
739 try:
740 try:
740 commits_source_repo = pull_request_latest.get_shadow_repo()
741 commits_source_repo = pull_request_latest.get_shadow_repo()
741 except Exception:
742 except Exception:
742 log.debug('Failed to get shadow repo', exc_info=True)
743 log.debug('Failed to get shadow repo', exc_info=True)
743 commits_source_repo = source_scm
744 commits_source_repo = source_scm
744
745
745 c.commits_source_repo = commits_source_repo
746 c.commits_source_repo = commits_source_repo
746 commit_cache = {}
747 commit_cache = {}
747 try:
748 try:
748 pre_load = ["author", "branch", "date", "message"]
749 pre_load = ["author", "branch", "date", "message"]
749 show_revs = pull_request_at_ver.revisions
750 show_revs = pull_request_at_ver.revisions
750 for rev in show_revs:
751 for rev in show_revs:
751 comm = commits_source_repo.get_commit(
752 comm = commits_source_repo.get_commit(
752 commit_id=rev, pre_load=pre_load)
753 commit_id=rev, pre_load=pre_load)
753 c.commit_ranges.append(comm)
754 c.commit_ranges.append(comm)
754 commit_cache[comm.raw_id] = comm
755 commit_cache[comm.raw_id] = comm
755
756
756 # Order here matters, we first need to get target, and then
757 # Order here matters, we first need to get target, and then
757 # the source
758 # the source
758 target_commit = commits_source_repo.get_commit(
759 target_commit = commits_source_repo.get_commit(
759 commit_id=safe_str(target_ref_id))
760 commit_id=safe_str(target_ref_id))
760
761
761 source_commit = commits_source_repo.get_commit(
762 source_commit = commits_source_repo.get_commit(
762 commit_id=safe_str(source_ref_id))
763 commit_id=safe_str(source_ref_id))
763
764
764 except CommitDoesNotExistError:
765 except CommitDoesNotExistError:
765 log.warning(
766 log.warning(
766 'Failed to get commit from `{}` repo'.format(
767 'Failed to get commit from `{}` repo'.format(
767 commits_source_repo), exc_info=True)
768 commits_source_repo), exc_info=True)
768 except RepositoryRequirementError:
769 except RepositoryRequirementError:
769 log.warning(
770 log.warning(
770 'Failed to get all required data from repo', exc_info=True)
771 'Failed to get all required data from repo', exc_info=True)
771 c.missing_requirements = True
772 c.missing_requirements = True
772
773
773 c.ancestor = None # set it to None, to hide it from PR view
774 c.ancestor = None # set it to None, to hide it from PR view
774
775
775 try:
776 try:
776 ancestor_id = source_scm.get_common_ancestor(
777 ancestor_id = source_scm.get_common_ancestor(
777 source_commit.raw_id, target_commit.raw_id, target_scm)
778 source_commit.raw_id, target_commit.raw_id, target_scm)
778 c.ancestor_commit = source_scm.get_commit(ancestor_id)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
779 except Exception:
780 except Exception:
780 c.ancestor_commit = None
781 c.ancestor_commit = None
781
782
782 c.statuses = source_repo.statuses(
783 c.statuses = source_repo.statuses(
783 [x.raw_id for x in c.commit_ranges])
784 [x.raw_id for x in c.commit_ranges])
784
785
785 # auto collapse if we have more than limit
786 # auto collapse if we have more than limit
786 collapse_limit = diffs.DiffProcessor._collapse_commits_over
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
787 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
788 c.compare_mode = compare
789 c.compare_mode = compare
789
790
790 c.missing_commits = False
791 c.missing_commits = False
791 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
792 or source_commit == target_commit):
793 or source_commit == target_commit):
793
794
794 c.missing_commits = True
795 c.missing_commits = True
795 else:
796 else:
796
797
797 c.diffset = self._get_diffset(
798 c.diffset = self._get_diffset(
798 commits_source_repo, source_ref_id, target_ref_id,
799 commits_source_repo, source_ref_id, target_ref_id,
799 target_commit, source_commit,
800 target_commit, source_commit,
800 diff_limit, file_limit, display_inline_comments)
801 diff_limit, file_limit, display_inline_comments)
801
802
802 c.limited_diff = c.diffset.limited_diff
803 c.limited_diff = c.diffset.limited_diff
803
804
804 # calculate removed files that are bound to comments
805 # calculate removed files that are bound to comments
805 comment_deleted_files = [
806 comment_deleted_files = [
806 fname for fname in display_inline_comments
807 fname for fname in display_inline_comments
807 if fname not in c.diffset.file_stats]
808 if fname not in c.diffset.file_stats]
808
809
809 c.deleted_files_comments = collections.defaultdict(dict)
810 c.deleted_files_comments = collections.defaultdict(dict)
810 for fname, per_line_comments in display_inline_comments.items():
811 for fname, per_line_comments in display_inline_comments.items():
811 if fname in comment_deleted_files:
812 if fname in comment_deleted_files:
812 c.deleted_files_comments[fname]['stats'] = 0
813 c.deleted_files_comments[fname]['stats'] = 0
813 c.deleted_files_comments[fname]['comments'] = list()
814 c.deleted_files_comments[fname]['comments'] = list()
814 for lno, comments in per_line_comments.items():
815 for lno, comments in per_line_comments.items():
815 c.deleted_files_comments[fname]['comments'].extend(
816 c.deleted_files_comments[fname]['comments'].extend(
816 comments)
817 comments)
817
818
818 # this is a hack to properly display links, when creating PR, the
819 # this is a hack to properly display links, when creating PR, the
819 # compare view and others uses different notation, and
820 # compare view and others uses different notation, and
820 # compare_commits.mako renders links based on the target_repo.
821 # compare_commits.mako renders links based on the target_repo.
821 # We need to swap that here to generate it properly on the html side
822 # We need to swap that here to generate it properly on the html side
822 c.target_repo = c.source_repo
823 c.target_repo = c.source_repo
823
824
824 c.commit_statuses = ChangesetStatus.STATUSES
825 c.commit_statuses = ChangesetStatus.STATUSES
825
826
826 c.show_version_changes = not pr_closed
827 c.show_version_changes = not pr_closed
827 if c.show_version_changes:
828 if c.show_version_changes:
828 cur_obj = pull_request_at_ver
829 cur_obj = pull_request_at_ver
829 prev_obj = prev_pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
830
831
831 old_commit_ids = prev_obj.revisions
832 old_commit_ids = prev_obj.revisions
832 new_commit_ids = cur_obj.revisions
833 new_commit_ids = cur_obj.revisions
833 commit_changes = PullRequestModel()._calculate_commit_id_changes(
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
834 old_commit_ids, new_commit_ids)
835 old_commit_ids, new_commit_ids)
835 c.commit_changes_summary = commit_changes
836 c.commit_changes_summary = commit_changes
836
837
837 # calculate the diff for commits between versions
838 # calculate the diff for commits between versions
838 c.commit_changes = []
839 c.commit_changes = []
839 mark = lambda cs, fw: list(
840 mark = lambda cs, fw: list(
840 h.itertools.izip_longest([], cs, fillvalue=fw))
841 h.itertools.izip_longest([], cs, fillvalue=fw))
841 for c_type, raw_id in mark(commit_changes.added, 'a') \
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
842 + mark(commit_changes.removed, 'r') \
843 + mark(commit_changes.removed, 'r') \
843 + mark(commit_changes.common, 'c'):
844 + mark(commit_changes.common, 'c'):
844
845
845 if raw_id in commit_cache:
846 if raw_id in commit_cache:
846 commit = commit_cache[raw_id]
847 commit = commit_cache[raw_id]
847 else:
848 else:
848 try:
849 try:
849 commit = commits_source_repo.get_commit(raw_id)
850 commit = commits_source_repo.get_commit(raw_id)
850 except CommitDoesNotExistError:
851 except CommitDoesNotExistError:
851 # in case we fail extracting still use "dummy" commit
852 # in case we fail extracting still use "dummy" commit
852 # for display in commit diff
853 # for display in commit diff
853 commit = h.AttributeDict(
854 commit = h.AttributeDict(
854 {'raw_id': raw_id,
855 {'raw_id': raw_id,
855 'message': 'EMPTY or MISSING COMMIT'})
856 'message': 'EMPTY or MISSING COMMIT'})
856 c.commit_changes.append([c_type, commit])
857 c.commit_changes.append([c_type, commit])
857
858
858 # current user review statuses for each version
859 # current user review statuses for each version
859 c.review_versions = {}
860 c.review_versions = {}
860 if c.rhodecode_user.user_id in allowed_reviewers:
861 if c.rhodecode_user.user_id in allowed_reviewers:
861 for co in general_comments:
862 for co in general_comments:
862 if co.author.user_id == c.rhodecode_user.user_id:
863 if co.author.user_id == c.rhodecode_user.user_id:
863 # each comment has a status change
864 # each comment has a status change
864 status = co.status_change
865 status = co.status_change
865 if status:
866 if status:
866 _ver_pr = status[0].comment.pull_request_version_id
867 _ver_pr = status[0].comment.pull_request_version_id
867 c.review_versions[_ver_pr] = status[0]
868 c.review_versions[_ver_pr] = status[0]
868
869
869 return render('/pullrequests/pullrequest_show.mako')
870 return render('/pullrequests/pullrequest_show.mako')
870
871
871 @LoginRequired()
872 @LoginRequired()
872 @NotAnonymous()
873 @NotAnonymous()
873 @HasRepoPermissionAnyDecorator(
874 @HasRepoPermissionAnyDecorator(
874 'repository.read', 'repository.write', 'repository.admin')
875 'repository.read', 'repository.write', 'repository.admin')
875 @auth.CSRFRequired()
876 @auth.CSRFRequired()
876 @jsonify
877 @jsonify
877 def comment(self, repo_name, pull_request_id):
878 def comment(self, repo_name, pull_request_id):
878 pull_request_id = safe_int(pull_request_id)
879 pull_request_id = safe_int(pull_request_id)
879 pull_request = PullRequest.get_or_404(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
880 if pull_request.is_closed():
881 if pull_request.is_closed():
881 log.debug('comment: forbidden because pull request is closed')
882 log.debug('comment: forbidden because pull request is closed')
882 raise HTTPForbidden()
883 raise HTTPForbidden()
883
884
884 status = request.POST.get('changeset_status', None)
885 status = request.POST.get('changeset_status', None)
885 text = request.POST.get('text')
886 text = request.POST.get('text')
886 comment_type = request.POST.get('comment_type')
887 comment_type = request.POST.get('comment_type')
887 resolves_comment_id = request.POST.get('resolves_comment_id', None)
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
888 close_pull_request = request.POST.get('close_pull_request')
889 close_pull_request = request.POST.get('close_pull_request')
889
890
890 # the logic here should work like following, if we submit close
891 # the logic here should work like following, if we submit close
891 # pr comment, use `close_pull_request_with_comment` function
892 # pr comment, use `close_pull_request_with_comment` function
892 # else handle regular comment logic
893 # else handle regular comment logic
893 user = c.rhodecode_user
894 user = c.rhodecode_user
894 repo = c.rhodecode_db_repo
895 repo = c.rhodecode_db_repo
895
896
896 if close_pull_request:
897 if close_pull_request:
897 # only owner or admin or person with write permissions
898 # only owner or admin or person with write permissions
898 allowed_to_close = PullRequestModel().check_user_update(
899 allowed_to_close = PullRequestModel().check_user_update(
899 pull_request, c.rhodecode_user)
900 pull_request, c.rhodecode_user)
900 if not allowed_to_close:
901 if not allowed_to_close:
901 log.debug('comment: forbidden because not allowed to close '
902 log.debug('comment: forbidden because not allowed to close '
902 'pull request %s', pull_request_id)
903 'pull request %s', pull_request_id)
903 raise HTTPForbidden()
904 raise HTTPForbidden()
904 comment, status = PullRequestModel().close_pull_request_with_comment(
905 comment, status = PullRequestModel().close_pull_request_with_comment(
905 pull_request, user, repo, message=text)
906 pull_request, user, repo, message=text)
906 Session().flush()
907 Session().flush()
907 events.trigger(
908 events.trigger(
908 events.PullRequestCommentEvent(pull_request, comment))
909 events.PullRequestCommentEvent(pull_request, comment))
909
910
910 else:
911 else:
911 # regular comment case, could be inline, or one with status.
912 # regular comment case, could be inline, or one with status.
912 # for that one we check also permissions
913 # for that one we check also permissions
913
914
914 allowed_to_change_status = PullRequestModel().check_user_change_status(
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
915 pull_request, c.rhodecode_user)
916 pull_request, c.rhodecode_user)
916
917
917 if status and allowed_to_change_status:
918 if status and allowed_to_change_status:
918 message = (_('Status change %(transition_icon)s %(status)s')
919 message = (_('Status change %(transition_icon)s %(status)s')
919 % {'transition_icon': '>',
920 % {'transition_icon': '>',
920 'status': ChangesetStatus.get_status_lbl(status)})
921 'status': ChangesetStatus.get_status_lbl(status)})
921 text = text or message
922 text = text or message
922
923
923 comment = CommentsModel().create(
924 comment = CommentsModel().create(
924 text=text,
925 text=text,
925 repo=c.rhodecode_db_repo.repo_id,
926 repo=c.rhodecode_db_repo.repo_id,
926 user=c.rhodecode_user.user_id,
927 user=c.rhodecode_user.user_id,
927 pull_request=pull_request_id,
928 pull_request=pull_request_id,
928 f_path=request.POST.get('f_path'),
929 f_path=request.POST.get('f_path'),
929 line_no=request.POST.get('line'),
930 line_no=request.POST.get('line'),
930 status_change=(ChangesetStatus.get_status_lbl(status)
931 status_change=(ChangesetStatus.get_status_lbl(status)
931 if status and allowed_to_change_status else None),
932 if status and allowed_to_change_status else None),
932 status_change_type=(status
933 status_change_type=(status
933 if status and allowed_to_change_status else None),
934 if status and allowed_to_change_status else None),
934 comment_type=comment_type,
935 comment_type=comment_type,
935 resolves_comment_id=resolves_comment_id
936 resolves_comment_id=resolves_comment_id
936 )
937 )
937
938
938 if allowed_to_change_status:
939 if allowed_to_change_status:
939 # calculate old status before we change it
940 # calculate old status before we change it
940 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
941
942
942 # get status if set !
943 # get status if set !
943 if status:
944 if status:
944 ChangesetStatusModel().set_status(
945 ChangesetStatusModel().set_status(
945 c.rhodecode_db_repo.repo_id,
946 c.rhodecode_db_repo.repo_id,
946 status,
947 status,
947 c.rhodecode_user.user_id,
948 c.rhodecode_user.user_id,
948 comment,
949 comment,
949 pull_request=pull_request_id
950 pull_request=pull_request_id
950 )
951 )
951
952
952 Session().flush()
953 Session().flush()
953 events.trigger(
954 events.trigger(
954 events.PullRequestCommentEvent(pull_request, comment))
955 events.PullRequestCommentEvent(pull_request, comment))
955
956
956 # we now calculate the status of pull request, and based on that
957 # we now calculate the status of pull request, and based on that
957 # calculation we set the commits status
958 # calculation we set the commits status
958 calculated_status = pull_request.calculated_review_status()
959 calculated_status = pull_request.calculated_review_status()
959 if old_calculated_status != calculated_status:
960 if old_calculated_status != calculated_status:
960 PullRequestModel()._trigger_pull_request_hook(
961 PullRequestModel()._trigger_pull_request_hook(
961 pull_request, c.rhodecode_user, 'review_status_change')
962 pull_request, c.rhodecode_user, 'review_status_change')
962
963
963 Session().commit()
964 Session().commit()
964
965
965 if not request.is_xhr:
966 if not request.is_xhr:
966 return redirect(h.url('pullrequest_show', repo_name=repo_name,
967 return redirect(h.url('pullrequest_show', repo_name=repo_name,
967 pull_request_id=pull_request_id))
968 pull_request_id=pull_request_id))
968
969
969 data = {
970 data = {
970 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
971 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
971 }
972 }
972 if comment:
973 if comment:
973 c.co = comment
974 c.co = comment
974 rendered_comment = render('changeset/changeset_comment_block.mako')
975 rendered_comment = render('changeset/changeset_comment_block.mako')
975 data.update(comment.get_dict())
976 data.update(comment.get_dict())
976 data.update({'rendered_text': rendered_comment})
977 data.update({'rendered_text': rendered_comment})
977
978
978 return data
979 return data
979
980
980 @LoginRequired()
981 @LoginRequired()
981 @NotAnonymous()
982 @NotAnonymous()
982 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
983 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
983 'repository.admin')
984 'repository.admin')
984 @auth.CSRFRequired()
985 @auth.CSRFRequired()
985 @jsonify
986 @jsonify
986 def delete_comment(self, repo_name, comment_id):
987 def delete_comment(self, repo_name, comment_id):
987 return self._delete_comment(comment_id)
988 return self._delete_comment(comment_id)
988
989
989 def _delete_comment(self, comment_id):
990 def _delete_comment(self, comment_id):
990 comment_id = safe_int(comment_id)
991 comment_id = safe_int(comment_id)
991 co = ChangesetComment.get_or_404(comment_id)
992 co = ChangesetComment.get_or_404(comment_id)
992 if co.pull_request.is_closed():
993 if co.pull_request.is_closed():
993 # don't allow deleting comments on closed pull request
994 # don't allow deleting comments on closed pull request
994 raise HTTPForbidden()
995 raise HTTPForbidden()
995
996
996 is_owner = co.author.user_id == c.rhodecode_user.user_id
997 is_owner = co.author.user_id == c.rhodecode_user.user_id
997 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 old_calculated_status = co.pull_request.calculated_review_status()
1000 old_calculated_status = co.pull_request.calculated_review_status()
1000 CommentsModel().delete(comment=co)
1001 CommentsModel().delete(comment=co, user=c.rhodecode_user)
1001 Session().commit()
1002 Session().commit()
1002 calculated_status = co.pull_request.calculated_review_status()
1003 calculated_status = co.pull_request.calculated_review_status()
1003 if old_calculated_status != calculated_status:
1004 if old_calculated_status != calculated_status:
1004 PullRequestModel()._trigger_pull_request_hook(
1005 PullRequestModel()._trigger_pull_request_hook(
1005 co.pull_request, c.rhodecode_user, 'review_status_change')
1006 co.pull_request, c.rhodecode_user, 'review_status_change')
1006 return True
1007 return True
1007 else:
1008 else:
1008 raise HTTPForbidden()
1009 raise HTTPForbidden()
@@ -1,240 +1,257 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.model import meta
24 from rhodecode.model import meta
25 from rhodecode.model.db import User, UserLog, Repository
25 from rhodecode.model.db import User, UserLog, Repository
26
26
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30 # action as key, and expected action_data as value
30 # action as key, and expected action_data as value
31 ACTIONS = {
31 ACTIONS_V1 = {
32 'user.login.success': {'user_agent': ''},
32 'user.login.success': {'user_agent': ''},
33 'user.login.failure': {'user_agent': ''},
33 'user.login.failure': {'user_agent': ''},
34 'user.logout': {'user_agent': ''},
34 'user.logout': {'user_agent': ''},
35 'user.password.reset_request': {},
35 'user.password.reset_request': {},
36 'user.push': {'user_agent': '', 'commit_ids': []},
36 'user.push': {'user_agent': '', 'commit_ids': []},
37 'user.pull': {'user_agent': ''},
37 'user.pull': {'user_agent': ''},
38
38
39 'user.create': {'data': {}},
39 'user.create': {'data': {}},
40 'user.delete': {'old_data': {}},
40 'user.delete': {'old_data': {}},
41 'user.edit': {'old_data': {}},
41 'user.edit': {'old_data': {}},
42 'user.edit.permissions': {},
42 'user.edit.permissions': {},
43 'user.edit.ip.add': {},
43 'user.edit.ip.add': {},
44 'user.edit.ip.delete': {},
44 'user.edit.ip.delete': {},
45 'user.edit.token.add': {},
45 'user.edit.token.add': {},
46 'user.edit.token.delete': {},
46 'user.edit.token.delete': {},
47 'user.edit.email.add': {},
47 'user.edit.email.add': {},
48 'user.edit.email.delete': {},
48 'user.edit.email.delete': {},
49 'user.edit.password_reset.enabled': {},
49 'user.edit.password_reset.enabled': {},
50 'user.edit.password_reset.disabled': {},
50 'user.edit.password_reset.disabled': {},
51
51
52 'user_group.create': {'data': {}},
52 'user_group.create': {'data': {}},
53 'user_group.delete': {'old_data': {}},
53 'user_group.delete': {'old_data': {}},
54 'user_group.edit': {'old_data': {}},
54 'user_group.edit': {'old_data': {}},
55 'user_group.edit.permissions': {},
55 'user_group.edit.permissions': {},
56 'user_group.edit.member.add': {},
56 'user_group.edit.member.add': {},
57 'user_group.edit.member.delete': {},
57 'user_group.edit.member.delete': {},
58
58
59 'repo.create': {'data': {}},
59 'repo.create': {'data': {}},
60 'repo.fork': {'data': {}},
60 'repo.fork': {'data': {}},
61 'repo.edit': {'old_data': {}},
61 'repo.edit': {'old_data': {}},
62 'repo.edit.permissions': {},
62 'repo.edit.permissions': {},
63 'repo.delete': {'old_data': {}},
63 'repo.delete': {'old_data': {}},
64 'repo.commit.strip': {},
64 'repo.commit.strip': {},
65 'repo.archive.download': {},
65 'repo.archive.download': {},
66
66
67 'repo.pull_request.create': '',
68 'repo.pull_request.edit': '',
69 'repo.pull_request.delete': '',
70 'repo.pull_request.close': '',
71 'repo.pull_request.merge': '',
72 'repo.pull_request.vote': '',
73 'repo.pull_request.comment.create': '',
74 'repo.pull_request.comment.delete': '',
75
76 'repo.pull_request.reviewer.add': '',
77 'repo.pull_request.reviewer.delete': '',
78
79 'repo.commit.comment.create': '',
80 'repo.commit.comment.delete': '',
81 'repo.commit.vote': '',
82
67 'repo_group.create': {'data': {}},
83 'repo_group.create': {'data': {}},
68 'repo_group.edit': {'old_data': {}},
84 'repo_group.edit': {'old_data': {}},
69 'repo_group.edit.permissions': {},
85 'repo_group.edit.permissions': {},
70 'repo_group.delete': {'old_data': {}},
86 'repo_group.delete': {'old_data': {}},
71 }
87 }
88 ACTIONS = ACTIONS_V1
72
89
73 SOURCE_WEB = 'source_web'
90 SOURCE_WEB = 'source_web'
74 SOURCE_API = 'source_api'
91 SOURCE_API = 'source_api'
75
92
76
93
77 class UserWrap(object):
94 class UserWrap(object):
78 """
95 """
79 Fake object used to imitate AuthUser
96 Fake object used to imitate AuthUser
80 """
97 """
81
98
82 def __init__(self, user_id=None, username=None, ip_addr=None):
99 def __init__(self, user_id=None, username=None, ip_addr=None):
83 self.user_id = user_id
100 self.user_id = user_id
84 self.username = username
101 self.username = username
85 self.ip_addr = ip_addr
102 self.ip_addr = ip_addr
86
103
87
104
88 class RepoWrap(object):
105 class RepoWrap(object):
89 """
106 """
90 Fake object used to imitate RepoObject that audit logger requires
107 Fake object used to imitate RepoObject that audit logger requires
91 """
108 """
92
109
93 def __init__(self, repo_id=None, repo_name=None):
110 def __init__(self, repo_id=None, repo_name=None):
94 self.repo_id = repo_id
111 self.repo_id = repo_id
95 self.repo_name = repo_name
112 self.repo_name = repo_name
96
113
97
114
98 def _store_log(action_name, action_data, user_id, username, user_data,
115 def _store_log(action_name, action_data, user_id, username, user_data,
99 ip_address, repository_id, repository_name):
116 ip_address, repository_id, repository_name):
100 user_log = UserLog()
117 user_log = UserLog()
101 user_log.version = UserLog.VERSION_2
118 user_log.version = UserLog.VERSION_2
102
119
103 user_log.action = action_name
120 user_log.action = action_name
104 user_log.action_data = action_data
121 user_log.action_data = action_data
105
122
106 user_log.user_ip = ip_address
123 user_log.user_ip = ip_address
107
124
108 user_log.user_id = user_id
125 user_log.user_id = user_id
109 user_log.username = username
126 user_log.username = username
110 user_log.user_data = user_data
127 user_log.user_data = user_data
111
128
112 user_log.repository_id = repository_id
129 user_log.repository_id = repository_id
113 user_log.repository_name = repository_name
130 user_log.repository_name = repository_name
114
131
115 user_log.action_date = datetime.datetime.now()
132 user_log.action_date = datetime.datetime.now()
116
133
117 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
134 log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s',
118 action_name, user_id, username, ip_address)
135 action_name, user_id, username, ip_address)
119
136
120 return user_log
137 return user_log
121
138
122
139
123 def store_web(*args, **kwargs):
140 def store_web(*args, **kwargs):
124 if 'action_data' not in kwargs:
141 if 'action_data' not in kwargs:
125 kwargs['action_data'] = {}
142 kwargs['action_data'] = {}
126 kwargs['action_data'].update({
143 kwargs['action_data'].update({
127 'source': SOURCE_WEB
144 'source': SOURCE_WEB
128 })
145 })
129 return store(*args, **kwargs)
146 return store(*args, **kwargs)
130
147
131
148
132 def store_api(*args, **kwargs):
149 def store_api(*args, **kwargs):
133 if 'action_data' not in kwargs:
150 if 'action_data' not in kwargs:
134 kwargs['action_data'] = {}
151 kwargs['action_data'] = {}
135 kwargs['action_data'].update({
152 kwargs['action_data'].update({
136 'source': SOURCE_API
153 'source': SOURCE_API
137 })
154 })
138 return store(*args, **kwargs)
155 return store(*args, **kwargs)
139
156
140
157
141 def store(action, user, action_data=None, user_data=None, ip_addr=None,
158 def store(action, user, action_data=None, user_data=None, ip_addr=None,
142 repo=None, sa_session=None, commit=False):
159 repo=None, sa_session=None, commit=False):
143 """
160 """
144 Audit logger for various actions made by users, typically this
161 Audit logger for various actions made by users, typically this
145 results in a call such::
162 results in a call such::
146
163
147 from rhodecode.lib import audit_logger
164 from rhodecode.lib import audit_logger
148
165
149 audit_logger.store(
166 audit_logger.store(
150 action='repo.edit', user=self._rhodecode_user)
167 action='repo.edit', user=self._rhodecode_user)
151 audit_logger.store(
168 audit_logger.store(
152 action='repo.delete', action_data={'data': repo_data},
169 action='repo.delete', action_data={'data': repo_data},
153 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
170 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
154
171
155 # repo action
172 # repo action
156 audit_logger.store(
173 audit_logger.store(
157 action='repo.delete',
174 action='repo.delete',
158 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
175 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
159 repo=audit_logger.RepoWrap(repo_name='some-repo'))
176 repo=audit_logger.RepoWrap(repo_name='some-repo'))
160
177
161 # repo action, when we know and have the repository object already
178 # repo action, when we know and have the repository object already
162 audit_logger.store(
179 audit_logger.store(
163 action='repo.delete',
180 action='repo.delete',
164 action_data={'source': audit_logger.SOURCE_WEB, },
181 action_data={'source': audit_logger.SOURCE_WEB, },
165 user=self._rhodecode_user,
182 user=self._rhodecode_user,
166 repo=repo_object)
183 repo=repo_object)
167
184
168 # alternative wrapper to the above
185 # alternative wrapper to the above
169 audit_logger.store_web(
186 audit_logger.store_web(
170 action='repo.delete',
187 action='repo.delete',
171 action_data={},
188 action_data={},
172 user=self._rhodecode_user,
189 user=self._rhodecode_user,
173 repo=repo_object)
190 repo=repo_object)
174
191
175 # without an user ?
192 # without an user ?
176 audit_logger.store(
193 audit_logger.store(
177 action='user.login.failure',
194 action='user.login.failure',
178 user=audit_logger.UserWrap(
195 user=audit_logger.UserWrap(
179 username=self.request.params.get('username'),
196 username=self.request.params.get('username'),
180 ip_addr=self.request.remote_addr))
197 ip_addr=self.request.remote_addr))
181
198
182 """
199 """
183 from rhodecode.lib.utils2 import safe_unicode
200 from rhodecode.lib.utils2 import safe_unicode
184 from rhodecode.lib.auth import AuthUser
201 from rhodecode.lib.auth import AuthUser
185
202
186 action_spec = ACTIONS.get(action, None)
203 action_spec = ACTIONS.get(action, None)
187 if action_spec is None:
204 if action_spec is None:
188 raise ValueError('Action `{}` is not supported'.format(action))
205 raise ValueError('Action `{}` is not supported'.format(action))
189
206
190 if not sa_session:
207 if not sa_session:
191 sa_session = meta.Session()
208 sa_session = meta.Session()
192
209
193 try:
210 try:
194 username = getattr(user, 'username', None)
211 username = getattr(user, 'username', None)
195 if not username:
212 if not username:
196 pass
213 pass
197
214
198 user_id = getattr(user, 'user_id', None)
215 user_id = getattr(user, 'user_id', None)
199 if not user_id:
216 if not user_id:
200 # maybe we have username ? Try to figure user_id from username
217 # maybe we have username ? Try to figure user_id from username
201 if username:
218 if username:
202 user_id = getattr(
219 user_id = getattr(
203 User.get_by_username(username), 'user_id', None)
220 User.get_by_username(username), 'user_id', None)
204
221
205 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
222 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
206 if not ip_addr:
223 if not ip_addr:
207 pass
224 pass
208
225
209 if not user_data:
226 if not user_data:
210 # try to get this from the auth user
227 # try to get this from the auth user
211 if isinstance(user, AuthUser):
228 if isinstance(user, AuthUser):
212 user_data = {
229 user_data = {
213 'username': user.username,
230 'username': user.username,
214 'email': user.email,
231 'email': user.email,
215 }
232 }
216
233
217 repository_name = getattr(repo, 'repo_name', None)
234 repository_name = getattr(repo, 'repo_name', None)
218 repository_id = getattr(repo, 'repo_id', None)
235 repository_id = getattr(repo, 'repo_id', None)
219 if not repository_id:
236 if not repository_id:
220 # maybe we have repo_name ? Try to figure repo_id from repo_name
237 # maybe we have repo_name ? Try to figure repo_id from repo_name
221 if repository_name:
238 if repository_name:
222 repository_id = getattr(
239 repository_id = getattr(
223 Repository.get_by_repo_name(repository_name), 'repo_id', None)
240 Repository.get_by_repo_name(repository_name), 'repo_id', None)
224
241
225 user_log = _store_log(
242 user_log = _store_log(
226 action_name=safe_unicode(action),
243 action_name=safe_unicode(action),
227 action_data=action_data or {},
244 action_data=action_data or {},
228 user_id=user_id,
245 user_id=user_id,
229 username=username,
246 username=username,
230 user_data=user_data or {},
247 user_data=user_data or {},
231 ip_address=safe_unicode(ip_addr),
248 ip_address=safe_unicode(ip_addr),
232 repository_id=repository_id,
249 repository_id=repository_id,
233 repository_name=repository_name
250 repository_name=repository_name
234 )
251 )
235 sa_session.add(user_log)
252 sa_session.add(user_log)
236 if commit:
253 if commit:
237 sa_session.commit()
254 sa_session.commit()
238
255
239 except Exception:
256 except Exception:
240 log.exception('AUDIT: failed to store audit log')
257 log.exception('AUDIT: failed to store audit log')
@@ -1,1044 +1,982 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 Utilities library for RhodeCode
22 Utilities library for RhodeCode
23 """
23 """
24
24
25 import datetime
25 import datetime
26 import decorator
26 import decorator
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30 import re
30 import re
31 import shutil
31 import shutil
32 import tempfile
32 import tempfile
33 import traceback
33 import traceback
34 import tarfile
34 import tarfile
35 import warnings
35 import warnings
36 import hashlib
36 import hashlib
37 from os.path import join as jn
37 from os.path import join as jn
38
38
39 import paste
39 import paste
40 import pkg_resources
40 import pkg_resources
41 from paste.script.command import Command, BadCommand
41 from paste.script.command import Command, BadCommand
42 from webhelpers.text import collapse, remove_formatting, strip_tags
42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 from mako import exceptions
43 from mako import exceptions
44 from pyramid.threadlocal import get_current_registry
44 from pyramid.threadlocal import get_current_registry
45 from pyramid.request import Request
45 from pyramid.request import Request
46
46
47 from rhodecode.lib.fakemod import create_module
47 from rhodecode.lib.fakemod import create_module
48 from rhodecode.lib.vcs.backends.base import Config
48 from rhodecode.lib.vcs.backends.base import Config
49 from rhodecode.lib.vcs.exceptions import VCSError
49 from rhodecode.lib.vcs.exceptions import VCSError
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 from rhodecode.lib.utils2 import (
51 from rhodecode.lib.utils2 import (
52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 from rhodecode.model import meta
53 from rhodecode.model import meta
54 from rhodecode.model.db import (
54 from rhodecode.model.db import (
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62
62
63 # String which contains characters that are not allowed in slug names for
63 # String which contains characters that are not allowed in slug names for
64 # repositories or repository groups. It is properly escaped to use it in
64 # repositories or repository groups. It is properly escaped to use it in
65 # regular expressions.
65 # regular expressions.
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67
67
68 # Regex that matches forbidden characters in repo/group slugs.
68 # Regex that matches forbidden characters in repo/group slugs.
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70
70
71 # Regex that matches allowed characters in repo/group slugs.
71 # Regex that matches allowed characters in repo/group slugs.
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73
73
74 # Regex that matches whole repo/group slugs.
74 # Regex that matches whole repo/group slugs.
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76
76
77 _license_cache = None
77 _license_cache = None
78
78
79
79
80 def repo_name_slug(value):
80 def repo_name_slug(value):
81 """
81 """
82 Return slug of name of repository
82 Return slug of name of repository
83 This function is called on each creation/modification
83 This function is called on each creation/modification
84 of repository to prevent bad names in repo
84 of repository to prevent bad names in repo
85 """
85 """
86 replacement_char = '-'
86 replacement_char = '-'
87
87
88 slug = remove_formatting(value)
88 slug = remove_formatting(value)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 slug = re.sub('[\s]+', '-', slug)
90 slug = re.sub('[\s]+', '-', slug)
91 slug = collapse(slug, replacement_char)
91 slug = collapse(slug, replacement_char)
92 return slug
92 return slug
93
93
94
94
95 #==============================================================================
95 #==============================================================================
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 #==============================================================================
97 #==============================================================================
98 def get_repo_slug(request):
98 def get_repo_slug(request):
99 if isinstance(request, Request) and getattr(request, 'db_repo', None):
99 if isinstance(request, Request) and getattr(request, 'db_repo', None):
100 # pyramid
100 # pyramid
101 _repo = request.db_repo.repo_name
101 _repo = request.db_repo.repo_name
102 else:
102 else:
103 # TODO(marcink): remove after pylons migration...
103 # TODO(marcink): remove after pylons migration...
104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105
105
106 if _repo:
106 if _repo:
107 _repo = _repo.rstrip('/')
107 _repo = _repo.rstrip('/')
108 return _repo
108 return _repo
109
109
110
110
111 def get_repo_group_slug(request):
111 def get_repo_group_slug(request):
112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
113 # pyramid
113 # pyramid
114 _group = request.matchdict.get('repo_group_name')
114 _group = request.matchdict.get('repo_group_name')
115 else:
115 else:
116 _group = request.environ['pylons.routes_dict'].get('group_name')
116 _group = request.environ['pylons.routes_dict'].get('group_name')
117
117
118 if _group:
118 if _group:
119 _group = _group.rstrip('/')
119 _group = _group.rstrip('/')
120 return _group
120 return _group
121
121
122
122
123 def get_user_group_slug(request):
123 def get_user_group_slug(request):
124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
125 # pyramid
125 # pyramid
126 _group = request.matchdict.get('user_group_id')
126 _group = request.matchdict.get('user_group_id')
127 else:
127 else:
128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
129
129
130 try:
130 try:
131 _group = UserGroup.get(_group)
131 _group = UserGroup.get(_group)
132 if _group:
132 if _group:
133 _group = _group.users_group_name
133 _group = _group.users_group_name
134 except Exception:
134 except Exception:
135 log.debug(traceback.format_exc())
135 log.debug(traceback.format_exc())
136 # catch all failures here
136 # catch all failures here
137 pass
137 pass
138
138
139 return _group
139 return _group
140
140
141
141
142 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
143 """
144 Action logger for various actions made by users
145
146 :param user: user that made this action, can be a unique username string or
147 object containing user_id attribute
148 :param action: action to log, should be on of predefined unique actions for
149 easy translations
150 :param repo: string name of repository or object containing repo_id,
151 that action was made on
152 :param ipaddr: optional ip address from what the action was made
153 :param sa: optional sqlalchemy session
154
155 """
156
157 if not sa:
158 sa = meta.Session()
159 # if we don't get explicit IP address try to get one from registered user
160 # in tmpl context var
161 if not ipaddr:
162 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
163
164 try:
165 if getattr(user, 'user_id', None):
166 user_obj = User.get(user.user_id)
167 elif isinstance(user, basestring):
168 user_obj = User.get_by_username(user)
169 else:
170 raise Exception('You have to provide a user object or a username')
171
172 if getattr(repo, 'repo_id', None):
173 repo_obj = Repository.get(repo.repo_id)
174 repo_name = repo_obj.repo_name
175 elif isinstance(repo, basestring):
176 repo_name = repo.lstrip('/')
177 repo_obj = Repository.get_by_repo_name(repo_name)
178 else:
179 repo_obj = None
180 repo_name = ''
181
182 user_log = UserLog()
183 user_log.user_id = user_obj.user_id
184 user_log.username = user_obj.username
185 action = safe_unicode(action)
186 user_log.action = action[:1200000]
187
188 user_log.repository = repo_obj
189 user_log.repository_name = repo_name
190
191 user_log.action_date = datetime.datetime.now()
192 user_log.user_ip = ipaddr
193 sa.add(user_log)
194
195 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
196 action, safe_unicode(repo), user_obj, ipaddr)
197 if commit:
198 sa.commit()
199 except Exception:
200 log.error(traceback.format_exc())
201 raise
202
203
204 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
142 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
205 """
143 """
206 Scans given path for repos and return (name,(type,path)) tuple
144 Scans given path for repos and return (name,(type,path)) tuple
207
145
208 :param path: path to scan for repositories
146 :param path: path to scan for repositories
209 :param recursive: recursive search and return names with subdirs in front
147 :param recursive: recursive search and return names with subdirs in front
210 """
148 """
211
149
212 # remove ending slash for better results
150 # remove ending slash for better results
213 path = path.rstrip(os.sep)
151 path = path.rstrip(os.sep)
214 log.debug('now scanning in %s location recursive:%s...', path, recursive)
152 log.debug('now scanning in %s location recursive:%s...', path, recursive)
215
153
216 def _get_repos(p):
154 def _get_repos(p):
217 dirpaths = _get_dirpaths(p)
155 dirpaths = _get_dirpaths(p)
218 if not _is_dir_writable(p):
156 if not _is_dir_writable(p):
219 log.warning('repo path without write access: %s', p)
157 log.warning('repo path without write access: %s', p)
220
158
221 for dirpath in dirpaths:
159 for dirpath in dirpaths:
222 if os.path.isfile(os.path.join(p, dirpath)):
160 if os.path.isfile(os.path.join(p, dirpath)):
223 continue
161 continue
224 cur_path = os.path.join(p, dirpath)
162 cur_path = os.path.join(p, dirpath)
225
163
226 # skip removed repos
164 # skip removed repos
227 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
165 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
228 continue
166 continue
229
167
230 #skip .<somethin> dirs
168 #skip .<somethin> dirs
231 if dirpath.startswith('.'):
169 if dirpath.startswith('.'):
232 continue
170 continue
233
171
234 try:
172 try:
235 scm_info = get_scm(cur_path)
173 scm_info = get_scm(cur_path)
236 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
174 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
237 except VCSError:
175 except VCSError:
238 if not recursive:
176 if not recursive:
239 continue
177 continue
240 #check if this dir containts other repos for recursive scan
178 #check if this dir containts other repos for recursive scan
241 rec_path = os.path.join(p, dirpath)
179 rec_path = os.path.join(p, dirpath)
242 if os.path.isdir(rec_path):
180 if os.path.isdir(rec_path):
243 for inner_scm in _get_repos(rec_path):
181 for inner_scm in _get_repos(rec_path):
244 yield inner_scm
182 yield inner_scm
245
183
246 return _get_repos(path)
184 return _get_repos(path)
247
185
248
186
249 def _get_dirpaths(p):
187 def _get_dirpaths(p):
250 try:
188 try:
251 # OS-independable way of checking if we have at least read-only
189 # OS-independable way of checking if we have at least read-only
252 # access or not.
190 # access or not.
253 dirpaths = os.listdir(p)
191 dirpaths = os.listdir(p)
254 except OSError:
192 except OSError:
255 log.warning('ignoring repo path without read access: %s', p)
193 log.warning('ignoring repo path without read access: %s', p)
256 return []
194 return []
257
195
258 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
196 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
259 # decode paths and suddenly returns unicode objects itself. The items it
197 # decode paths and suddenly returns unicode objects itself. The items it
260 # cannot decode are returned as strings and cause issues.
198 # cannot decode are returned as strings and cause issues.
261 #
199 #
262 # Those paths are ignored here until a solid solution for path handling has
200 # Those paths are ignored here until a solid solution for path handling has
263 # been built.
201 # been built.
264 expected_type = type(p)
202 expected_type = type(p)
265
203
266 def _has_correct_type(item):
204 def _has_correct_type(item):
267 if type(item) is not expected_type:
205 if type(item) is not expected_type:
268 log.error(
206 log.error(
269 u"Ignoring path %s since it cannot be decoded into unicode.",
207 u"Ignoring path %s since it cannot be decoded into unicode.",
270 # Using "repr" to make sure that we see the byte value in case
208 # Using "repr" to make sure that we see the byte value in case
271 # of support.
209 # of support.
272 repr(item))
210 repr(item))
273 return False
211 return False
274 return True
212 return True
275
213
276 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
214 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
277
215
278 return dirpaths
216 return dirpaths
279
217
280
218
281 def _is_dir_writable(path):
219 def _is_dir_writable(path):
282 """
220 """
283 Probe if `path` is writable.
221 Probe if `path` is writable.
284
222
285 Due to trouble on Cygwin / Windows, this is actually probing if it is
223 Due to trouble on Cygwin / Windows, this is actually probing if it is
286 possible to create a file inside of `path`, stat does not produce reliable
224 possible to create a file inside of `path`, stat does not produce reliable
287 results in this case.
225 results in this case.
288 """
226 """
289 try:
227 try:
290 with tempfile.TemporaryFile(dir=path):
228 with tempfile.TemporaryFile(dir=path):
291 pass
229 pass
292 except OSError:
230 except OSError:
293 return False
231 return False
294 return True
232 return True
295
233
296
234
297 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
235 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
298 """
236 """
299 Returns True if given path is a valid repository False otherwise.
237 Returns True if given path is a valid repository False otherwise.
300 If expect_scm param is given also, compare if given scm is the same
238 If expect_scm param is given also, compare if given scm is the same
301 as expected from scm parameter. If explicit_scm is given don't try to
239 as expected from scm parameter. If explicit_scm is given don't try to
302 detect the scm, just use the given one to check if repo is valid
240 detect the scm, just use the given one to check if repo is valid
303
241
304 :param repo_name:
242 :param repo_name:
305 :param base_path:
243 :param base_path:
306 :param expect_scm:
244 :param expect_scm:
307 :param explicit_scm:
245 :param explicit_scm:
308
246
309 :return True: if given path is a valid repository
247 :return True: if given path is a valid repository
310 """
248 """
311 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
249 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
312 log.debug('Checking if `%s` is a valid path for repository. '
250 log.debug('Checking if `%s` is a valid path for repository. '
313 'Explicit type: %s', repo_name, explicit_scm)
251 'Explicit type: %s', repo_name, explicit_scm)
314
252
315 try:
253 try:
316 if explicit_scm:
254 if explicit_scm:
317 detected_scms = [get_scm_backend(explicit_scm)]
255 detected_scms = [get_scm_backend(explicit_scm)]
318 else:
256 else:
319 detected_scms = get_scm(full_path)
257 detected_scms = get_scm(full_path)
320
258
321 if expect_scm:
259 if expect_scm:
322 return detected_scms[0] == expect_scm
260 return detected_scms[0] == expect_scm
323 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
261 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
324 return True
262 return True
325 except VCSError:
263 except VCSError:
326 log.debug('path: %s is not a valid repo !', full_path)
264 log.debug('path: %s is not a valid repo !', full_path)
327 return False
265 return False
328
266
329
267
330 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
268 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
331 """
269 """
332 Returns True if given path is a repository group, False otherwise
270 Returns True if given path is a repository group, False otherwise
333
271
334 :param repo_name:
272 :param repo_name:
335 :param base_path:
273 :param base_path:
336 """
274 """
337 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
275 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
338 log.debug('Checking if `%s` is a valid path for repository group',
276 log.debug('Checking if `%s` is a valid path for repository group',
339 repo_group_name)
277 repo_group_name)
340
278
341 # check if it's not a repo
279 # check if it's not a repo
342 if is_valid_repo(repo_group_name, base_path):
280 if is_valid_repo(repo_group_name, base_path):
343 log.debug('Repo called %s exist, it is not a valid '
281 log.debug('Repo called %s exist, it is not a valid '
344 'repo group' % repo_group_name)
282 'repo group' % repo_group_name)
345 return False
283 return False
346
284
347 try:
285 try:
348 # we need to check bare git repos at higher level
286 # we need to check bare git repos at higher level
349 # since we might match branches/hooks/info/objects or possible
287 # since we might match branches/hooks/info/objects or possible
350 # other things inside bare git repo
288 # other things inside bare git repo
351 scm_ = get_scm(os.path.dirname(full_path))
289 scm_ = get_scm(os.path.dirname(full_path))
352 log.debug('path: %s is a vcs object:%s, not valid '
290 log.debug('path: %s is a vcs object:%s, not valid '
353 'repo group' % (full_path, scm_))
291 'repo group' % (full_path, scm_))
354 return False
292 return False
355 except VCSError:
293 except VCSError:
356 pass
294 pass
357
295
358 # check if it's a valid path
296 # check if it's a valid path
359 if skip_path_check or os.path.isdir(full_path):
297 if skip_path_check or os.path.isdir(full_path):
360 log.debug('path: %s is a valid repo group !', full_path)
298 log.debug('path: %s is a valid repo group !', full_path)
361 return True
299 return True
362
300
363 log.debug('path: %s is not a valid repo group !', full_path)
301 log.debug('path: %s is not a valid repo group !', full_path)
364 return False
302 return False
365
303
366
304
367 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
305 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
368 while True:
306 while True:
369 ok = raw_input(prompt)
307 ok = raw_input(prompt)
370 if ok.lower() in ('y', 'ye', 'yes'):
308 if ok.lower() in ('y', 'ye', 'yes'):
371 return True
309 return True
372 if ok.lower() in ('n', 'no', 'nop', 'nope'):
310 if ok.lower() in ('n', 'no', 'nop', 'nope'):
373 return False
311 return False
374 retries = retries - 1
312 retries = retries - 1
375 if retries < 0:
313 if retries < 0:
376 raise IOError
314 raise IOError
377 print(complaint)
315 print(complaint)
378
316
379 # propagated from mercurial documentation
317 # propagated from mercurial documentation
380 ui_sections = [
318 ui_sections = [
381 'alias', 'auth',
319 'alias', 'auth',
382 'decode/encode', 'defaults',
320 'decode/encode', 'defaults',
383 'diff', 'email',
321 'diff', 'email',
384 'extensions', 'format',
322 'extensions', 'format',
385 'merge-patterns', 'merge-tools',
323 'merge-patterns', 'merge-tools',
386 'hooks', 'http_proxy',
324 'hooks', 'http_proxy',
387 'smtp', 'patch',
325 'smtp', 'patch',
388 'paths', 'profiling',
326 'paths', 'profiling',
389 'server', 'trusted',
327 'server', 'trusted',
390 'ui', 'web', ]
328 'ui', 'web', ]
391
329
392
330
393 def config_data_from_db(clear_session=True, repo=None):
331 def config_data_from_db(clear_session=True, repo=None):
394 """
332 """
395 Read the configuration data from the database and return configuration
333 Read the configuration data from the database and return configuration
396 tuples.
334 tuples.
397 """
335 """
398 from rhodecode.model.settings import VcsSettingsModel
336 from rhodecode.model.settings import VcsSettingsModel
399
337
400 config = []
338 config = []
401
339
402 sa = meta.Session()
340 sa = meta.Session()
403 settings_model = VcsSettingsModel(repo=repo, sa=sa)
341 settings_model = VcsSettingsModel(repo=repo, sa=sa)
404
342
405 ui_settings = settings_model.get_ui_settings()
343 ui_settings = settings_model.get_ui_settings()
406
344
407 for setting in ui_settings:
345 for setting in ui_settings:
408 if setting.active:
346 if setting.active:
409 log.debug(
347 log.debug(
410 'settings ui from db: [%s] %s=%s',
348 'settings ui from db: [%s] %s=%s',
411 setting.section, setting.key, setting.value)
349 setting.section, setting.key, setting.value)
412 config.append((
350 config.append((
413 safe_str(setting.section), safe_str(setting.key),
351 safe_str(setting.section), safe_str(setting.key),
414 safe_str(setting.value)))
352 safe_str(setting.value)))
415 if setting.key == 'push_ssl':
353 if setting.key == 'push_ssl':
416 # force set push_ssl requirement to False, rhodecode
354 # force set push_ssl requirement to False, rhodecode
417 # handles that
355 # handles that
418 config.append((
356 config.append((
419 safe_str(setting.section), safe_str(setting.key), False))
357 safe_str(setting.section), safe_str(setting.key), False))
420 if clear_session:
358 if clear_session:
421 meta.Session.remove()
359 meta.Session.remove()
422
360
423 # TODO: mikhail: probably it makes no sense to re-read hooks information.
361 # TODO: mikhail: probably it makes no sense to re-read hooks information.
424 # It's already there and activated/deactivated
362 # It's already there and activated/deactivated
425 skip_entries = []
363 skip_entries = []
426 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
364 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
427 if 'pull' not in enabled_hook_classes:
365 if 'pull' not in enabled_hook_classes:
428 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
366 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
429 if 'push' not in enabled_hook_classes:
367 if 'push' not in enabled_hook_classes:
430 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
368 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
431 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
369 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
432 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
370 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
433
371
434 config = [entry for entry in config if entry[:2] not in skip_entries]
372 config = [entry for entry in config if entry[:2] not in skip_entries]
435
373
436 return config
374 return config
437
375
438
376
439 def make_db_config(clear_session=True, repo=None):
377 def make_db_config(clear_session=True, repo=None):
440 """
378 """
441 Create a :class:`Config` instance based on the values in the database.
379 Create a :class:`Config` instance based on the values in the database.
442 """
380 """
443 config = Config()
381 config = Config()
444 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
382 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
445 for section, option, value in config_data:
383 for section, option, value in config_data:
446 config.set(section, option, value)
384 config.set(section, option, value)
447 return config
385 return config
448
386
449
387
450 def get_enabled_hook_classes(ui_settings):
388 def get_enabled_hook_classes(ui_settings):
451 """
389 """
452 Return the enabled hook classes.
390 Return the enabled hook classes.
453
391
454 :param ui_settings: List of ui_settings as returned
392 :param ui_settings: List of ui_settings as returned
455 by :meth:`VcsSettingsModel.get_ui_settings`
393 by :meth:`VcsSettingsModel.get_ui_settings`
456
394
457 :return: a list with the enabled hook classes. The order is not guaranteed.
395 :return: a list with the enabled hook classes. The order is not guaranteed.
458 :rtype: list
396 :rtype: list
459 """
397 """
460 enabled_hooks = []
398 enabled_hooks = []
461 active_hook_keys = [
399 active_hook_keys = [
462 key for section, key, value, active in ui_settings
400 key for section, key, value, active in ui_settings
463 if section == 'hooks' and active]
401 if section == 'hooks' and active]
464
402
465 hook_names = {
403 hook_names = {
466 RhodeCodeUi.HOOK_PUSH: 'push',
404 RhodeCodeUi.HOOK_PUSH: 'push',
467 RhodeCodeUi.HOOK_PULL: 'pull',
405 RhodeCodeUi.HOOK_PULL: 'pull',
468 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
406 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
469 }
407 }
470
408
471 for key in active_hook_keys:
409 for key in active_hook_keys:
472 hook = hook_names.get(key)
410 hook = hook_names.get(key)
473 if hook:
411 if hook:
474 enabled_hooks.append(hook)
412 enabled_hooks.append(hook)
475
413
476 return enabled_hooks
414 return enabled_hooks
477
415
478
416
479 def set_rhodecode_config(config):
417 def set_rhodecode_config(config):
480 """
418 """
481 Updates pylons config with new settings from database
419 Updates pylons config with new settings from database
482
420
483 :param config:
421 :param config:
484 """
422 """
485 from rhodecode.model.settings import SettingsModel
423 from rhodecode.model.settings import SettingsModel
486 app_settings = SettingsModel().get_all_settings()
424 app_settings = SettingsModel().get_all_settings()
487
425
488 for k, v in app_settings.items():
426 for k, v in app_settings.items():
489 config[k] = v
427 config[k] = v
490
428
491
429
492 def get_rhodecode_realm():
430 def get_rhodecode_realm():
493 """
431 """
494 Return the rhodecode realm from database.
432 Return the rhodecode realm from database.
495 """
433 """
496 from rhodecode.model.settings import SettingsModel
434 from rhodecode.model.settings import SettingsModel
497 realm = SettingsModel().get_setting_by_name('realm')
435 realm = SettingsModel().get_setting_by_name('realm')
498 return safe_str(realm.app_settings_value)
436 return safe_str(realm.app_settings_value)
499
437
500
438
501 def get_rhodecode_base_path():
439 def get_rhodecode_base_path():
502 """
440 """
503 Returns the base path. The base path is the filesystem path which points
441 Returns the base path. The base path is the filesystem path which points
504 to the repository store.
442 to the repository store.
505 """
443 """
506 from rhodecode.model.settings import SettingsModel
444 from rhodecode.model.settings import SettingsModel
507 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
445 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
508 return safe_str(paths_ui.ui_value)
446 return safe_str(paths_ui.ui_value)
509
447
510
448
511 def map_groups(path):
449 def map_groups(path):
512 """
450 """
513 Given a full path to a repository, create all nested groups that this
451 Given a full path to a repository, create all nested groups that this
514 repo is inside. This function creates parent-child relationships between
452 repo is inside. This function creates parent-child relationships between
515 groups and creates default perms for all new groups.
453 groups and creates default perms for all new groups.
516
454
517 :param paths: full path to repository
455 :param paths: full path to repository
518 """
456 """
519 from rhodecode.model.repo_group import RepoGroupModel
457 from rhodecode.model.repo_group import RepoGroupModel
520 sa = meta.Session()
458 sa = meta.Session()
521 groups = path.split(Repository.NAME_SEP)
459 groups = path.split(Repository.NAME_SEP)
522 parent = None
460 parent = None
523 group = None
461 group = None
524
462
525 # last element is repo in nested groups structure
463 # last element is repo in nested groups structure
526 groups = groups[:-1]
464 groups = groups[:-1]
527 rgm = RepoGroupModel(sa)
465 rgm = RepoGroupModel(sa)
528 owner = User.get_first_super_admin()
466 owner = User.get_first_super_admin()
529 for lvl, group_name in enumerate(groups):
467 for lvl, group_name in enumerate(groups):
530 group_name = '/'.join(groups[:lvl] + [group_name])
468 group_name = '/'.join(groups[:lvl] + [group_name])
531 group = RepoGroup.get_by_group_name(group_name)
469 group = RepoGroup.get_by_group_name(group_name)
532 desc = '%s group' % group_name
470 desc = '%s group' % group_name
533
471
534 # skip folders that are now removed repos
472 # skip folders that are now removed repos
535 if REMOVED_REPO_PAT.match(group_name):
473 if REMOVED_REPO_PAT.match(group_name):
536 break
474 break
537
475
538 if group is None:
476 if group is None:
539 log.debug('creating group level: %s group_name: %s',
477 log.debug('creating group level: %s group_name: %s',
540 lvl, group_name)
478 lvl, group_name)
541 group = RepoGroup(group_name, parent)
479 group = RepoGroup(group_name, parent)
542 group.group_description = desc
480 group.group_description = desc
543 group.user = owner
481 group.user = owner
544 sa.add(group)
482 sa.add(group)
545 perm_obj = rgm._create_default_perms(group)
483 perm_obj = rgm._create_default_perms(group)
546 sa.add(perm_obj)
484 sa.add(perm_obj)
547 sa.flush()
485 sa.flush()
548
486
549 parent = group
487 parent = group
550 return group
488 return group
551
489
552
490
553 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
491 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
554 """
492 """
555 maps all repos given in initial_repo_list, non existing repositories
493 maps all repos given in initial_repo_list, non existing repositories
556 are created, if remove_obsolete is True it also checks for db entries
494 are created, if remove_obsolete is True it also checks for db entries
557 that are not in initial_repo_list and removes them.
495 that are not in initial_repo_list and removes them.
558
496
559 :param initial_repo_list: list of repositories found by scanning methods
497 :param initial_repo_list: list of repositories found by scanning methods
560 :param remove_obsolete: check for obsolete entries in database
498 :param remove_obsolete: check for obsolete entries in database
561 """
499 """
562 from rhodecode.model.repo import RepoModel
500 from rhodecode.model.repo import RepoModel
563 from rhodecode.model.scm import ScmModel
501 from rhodecode.model.scm import ScmModel
564 from rhodecode.model.repo_group import RepoGroupModel
502 from rhodecode.model.repo_group import RepoGroupModel
565 from rhodecode.model.settings import SettingsModel
503 from rhodecode.model.settings import SettingsModel
566
504
567 sa = meta.Session()
505 sa = meta.Session()
568 repo_model = RepoModel()
506 repo_model = RepoModel()
569 user = User.get_first_super_admin()
507 user = User.get_first_super_admin()
570 added = []
508 added = []
571
509
572 # creation defaults
510 # creation defaults
573 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
511 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
574 enable_statistics = defs.get('repo_enable_statistics')
512 enable_statistics = defs.get('repo_enable_statistics')
575 enable_locking = defs.get('repo_enable_locking')
513 enable_locking = defs.get('repo_enable_locking')
576 enable_downloads = defs.get('repo_enable_downloads')
514 enable_downloads = defs.get('repo_enable_downloads')
577 private = defs.get('repo_private')
515 private = defs.get('repo_private')
578
516
579 for name, repo in initial_repo_list.items():
517 for name, repo in initial_repo_list.items():
580 group = map_groups(name)
518 group = map_groups(name)
581 unicode_name = safe_unicode(name)
519 unicode_name = safe_unicode(name)
582 db_repo = repo_model.get_by_repo_name(unicode_name)
520 db_repo = repo_model.get_by_repo_name(unicode_name)
583 # found repo that is on filesystem not in RhodeCode database
521 # found repo that is on filesystem not in RhodeCode database
584 if not db_repo:
522 if not db_repo:
585 log.info('repository %s not found, creating now', name)
523 log.info('repository %s not found, creating now', name)
586 added.append(name)
524 added.append(name)
587 desc = (repo.description
525 desc = (repo.description
588 if repo.description != 'unknown'
526 if repo.description != 'unknown'
589 else '%s repository' % name)
527 else '%s repository' % name)
590
528
591 db_repo = repo_model._create_repo(
529 db_repo = repo_model._create_repo(
592 repo_name=name,
530 repo_name=name,
593 repo_type=repo.alias,
531 repo_type=repo.alias,
594 description=desc,
532 description=desc,
595 repo_group=getattr(group, 'group_id', None),
533 repo_group=getattr(group, 'group_id', None),
596 owner=user,
534 owner=user,
597 enable_locking=enable_locking,
535 enable_locking=enable_locking,
598 enable_downloads=enable_downloads,
536 enable_downloads=enable_downloads,
599 enable_statistics=enable_statistics,
537 enable_statistics=enable_statistics,
600 private=private,
538 private=private,
601 state=Repository.STATE_CREATED
539 state=Repository.STATE_CREATED
602 )
540 )
603 sa.commit()
541 sa.commit()
604 # we added that repo just now, and make sure we updated server info
542 # we added that repo just now, and make sure we updated server info
605 if db_repo.repo_type == 'git':
543 if db_repo.repo_type == 'git':
606 git_repo = db_repo.scm_instance()
544 git_repo = db_repo.scm_instance()
607 # update repository server-info
545 # update repository server-info
608 log.debug('Running update server info')
546 log.debug('Running update server info')
609 git_repo._update_server_info()
547 git_repo._update_server_info()
610
548
611 db_repo.update_commit_cache()
549 db_repo.update_commit_cache()
612
550
613 config = db_repo._config
551 config = db_repo._config
614 config.set('extensions', 'largefiles', '')
552 config.set('extensions', 'largefiles', '')
615 ScmModel().install_hooks(
553 ScmModel().install_hooks(
616 db_repo.scm_instance(config=config),
554 db_repo.scm_instance(config=config),
617 repo_type=db_repo.repo_type)
555 repo_type=db_repo.repo_type)
618
556
619 removed = []
557 removed = []
620 if remove_obsolete:
558 if remove_obsolete:
621 # remove from database those repositories that are not in the filesystem
559 # remove from database those repositories that are not in the filesystem
622 for repo in sa.query(Repository).all():
560 for repo in sa.query(Repository).all():
623 if repo.repo_name not in initial_repo_list.keys():
561 if repo.repo_name not in initial_repo_list.keys():
624 log.debug("Removing non-existing repository found in db `%s`",
562 log.debug("Removing non-existing repository found in db `%s`",
625 repo.repo_name)
563 repo.repo_name)
626 try:
564 try:
627 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
565 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
628 sa.commit()
566 sa.commit()
629 removed.append(repo.repo_name)
567 removed.append(repo.repo_name)
630 except Exception:
568 except Exception:
631 # don't hold further removals on error
569 # don't hold further removals on error
632 log.error(traceback.format_exc())
570 log.error(traceback.format_exc())
633 sa.rollback()
571 sa.rollback()
634
572
635 def splitter(full_repo_name):
573 def splitter(full_repo_name):
636 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
574 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
637 gr_name = None
575 gr_name = None
638 if len(_parts) == 2:
576 if len(_parts) == 2:
639 gr_name = _parts[0]
577 gr_name = _parts[0]
640 return gr_name
578 return gr_name
641
579
642 initial_repo_group_list = [splitter(x) for x in
580 initial_repo_group_list = [splitter(x) for x in
643 initial_repo_list.keys() if splitter(x)]
581 initial_repo_list.keys() if splitter(x)]
644
582
645 # remove from database those repository groups that are not in the
583 # remove from database those repository groups that are not in the
646 # filesystem due to parent child relationships we need to delete them
584 # filesystem due to parent child relationships we need to delete them
647 # in a specific order of most nested first
585 # in a specific order of most nested first
648 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
586 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
649 nested_sort = lambda gr: len(gr.split('/'))
587 nested_sort = lambda gr: len(gr.split('/'))
650 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
588 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
651 if group_name not in initial_repo_group_list:
589 if group_name not in initial_repo_group_list:
652 repo_group = RepoGroup.get_by_group_name(group_name)
590 repo_group = RepoGroup.get_by_group_name(group_name)
653 if (repo_group.children.all() or
591 if (repo_group.children.all() or
654 not RepoGroupModel().check_exist_filesystem(
592 not RepoGroupModel().check_exist_filesystem(
655 group_name=group_name, exc_on_failure=False)):
593 group_name=group_name, exc_on_failure=False)):
656 continue
594 continue
657
595
658 log.info(
596 log.info(
659 'Removing non-existing repository group found in db `%s`',
597 'Removing non-existing repository group found in db `%s`',
660 group_name)
598 group_name)
661 try:
599 try:
662 RepoGroupModel(sa).delete(group_name, fs_remove=False)
600 RepoGroupModel(sa).delete(group_name, fs_remove=False)
663 sa.commit()
601 sa.commit()
664 removed.append(group_name)
602 removed.append(group_name)
665 except Exception:
603 except Exception:
666 # don't hold further removals on error
604 # don't hold further removals on error
667 log.exception(
605 log.exception(
668 'Unable to remove repository group `%s`',
606 'Unable to remove repository group `%s`',
669 group_name)
607 group_name)
670 sa.rollback()
608 sa.rollback()
671 raise
609 raise
672
610
673 return added, removed
611 return added, removed
674
612
675
613
676 def get_default_cache_settings(settings):
614 def get_default_cache_settings(settings):
677 cache_settings = {}
615 cache_settings = {}
678 for key in settings.keys():
616 for key in settings.keys():
679 for prefix in ['beaker.cache.', 'cache.']:
617 for prefix in ['beaker.cache.', 'cache.']:
680 if key.startswith(prefix):
618 if key.startswith(prefix):
681 name = key.split(prefix)[1].strip()
619 name = key.split(prefix)[1].strip()
682 cache_settings[name] = settings[key].strip()
620 cache_settings[name] = settings[key].strip()
683 return cache_settings
621 return cache_settings
684
622
685
623
686 # set cache regions for beaker so celery can utilise it
624 # set cache regions for beaker so celery can utilise it
687 def add_cache(settings):
625 def add_cache(settings):
688 from rhodecode.lib import caches
626 from rhodecode.lib import caches
689 cache_settings = {'regions': None}
627 cache_settings = {'regions': None}
690 # main cache settings used as default ...
628 # main cache settings used as default ...
691 cache_settings.update(get_default_cache_settings(settings))
629 cache_settings.update(get_default_cache_settings(settings))
692
630
693 if cache_settings['regions']:
631 if cache_settings['regions']:
694 for region in cache_settings['regions'].split(','):
632 for region in cache_settings['regions'].split(','):
695 region = region.strip()
633 region = region.strip()
696 region_settings = {}
634 region_settings = {}
697 for key, value in cache_settings.items():
635 for key, value in cache_settings.items():
698 if key.startswith(region):
636 if key.startswith(region):
699 region_settings[key.split('.')[1]] = value
637 region_settings[key.split('.')[1]] = value
700
638
701 caches.configure_cache_region(
639 caches.configure_cache_region(
702 region, region_settings, cache_settings)
640 region, region_settings, cache_settings)
703
641
704
642
705 def load_rcextensions(root_path):
643 def load_rcextensions(root_path):
706 import rhodecode
644 import rhodecode
707 from rhodecode.config import conf
645 from rhodecode.config import conf
708
646
709 path = os.path.join(root_path, 'rcextensions', '__init__.py')
647 path = os.path.join(root_path, 'rcextensions', '__init__.py')
710 if os.path.isfile(path):
648 if os.path.isfile(path):
711 rcext = create_module('rc', path)
649 rcext = create_module('rc', path)
712 EXT = rhodecode.EXTENSIONS = rcext
650 EXT = rhodecode.EXTENSIONS = rcext
713 log.debug('Found rcextensions now loading %s...', rcext)
651 log.debug('Found rcextensions now loading %s...', rcext)
714
652
715 # Additional mappings that are not present in the pygments lexers
653 # Additional mappings that are not present in the pygments lexers
716 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
654 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
717
655
718 # auto check if the module is not missing any data, set to default if is
656 # auto check if the module is not missing any data, set to default if is
719 # this will help autoupdate new feature of rcext module
657 # this will help autoupdate new feature of rcext module
720 #from rhodecode.config import rcextensions
658 #from rhodecode.config import rcextensions
721 #for k in dir(rcextensions):
659 #for k in dir(rcextensions):
722 # if not k.startswith('_') and not hasattr(EXT, k):
660 # if not k.startswith('_') and not hasattr(EXT, k):
723 # setattr(EXT, k, getattr(rcextensions, k))
661 # setattr(EXT, k, getattr(rcextensions, k))
724
662
725
663
726 def get_custom_lexer(extension):
664 def get_custom_lexer(extension):
727 """
665 """
728 returns a custom lexer if it is defined in rcextensions module, or None
666 returns a custom lexer if it is defined in rcextensions module, or None
729 if there's no custom lexer defined
667 if there's no custom lexer defined
730 """
668 """
731 import rhodecode
669 import rhodecode
732 from pygments import lexers
670 from pygments import lexers
733
671
734 # custom override made by RhodeCode
672 # custom override made by RhodeCode
735 if extension in ['mako']:
673 if extension in ['mako']:
736 return lexers.get_lexer_by_name('html+mako')
674 return lexers.get_lexer_by_name('html+mako')
737
675
738 # check if we didn't define this extension as other lexer
676 # check if we didn't define this extension as other lexer
739 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
677 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
740 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
678 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
741 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
679 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
742 return lexers.get_lexer_by_name(_lexer_name)
680 return lexers.get_lexer_by_name(_lexer_name)
743
681
744
682
745 #==============================================================================
683 #==============================================================================
746 # TEST FUNCTIONS AND CREATORS
684 # TEST FUNCTIONS AND CREATORS
747 #==============================================================================
685 #==============================================================================
748 def create_test_index(repo_location, config):
686 def create_test_index(repo_location, config):
749 """
687 """
750 Makes default test index.
688 Makes default test index.
751 """
689 """
752 import rc_testdata
690 import rc_testdata
753
691
754 rc_testdata.extract_search_index(
692 rc_testdata.extract_search_index(
755 'vcs_search_index', os.path.dirname(config['search.location']))
693 'vcs_search_index', os.path.dirname(config['search.location']))
756
694
757
695
758 def create_test_directory(test_path):
696 def create_test_directory(test_path):
759 """
697 """
760 Create test directory if it doesn't exist.
698 Create test directory if it doesn't exist.
761 """
699 """
762 if not os.path.isdir(test_path):
700 if not os.path.isdir(test_path):
763 log.debug('Creating testdir %s', test_path)
701 log.debug('Creating testdir %s', test_path)
764 os.makedirs(test_path)
702 os.makedirs(test_path)
765
703
766
704
767 def create_test_database(test_path, config):
705 def create_test_database(test_path, config):
768 """
706 """
769 Makes a fresh database.
707 Makes a fresh database.
770 """
708 """
771 from rhodecode.lib.db_manage import DbManage
709 from rhodecode.lib.db_manage import DbManage
772
710
773 # PART ONE create db
711 # PART ONE create db
774 dbconf = config['sqlalchemy.db1.url']
712 dbconf = config['sqlalchemy.db1.url']
775 log.debug('making test db %s', dbconf)
713 log.debug('making test db %s', dbconf)
776
714
777 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
715 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
778 tests=True, cli_args={'force_ask': True})
716 tests=True, cli_args={'force_ask': True})
779 dbmanage.create_tables(override=True)
717 dbmanage.create_tables(override=True)
780 dbmanage.set_db_version()
718 dbmanage.set_db_version()
781 # for tests dynamically set new root paths based on generated content
719 # for tests dynamically set new root paths based on generated content
782 dbmanage.create_settings(dbmanage.config_prompt(test_path))
720 dbmanage.create_settings(dbmanage.config_prompt(test_path))
783 dbmanage.create_default_user()
721 dbmanage.create_default_user()
784 dbmanage.create_test_admin_and_users()
722 dbmanage.create_test_admin_and_users()
785 dbmanage.create_permissions()
723 dbmanage.create_permissions()
786 dbmanage.populate_default_permissions()
724 dbmanage.populate_default_permissions()
787 Session().commit()
725 Session().commit()
788
726
789
727
790 def create_test_repositories(test_path, config):
728 def create_test_repositories(test_path, config):
791 """
729 """
792 Creates test repositories in the temporary directory. Repositories are
730 Creates test repositories in the temporary directory. Repositories are
793 extracted from archives within the rc_testdata package.
731 extracted from archives within the rc_testdata package.
794 """
732 """
795 import rc_testdata
733 import rc_testdata
796 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
734 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
797
735
798 log.debug('making test vcs repositories')
736 log.debug('making test vcs repositories')
799
737
800 idx_path = config['search.location']
738 idx_path = config['search.location']
801 data_path = config['cache_dir']
739 data_path = config['cache_dir']
802
740
803 # clean index and data
741 # clean index and data
804 if idx_path and os.path.exists(idx_path):
742 if idx_path and os.path.exists(idx_path):
805 log.debug('remove %s', idx_path)
743 log.debug('remove %s', idx_path)
806 shutil.rmtree(idx_path)
744 shutil.rmtree(idx_path)
807
745
808 if data_path and os.path.exists(data_path):
746 if data_path and os.path.exists(data_path):
809 log.debug('remove %s', data_path)
747 log.debug('remove %s', data_path)
810 shutil.rmtree(data_path)
748 shutil.rmtree(data_path)
811
749
812 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
750 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
813 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
751 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
814
752
815 # Note: Subversion is in the process of being integrated with the system,
753 # Note: Subversion is in the process of being integrated with the system,
816 # until we have a properly packed version of the test svn repository, this
754 # until we have a properly packed version of the test svn repository, this
817 # tries to copy over the repo from a package "rc_testdata"
755 # tries to copy over the repo from a package "rc_testdata"
818 svn_repo_path = rc_testdata.get_svn_repo_archive()
756 svn_repo_path = rc_testdata.get_svn_repo_archive()
819 with tarfile.open(svn_repo_path) as tar:
757 with tarfile.open(svn_repo_path) as tar:
820 tar.extractall(jn(test_path, SVN_REPO))
758 tar.extractall(jn(test_path, SVN_REPO))
821
759
822
760
823 #==============================================================================
761 #==============================================================================
824 # PASTER COMMANDS
762 # PASTER COMMANDS
825 #==============================================================================
763 #==============================================================================
826 class BasePasterCommand(Command):
764 class BasePasterCommand(Command):
827 """
765 """
828 Abstract Base Class for paster commands.
766 Abstract Base Class for paster commands.
829
767
830 The celery commands are somewhat aggressive about loading
768 The celery commands are somewhat aggressive about loading
831 celery.conf, and since our module sets the `CELERY_LOADER`
769 celery.conf, and since our module sets the `CELERY_LOADER`
832 environment variable to our loader, we have to bootstrap a bit and
770 environment variable to our loader, we have to bootstrap a bit and
833 make sure we've had a chance to load the pylons config off of the
771 make sure we've had a chance to load the pylons config off of the
834 command line, otherwise everything fails.
772 command line, otherwise everything fails.
835 """
773 """
836 min_args = 1
774 min_args = 1
837 min_args_error = "Please provide a paster config file as an argument."
775 min_args_error = "Please provide a paster config file as an argument."
838 takes_config_file = 1
776 takes_config_file = 1
839 requires_config_file = True
777 requires_config_file = True
840
778
841 def notify_msg(self, msg, log=False):
779 def notify_msg(self, msg, log=False):
842 """Make a notification to user, additionally if logger is passed
780 """Make a notification to user, additionally if logger is passed
843 it logs this action using given logger
781 it logs this action using given logger
844
782
845 :param msg: message that will be printed to user
783 :param msg: message that will be printed to user
846 :param log: logging instance, to use to additionally log this message
784 :param log: logging instance, to use to additionally log this message
847
785
848 """
786 """
849 if log and isinstance(log, logging):
787 if log and isinstance(log, logging):
850 log(msg)
788 log(msg)
851
789
852 def run(self, args):
790 def run(self, args):
853 """
791 """
854 Overrides Command.run
792 Overrides Command.run
855
793
856 Checks for a config file argument and loads it.
794 Checks for a config file argument and loads it.
857 """
795 """
858 if len(args) < self.min_args:
796 if len(args) < self.min_args:
859 raise BadCommand(
797 raise BadCommand(
860 self.min_args_error % {'min_args': self.min_args,
798 self.min_args_error % {'min_args': self.min_args,
861 'actual_args': len(args)})
799 'actual_args': len(args)})
862
800
863 # Decrement because we're going to lob off the first argument.
801 # Decrement because we're going to lob off the first argument.
864 # @@ This is hacky
802 # @@ This is hacky
865 self.min_args -= 1
803 self.min_args -= 1
866 self.bootstrap_config(args[0])
804 self.bootstrap_config(args[0])
867 self.update_parser()
805 self.update_parser()
868 return super(BasePasterCommand, self).run(args[1:])
806 return super(BasePasterCommand, self).run(args[1:])
869
807
870 def update_parser(self):
808 def update_parser(self):
871 """
809 """
872 Abstract method. Allows for the class' parser to be updated
810 Abstract method. Allows for the class' parser to be updated
873 before the superclass' `run` method is called. Necessary to
811 before the superclass' `run` method is called. Necessary to
874 allow options/arguments to be passed through to the underlying
812 allow options/arguments to be passed through to the underlying
875 celery command.
813 celery command.
876 """
814 """
877 raise NotImplementedError("Abstract Method.")
815 raise NotImplementedError("Abstract Method.")
878
816
879 def bootstrap_config(self, conf):
817 def bootstrap_config(self, conf):
880 """
818 """
881 Loads the pylons configuration.
819 Loads the pylons configuration.
882 """
820 """
883 from pylons import config as pylonsconfig
821 from pylons import config as pylonsconfig
884
822
885 self.path_to_ini_file = os.path.realpath(conf)
823 self.path_to_ini_file = os.path.realpath(conf)
886 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
824 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
887 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
825 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
888
826
889 def _init_session(self):
827 def _init_session(self):
890 """
828 """
891 Inits SqlAlchemy Session
829 Inits SqlAlchemy Session
892 """
830 """
893 logging.config.fileConfig(self.path_to_ini_file)
831 logging.config.fileConfig(self.path_to_ini_file)
894 from pylons import config
832 from pylons import config
895 from rhodecode.config.utils import initialize_database
833 from rhodecode.config.utils import initialize_database
896
834
897 # get to remove repos !!
835 # get to remove repos !!
898 add_cache(config)
836 add_cache(config)
899 initialize_database(config)
837 initialize_database(config)
900
838
901
839
902 @decorator.decorator
840 @decorator.decorator
903 def jsonify(func, *args, **kwargs):
841 def jsonify(func, *args, **kwargs):
904 """Action decorator that formats output for JSON
842 """Action decorator that formats output for JSON
905
843
906 Given a function that will return content, this decorator will turn
844 Given a function that will return content, this decorator will turn
907 the result into JSON, with a content-type of 'application/json' and
845 the result into JSON, with a content-type of 'application/json' and
908 output it.
846 output it.
909
847
910 """
848 """
911 from pylons.decorators.util import get_pylons
849 from pylons.decorators.util import get_pylons
912 from rhodecode.lib.ext_json import json
850 from rhodecode.lib.ext_json import json
913 pylons = get_pylons(args)
851 pylons = get_pylons(args)
914 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
852 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
915 data = func(*args, **kwargs)
853 data = func(*args, **kwargs)
916 if isinstance(data, (list, tuple)):
854 if isinstance(data, (list, tuple)):
917 msg = "JSON responses with Array envelopes are susceptible to " \
855 msg = "JSON responses with Array envelopes are susceptible to " \
918 "cross-site data leak attacks, see " \
856 "cross-site data leak attacks, see " \
919 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
857 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
920 warnings.warn(msg, Warning, 2)
858 warnings.warn(msg, Warning, 2)
921 log.warning(msg)
859 log.warning(msg)
922 log.debug("Returning JSON wrapped action output")
860 log.debug("Returning JSON wrapped action output")
923 return json.dumps(data, encoding='utf-8')
861 return json.dumps(data, encoding='utf-8')
924
862
925
863
926 class PartialRenderer(object):
864 class PartialRenderer(object):
927 """
865 """
928 Partial renderer used to render chunks of html used in datagrids
866 Partial renderer used to render chunks of html used in datagrids
929 use like::
867 use like::
930
868
931 _render = PartialRenderer('data_table/_dt_elements.mako')
869 _render = PartialRenderer('data_table/_dt_elements.mako')
932 _render('quick_menu', args, kwargs)
870 _render('quick_menu', args, kwargs)
933 PartialRenderer.h,
871 PartialRenderer.h,
934 c,
872 c,
935 _,
873 _,
936 ungettext
874 ungettext
937 are the template stuff initialized inside and can be re-used later
875 are the template stuff initialized inside and can be re-used later
938
876
939 :param tmpl_name: template path relate to /templates/ dir
877 :param tmpl_name: template path relate to /templates/ dir
940 """
878 """
941
879
942 def __init__(self, tmpl_name):
880 def __init__(self, tmpl_name):
943 import rhodecode
881 import rhodecode
944 from pylons import request, tmpl_context as c
882 from pylons import request, tmpl_context as c
945 from pylons.i18n.translation import _, ungettext
883 from pylons.i18n.translation import _, ungettext
946 from rhodecode.lib import helpers as h
884 from rhodecode.lib import helpers as h
947
885
948 self.tmpl_name = tmpl_name
886 self.tmpl_name = tmpl_name
949 self.rhodecode = rhodecode
887 self.rhodecode = rhodecode
950 self.c = c
888 self.c = c
951 self._ = _
889 self._ = _
952 self.ungettext = ungettext
890 self.ungettext = ungettext
953 self.h = h
891 self.h = h
954 self.request = request
892 self.request = request
955
893
956 def _mako_lookup(self):
894 def _mako_lookup(self):
957 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
895 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
958 return _tmpl_lookup.get_template(self.tmpl_name)
896 return _tmpl_lookup.get_template(self.tmpl_name)
959
897
960 def _update_kwargs_for_render(self, kwargs):
898 def _update_kwargs_for_render(self, kwargs):
961 """
899 """
962 Inject params required for Mako rendering
900 Inject params required for Mako rendering
963 """
901 """
964 _kwargs = {
902 _kwargs = {
965 '_': self._,
903 '_': self._,
966 'h': self.h,
904 'h': self.h,
967 'c': self.c,
905 'c': self.c,
968 'request': self.request,
906 'request': self.request,
969 'ungettext': self.ungettext,
907 'ungettext': self.ungettext,
970 }
908 }
971 _kwargs.update(kwargs)
909 _kwargs.update(kwargs)
972 return _kwargs
910 return _kwargs
973
911
974 def _render_with_exc(self, render_func, args, kwargs):
912 def _render_with_exc(self, render_func, args, kwargs):
975 try:
913 try:
976 return render_func.render(*args, **kwargs)
914 return render_func.render(*args, **kwargs)
977 except:
915 except:
978 log.error(exceptions.text_error_template().render())
916 log.error(exceptions.text_error_template().render())
979 raise
917 raise
980
918
981 def _get_template(self, template_obj, def_name):
919 def _get_template(self, template_obj, def_name):
982 if def_name:
920 if def_name:
983 tmpl = template_obj.get_def(def_name)
921 tmpl = template_obj.get_def(def_name)
984 else:
922 else:
985 tmpl = template_obj
923 tmpl = template_obj
986 return tmpl
924 return tmpl
987
925
988 def render(self, def_name, *args, **kwargs):
926 def render(self, def_name, *args, **kwargs):
989 lookup_obj = self._mako_lookup()
927 lookup_obj = self._mako_lookup()
990 tmpl = self._get_template(lookup_obj, def_name=def_name)
928 tmpl = self._get_template(lookup_obj, def_name=def_name)
991 kwargs = self._update_kwargs_for_render(kwargs)
929 kwargs = self._update_kwargs_for_render(kwargs)
992 return self._render_with_exc(tmpl, args, kwargs)
930 return self._render_with_exc(tmpl, args, kwargs)
993
931
994 def __call__(self, tmpl, *args, **kwargs):
932 def __call__(self, tmpl, *args, **kwargs):
995 return self.render(tmpl, *args, **kwargs)
933 return self.render(tmpl, *args, **kwargs)
996
934
997
935
998 def password_changed(auth_user, session):
936 def password_changed(auth_user, session):
999 # Never report password change in case of default user or anonymous user.
937 # Never report password change in case of default user or anonymous user.
1000 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
938 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
1001 return False
939 return False
1002
940
1003 password_hash = md5(auth_user.password) if auth_user.password else None
941 password_hash = md5(auth_user.password) if auth_user.password else None
1004 rhodecode_user = session.get('rhodecode_user', {})
942 rhodecode_user = session.get('rhodecode_user', {})
1005 session_password_hash = rhodecode_user.get('password', '')
943 session_password_hash = rhodecode_user.get('password', '')
1006 return password_hash != session_password_hash
944 return password_hash != session_password_hash
1007
945
1008
946
1009 def read_opensource_licenses():
947 def read_opensource_licenses():
1010 global _license_cache
948 global _license_cache
1011
949
1012 if not _license_cache:
950 if not _license_cache:
1013 licenses = pkg_resources.resource_string(
951 licenses = pkg_resources.resource_string(
1014 'rhodecode', 'config/licenses.json')
952 'rhodecode', 'config/licenses.json')
1015 _license_cache = json.loads(licenses)
953 _license_cache = json.loads(licenses)
1016
954
1017 return _license_cache
955 return _license_cache
1018
956
1019
957
1020 def get_registry(request):
958 def get_registry(request):
1021 """
959 """
1022 Utility to get the pyramid registry from a request. During migration to
960 Utility to get the pyramid registry from a request. During migration to
1023 pyramid we sometimes want to use the pyramid registry from pylons context.
961 pyramid we sometimes want to use the pyramid registry from pylons context.
1024 Therefore this utility returns `request.registry` for pyramid requests and
962 Therefore this utility returns `request.registry` for pyramid requests and
1025 uses `get_current_registry()` for pylons requests.
963 uses `get_current_registry()` for pylons requests.
1026 """
964 """
1027 try:
965 try:
1028 return request.registry
966 return request.registry
1029 except AttributeError:
967 except AttributeError:
1030 return get_current_registry()
968 return get_current_registry()
1031
969
1032
970
1033 def generate_platform_uuid():
971 def generate_platform_uuid():
1034 """
972 """
1035 Generates platform UUID based on it's name
973 Generates platform UUID based on it's name
1036 """
974 """
1037 import platform
975 import platform
1038
976
1039 try:
977 try:
1040 uuid_list = [platform.platform()]
978 uuid_list = [platform.platform()]
1041 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
979 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
1042 except Exception as e:
980 except Exception as e:
1043 log.error('Failed to generate host uuid: %s' % e)
981 log.error('Failed to generate host uuid: %s' % e)
1044 return 'UNDEFINED'
982 return 'UNDEFINED'
@@ -1,650 +1,666 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry, get_current_request
32 from pyramid.threadlocal import get_current_registry, get_current_request
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
150
151 todos = Session().query(ChangesetComment) \
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
156
157 if not show_outdated:
157 if not show_outdated:
158 todos = todos.filter(
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
160 ChangesetComment.COMMENT_OUTDATED)
161
161
162 todos = todos.all()
162 todos = todos.all()
163
163
164 return todos
164 return todos
165
165
166 def _log_audit_action(self, action, action_data, user, comment):
167 audit_logger.store(
168 action=action,
169 action_data=action_data,
170 user=user,
171 repo=comment.repo)
172
166 def create(self, text, repo, user, commit_id=None, pull_request=None,
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
167 f_path=None, line_no=None, status_change=None,
174 f_path=None, line_no=None, status_change=None,
168 status_change_type=None, comment_type=None,
175 status_change_type=None, comment_type=None,
169 resolves_comment_id=None, closing_pr=False, send_email=True,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
170 renderer=None):
177 renderer=None):
171 """
178 """
172 Creates new comment for commit or pull request.
179 Creates new comment for commit or pull request.
173 IF status_change is not none this comment is associated with a
180 IF status_change is not none this comment is associated with a
174 status change of commit or commit associated with pull request
181 status change of commit or commit associated with pull request
175
182
176 :param text:
183 :param text:
177 :param repo:
184 :param repo:
178 :param user:
185 :param user:
179 :param commit_id:
186 :param commit_id:
180 :param pull_request:
187 :param pull_request:
181 :param f_path:
188 :param f_path:
182 :param line_no:
189 :param line_no:
183 :param status_change: Label for status change
190 :param status_change: Label for status change
184 :param comment_type: Type of comment
191 :param comment_type: Type of comment
185 :param status_change_type: type of status change
192 :param status_change_type: type of status change
186 :param closing_pr:
193 :param closing_pr:
187 :param send_email:
194 :param send_email:
188 :param renderer: pick renderer for this comment
195 :param renderer: pick renderer for this comment
189 """
196 """
190 if not text:
197 if not text:
191 log.warning('Missing text for comment, skipping...')
198 log.warning('Missing text for comment, skipping...')
192 return
199 return
193
200
194 if not renderer:
201 if not renderer:
195 renderer = self._get_renderer()
202 renderer = self._get_renderer()
196
203
197 repo = self._get_repo(repo)
204 repo = self._get_repo(repo)
198 user = self._get_user(user)
205 user = self._get_user(user)
199
206
200 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
201 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
202 comment_body=text,
209 comment_body=text,
203 comment_type=comment_type,
210 comment_type=comment_type,
204 comment_file=f_path,
211 comment_file=f_path,
205 comment_line=line_no,
212 comment_line=line_no,
206 renderer_type=renderer,
213 renderer_type=renderer,
207 status_change=status_change_type,
214 status_change=status_change_type,
208 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
209 repo=repo.repo_id,
216 repo=repo.repo_id,
210 user=user.user_id,
217 user=user.user_id,
211 ))
218 ))
212
219
213 comment = ChangesetComment()
220 comment = ChangesetComment()
214 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
215 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
216 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
217 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
218 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
219
226
220 comment.repo = repo
227 comment.repo = repo
221 comment.author = user
228 comment.author = user
222 comment.resolved_comment = self.__get_commit_comment(
229 comment.resolved_comment = self.__get_commit_comment(
223 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
224
231
225 pull_request_id = pull_request
232 pull_request_id = pull_request
226
233
227 commit_obj = None
234 commit_obj = None
228 pull_request_obj = None
235 pull_request_obj = None
229
236
230 if commit_id:
237 if commit_id:
231 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
232 # do a lookup, so we don't pass something bad here
239 # do a lookup, so we don't pass something bad here
233 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
234 comment.revision = commit_obj.raw_id
241 comment.revision = commit_obj.raw_id
235
242
236 elif pull_request_id:
243 elif pull_request_id:
237 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
238 pull_request_obj = self.__get_pull_request(pull_request_id)
245 pull_request_obj = self.__get_pull_request(pull_request_id)
239 comment.pull_request = pull_request_obj
246 comment.pull_request = pull_request_obj
240 else:
247 else:
241 raise Exception('Please specify commit or pull_request_id')
248 raise Exception('Please specify commit or pull_request_id')
242
249
243 Session().add(comment)
250 Session().add(comment)
244 Session().flush()
251 Session().flush()
245 kwargs = {
252 kwargs = {
246 'user': user,
253 'user': user,
247 'renderer_type': renderer,
254 'renderer_type': renderer,
248 'repo_name': repo.repo_name,
255 'repo_name': repo.repo_name,
249 'status_change': status_change,
256 'status_change': status_change,
250 'status_change_type': status_change_type,
257 'status_change_type': status_change_type,
251 'comment_body': text,
258 'comment_body': text,
252 'comment_file': f_path,
259 'comment_file': f_path,
253 'comment_line': line_no,
260 'comment_line': line_no,
254 'comment_type': comment_type or 'note'
261 'comment_type': comment_type or 'note'
255 }
262 }
256
263
257 if commit_obj:
264 if commit_obj:
258 recipients = ChangesetComment.get_users(
265 recipients = ChangesetComment.get_users(
259 revision=commit_obj.raw_id)
266 revision=commit_obj.raw_id)
260 # add commit author if it's in RhodeCode system
267 # add commit author if it's in RhodeCode system
261 cs_author = User.get_from_cs_author(commit_obj.author)
268 cs_author = User.get_from_cs_author(commit_obj.author)
262 if not cs_author:
269 if not cs_author:
263 # use repo owner if we cannot extract the author correctly
270 # use repo owner if we cannot extract the author correctly
264 cs_author = repo.user
271 cs_author = repo.user
265 recipients += [cs_author]
272 recipients += [cs_author]
266
273
267 commit_comment_url = self.get_url(comment)
274 commit_comment_url = self.get_url(comment)
268
275
269 target_repo_url = h.link_to(
276 target_repo_url = h.link_to(
270 repo.repo_name,
277 repo.repo_name,
271 h.route_url('repo_summary', repo_name=repo.repo_name))
278 h.route_url('repo_summary', repo_name=repo.repo_name))
272
279
273 # commit specifics
280 # commit specifics
274 kwargs.update({
281 kwargs.update({
275 'commit': commit_obj,
282 'commit': commit_obj,
276 'commit_message': commit_obj.message,
283 'commit_message': commit_obj.message,
277 'commit_target_repo': target_repo_url,
284 'commit_target_repo': target_repo_url,
278 'commit_comment_url': commit_comment_url,
285 'commit_comment_url': commit_comment_url,
279 })
286 })
280
287
281 elif pull_request_obj:
288 elif pull_request_obj:
282 # get the current participants of this pull request
289 # get the current participants of this pull request
283 recipients = ChangesetComment.get_users(
290 recipients = ChangesetComment.get_users(
284 pull_request_id=pull_request_obj.pull_request_id)
291 pull_request_id=pull_request_obj.pull_request_id)
285 # add pull request author
292 # add pull request author
286 recipients += [pull_request_obj.author]
293 recipients += [pull_request_obj.author]
287
294
288 # add the reviewers to notification
295 # add the reviewers to notification
289 recipients += [x.user for x in pull_request_obj.reviewers]
296 recipients += [x.user for x in pull_request_obj.reviewers]
290
297
291 pr_target_repo = pull_request_obj.target_repo
298 pr_target_repo = pull_request_obj.target_repo
292 pr_source_repo = pull_request_obj.source_repo
299 pr_source_repo = pull_request_obj.source_repo
293
300
294 pr_comment_url = h.url(
301 pr_comment_url = h.url(
295 'pullrequest_show',
302 'pullrequest_show',
296 repo_name=pr_target_repo.repo_name,
303 repo_name=pr_target_repo.repo_name,
297 pull_request_id=pull_request_obj.pull_request_id,
304 pull_request_id=pull_request_obj.pull_request_id,
298 anchor='comment-%s' % comment.comment_id,
305 anchor='comment-%s' % comment.comment_id,
299 qualified=True,)
306 qualified=True,)
300
307
301 # set some variables for email notification
308 # set some variables for email notification
302 pr_target_repo_url = h.route_url(
309 pr_target_repo_url = h.route_url(
303 'repo_summary', repo_name=pr_target_repo.repo_name)
310 'repo_summary', repo_name=pr_target_repo.repo_name)
304
311
305 pr_source_repo_url = h.route_url(
312 pr_source_repo_url = h.route_url(
306 'repo_summary', repo_name=pr_source_repo.repo_name)
313 'repo_summary', repo_name=pr_source_repo.repo_name)
307
314
308 # pull request specifics
315 # pull request specifics
309 kwargs.update({
316 kwargs.update({
310 'pull_request': pull_request_obj,
317 'pull_request': pull_request_obj,
311 'pr_id': pull_request_obj.pull_request_id,
318 'pr_id': pull_request_obj.pull_request_id,
312 'pr_target_repo': pr_target_repo,
319 'pr_target_repo': pr_target_repo,
313 'pr_target_repo_url': pr_target_repo_url,
320 'pr_target_repo_url': pr_target_repo_url,
314 'pr_source_repo': pr_source_repo,
321 'pr_source_repo': pr_source_repo,
315 'pr_source_repo_url': pr_source_repo_url,
322 'pr_source_repo_url': pr_source_repo_url,
316 'pr_comment_url': pr_comment_url,
323 'pr_comment_url': pr_comment_url,
317 'pr_closing': closing_pr,
324 'pr_closing': closing_pr,
318 })
325 })
319 if send_email:
326 if send_email:
320 # pre-generate the subject for notification itself
327 # pre-generate the subject for notification itself
321 (subject,
328 (subject,
322 _h, _e, # we don't care about those
329 _h, _e, # we don't care about those
323 body_plaintext) = EmailNotificationModel().render_email(
330 body_plaintext) = EmailNotificationModel().render_email(
324 notification_type, **kwargs)
331 notification_type, **kwargs)
325
332
326 mention_recipients = set(
333 mention_recipients = set(
327 self._extract_mentions(text)).difference(recipients)
334 self._extract_mentions(text)).difference(recipients)
328
335
329 # create notification objects, and emails
336 # create notification objects, and emails
330 NotificationModel().create(
337 NotificationModel().create(
331 created_by=user,
338 created_by=user,
332 notification_subject=subject,
339 notification_subject=subject,
333 notification_body=body_plaintext,
340 notification_body=body_plaintext,
334 notification_type=notification_type,
341 notification_type=notification_type,
335 recipients=recipients,
342 recipients=recipients,
336 mention_recipients=mention_recipients,
343 mention_recipients=mention_recipients,
337 email_kwargs=kwargs,
344 email_kwargs=kwargs,
338 )
345 )
339
346
340 action = (
347 Session().flush()
341 'user_commented_pull_request:{}'.format(
348 if comment.pull_request:
342 comment.pull_request.pull_request_id)
349 action = 'repo.pull_request.comment.create'
343 if comment.pull_request
350 else:
344 else 'user_commented_revision:{}'.format(comment.revision)
351 action = 'repo.commit.comment.create'
345 )
352
346 action_logger(user, action, comment.repo)
353 comment_data = comment.get_api_data()
354 self._log_audit_action(
355 action, {'data': comment_data}, user, comment)
347
356
348 registry = get_current_registry()
357 registry = get_current_registry()
349 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
350 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
351 msg_url = ''
360 msg_url = ''
352 if commit_obj:
361 if commit_obj:
353 msg_url = commit_comment_url
362 msg_url = commit_comment_url
354 repo_name = repo.repo_name
363 repo_name = repo.repo_name
355 elif pull_request_obj:
364 elif pull_request_obj:
356 msg_url = pr_comment_url
365 msg_url = pr_comment_url
357 repo_name = pr_target_repo.repo_name
366 repo_name = pr_target_repo.repo_name
358
367
359 if channelstream_config.get('enabled'):
368 if channelstream_config.get('enabled'):
360 message = '<strong>{}</strong> {} - ' \
369 message = '<strong>{}</strong> {} - ' \
361 '<a onclick="window.location=\'{}\';' \
370 '<a onclick="window.location=\'{}\';' \
362 'window.location.reload()">' \
371 'window.location.reload()">' \
363 '<strong>{}</strong></a>'
372 '<strong>{}</strong></a>'
364 message = message.format(
373 message = message.format(
365 user.username, _('made a comment'), msg_url,
374 user.username, _('made a comment'), msg_url,
366 _('Show it now'))
375 _('Show it now'))
367 channel = '/repo${}$/pr/{}'.format(
376 channel = '/repo${}$/pr/{}'.format(
368 repo_name,
377 repo_name,
369 pull_request_id
378 pull_request_id
370 )
379 )
371 payload = {
380 payload = {
372 'type': 'message',
381 'type': 'message',
373 'timestamp': datetime.utcnow(),
382 'timestamp': datetime.utcnow(),
374 'user': 'system',
383 'user': 'system',
375 'exclude_users': [user.username],
384 'exclude_users': [user.username],
376 'channel': channel,
385 'channel': channel,
377 'message': {
386 'message': {
378 'message': message,
387 'message': message,
379 'level': 'info',
388 'level': 'info',
380 'topic': '/notifications'
389 'topic': '/notifications'
381 }
390 }
382 }
391 }
383 channelstream_request(channelstream_config, [payload],
392 channelstream_request(channelstream_config, [payload],
384 '/message', raise_exc=False)
393 '/message', raise_exc=False)
385
394
386 return comment
395 return comment
387
396
388 def delete(self, comment):
397 def delete(self, comment, user):
389 """
398 """
390 Deletes given comment
399 Deletes given comment
391
392 :param comment_id:
393 """
400 """
394 comment = self.__get_commit_comment(comment)
401 comment = self.__get_commit_comment(comment)
402 old_data = comment.get_api_data()
395 Session().delete(comment)
403 Session().delete(comment)
396
404
405 if comment.pull_request:
406 action = 'repo.pull_request.comment.delete'
407 else:
408 action = 'repo.commit.comment.delete'
409
410 self._log_audit_action(
411 action, {'old_data': old_data}, user, comment)
412
397 return comment
413 return comment
398
414
399 def get_all_comments(self, repo_id, revision=None, pull_request=None):
415 def get_all_comments(self, repo_id, revision=None, pull_request=None):
400 q = ChangesetComment.query()\
416 q = ChangesetComment.query()\
401 .filter(ChangesetComment.repo_id == repo_id)
417 .filter(ChangesetComment.repo_id == repo_id)
402 if revision:
418 if revision:
403 q = q.filter(ChangesetComment.revision == revision)
419 q = q.filter(ChangesetComment.revision == revision)
404 elif pull_request:
420 elif pull_request:
405 pull_request = self.__get_pull_request(pull_request)
421 pull_request = self.__get_pull_request(pull_request)
406 q = q.filter(ChangesetComment.pull_request == pull_request)
422 q = q.filter(ChangesetComment.pull_request == pull_request)
407 else:
423 else:
408 raise Exception('Please specify commit or pull_request')
424 raise Exception('Please specify commit or pull_request')
409 q = q.order_by(ChangesetComment.created_on)
425 q = q.order_by(ChangesetComment.created_on)
410 return q.all()
426 return q.all()
411
427
412 def get_url(self, comment, request=None, permalink=False):
428 def get_url(self, comment, request=None, permalink=False):
413 if not request:
429 if not request:
414 request = get_current_request()
430 request = get_current_request()
415
431
416 comment = self.__get_commit_comment(comment)
432 comment = self.__get_commit_comment(comment)
417 if comment.pull_request:
433 if comment.pull_request:
418 pull_request = comment.pull_request
434 pull_request = comment.pull_request
419 if permalink:
435 if permalink:
420 return request.route_url(
436 return request.route_url(
421 'pull_requests_global',
437 'pull_requests_global',
422 pull_request_id=pull_request.pull_request_id,
438 pull_request_id=pull_request.pull_request_id,
423 _anchor='comment-%s' % comment.comment_id)
439 _anchor='comment-%s' % comment.comment_id)
424 else:
440 else:
425 return request.route_url(
441 return request.route_url(
426 'pullrequest_show',
442 'pullrequest_show',
427 repo_name=safe_str(pull_request.target_repo.repo_name),
443 repo_name=safe_str(pull_request.target_repo.repo_name),
428 pull_request_id=pull_request.pull_request_id,
444 pull_request_id=pull_request.pull_request_id,
429 _anchor='comment-%s' % comment.comment_id)
445 _anchor='comment-%s' % comment.comment_id)
430
446
431 else:
447 else:
432 repo = comment.repo
448 repo = comment.repo
433 commit_id = comment.revision
449 commit_id = comment.revision
434
450
435 if permalink:
451 if permalink:
436 return request.route_url(
452 return request.route_url(
437 'repo_commit', repo_name=safe_str(repo.repo_id),
453 'repo_commit', repo_name=safe_str(repo.repo_id),
438 commit_id=commit_id,
454 commit_id=commit_id,
439 _anchor='comment-%s' % comment.comment_id)
455 _anchor='comment-%s' % comment.comment_id)
440
456
441 else:
457 else:
442 return request.route_url(
458 return request.route_url(
443 'repo_commit', repo_name=safe_str(repo.repo_name),
459 'repo_commit', repo_name=safe_str(repo.repo_name),
444 commit_id=commit_id,
460 commit_id=commit_id,
445 _anchor='comment-%s' % comment.comment_id)
461 _anchor='comment-%s' % comment.comment_id)
446
462
447 def get_comments(self, repo_id, revision=None, pull_request=None):
463 def get_comments(self, repo_id, revision=None, pull_request=None):
448 """
464 """
449 Gets main comments based on revision or pull_request_id
465 Gets main comments based on revision or pull_request_id
450
466
451 :param repo_id:
467 :param repo_id:
452 :param revision:
468 :param revision:
453 :param pull_request:
469 :param pull_request:
454 """
470 """
455
471
456 q = ChangesetComment.query()\
472 q = ChangesetComment.query()\
457 .filter(ChangesetComment.repo_id == repo_id)\
473 .filter(ChangesetComment.repo_id == repo_id)\
458 .filter(ChangesetComment.line_no == None)\
474 .filter(ChangesetComment.line_no == None)\
459 .filter(ChangesetComment.f_path == None)
475 .filter(ChangesetComment.f_path == None)
460 if revision:
476 if revision:
461 q = q.filter(ChangesetComment.revision == revision)
477 q = q.filter(ChangesetComment.revision == revision)
462 elif pull_request:
478 elif pull_request:
463 pull_request = self.__get_pull_request(pull_request)
479 pull_request = self.__get_pull_request(pull_request)
464 q = q.filter(ChangesetComment.pull_request == pull_request)
480 q = q.filter(ChangesetComment.pull_request == pull_request)
465 else:
481 else:
466 raise Exception('Please specify commit or pull_request')
482 raise Exception('Please specify commit or pull_request')
467 q = q.order_by(ChangesetComment.created_on)
483 q = q.order_by(ChangesetComment.created_on)
468 return q.all()
484 return q.all()
469
485
470 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
486 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
471 q = self._get_inline_comments_query(repo_id, revision, pull_request)
487 q = self._get_inline_comments_query(repo_id, revision, pull_request)
472 return self._group_comments_by_path_and_line_number(q)
488 return self._group_comments_by_path_and_line_number(q)
473
489
474 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
490 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
475 version=None):
491 version=None):
476 inline_cnt = 0
492 inline_cnt = 0
477 for fname, per_line_comments in inline_comments.iteritems():
493 for fname, per_line_comments in inline_comments.iteritems():
478 for lno, comments in per_line_comments.iteritems():
494 for lno, comments in per_line_comments.iteritems():
479 for comm in comments:
495 for comm in comments:
480 if not comm.outdated_at_version(version) and skip_outdated:
496 if not comm.outdated_at_version(version) and skip_outdated:
481 inline_cnt += 1
497 inline_cnt += 1
482
498
483 return inline_cnt
499 return inline_cnt
484
500
485 def get_outdated_comments(self, repo_id, pull_request):
501 def get_outdated_comments(self, repo_id, pull_request):
486 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
502 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
487 # of a pull request.
503 # of a pull request.
488 q = self._all_inline_comments_of_pull_request(pull_request)
504 q = self._all_inline_comments_of_pull_request(pull_request)
489 q = q.filter(
505 q = q.filter(
490 ChangesetComment.display_state ==
506 ChangesetComment.display_state ==
491 ChangesetComment.COMMENT_OUTDATED
507 ChangesetComment.COMMENT_OUTDATED
492 ).order_by(ChangesetComment.comment_id.asc())
508 ).order_by(ChangesetComment.comment_id.asc())
493
509
494 return self._group_comments_by_path_and_line_number(q)
510 return self._group_comments_by_path_and_line_number(q)
495
511
496 def _get_inline_comments_query(self, repo_id, revision, pull_request):
512 def _get_inline_comments_query(self, repo_id, revision, pull_request):
497 # TODO: johbo: Split this into two methods: One for PR and one for
513 # TODO: johbo: Split this into two methods: One for PR and one for
498 # commit.
514 # commit.
499 if revision:
515 if revision:
500 q = Session().query(ChangesetComment).filter(
516 q = Session().query(ChangesetComment).filter(
501 ChangesetComment.repo_id == repo_id,
517 ChangesetComment.repo_id == repo_id,
502 ChangesetComment.line_no != null(),
518 ChangesetComment.line_no != null(),
503 ChangesetComment.f_path != null(),
519 ChangesetComment.f_path != null(),
504 ChangesetComment.revision == revision)
520 ChangesetComment.revision == revision)
505
521
506 elif pull_request:
522 elif pull_request:
507 pull_request = self.__get_pull_request(pull_request)
523 pull_request = self.__get_pull_request(pull_request)
508 if not CommentsModel.use_outdated_comments(pull_request):
524 if not CommentsModel.use_outdated_comments(pull_request):
509 q = self._visible_inline_comments_of_pull_request(pull_request)
525 q = self._visible_inline_comments_of_pull_request(pull_request)
510 else:
526 else:
511 q = self._all_inline_comments_of_pull_request(pull_request)
527 q = self._all_inline_comments_of_pull_request(pull_request)
512
528
513 else:
529 else:
514 raise Exception('Please specify commit or pull_request_id')
530 raise Exception('Please specify commit or pull_request_id')
515 q = q.order_by(ChangesetComment.comment_id.asc())
531 q = q.order_by(ChangesetComment.comment_id.asc())
516 return q
532 return q
517
533
518 def _group_comments_by_path_and_line_number(self, q):
534 def _group_comments_by_path_and_line_number(self, q):
519 comments = q.all()
535 comments = q.all()
520 paths = collections.defaultdict(lambda: collections.defaultdict(list))
536 paths = collections.defaultdict(lambda: collections.defaultdict(list))
521 for co in comments:
537 for co in comments:
522 paths[co.f_path][co.line_no].append(co)
538 paths[co.f_path][co.line_no].append(co)
523 return paths
539 return paths
524
540
525 @classmethod
541 @classmethod
526 def needed_extra_diff_context(cls):
542 def needed_extra_diff_context(cls):
527 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
543 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
528
544
529 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
545 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
530 if not CommentsModel.use_outdated_comments(pull_request):
546 if not CommentsModel.use_outdated_comments(pull_request):
531 return
547 return
532
548
533 comments = self._visible_inline_comments_of_pull_request(pull_request)
549 comments = self._visible_inline_comments_of_pull_request(pull_request)
534 comments_to_outdate = comments.all()
550 comments_to_outdate = comments.all()
535
551
536 for comment in comments_to_outdate:
552 for comment in comments_to_outdate:
537 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
553 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
538
554
539 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
555 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
540 diff_line = _parse_comment_line_number(comment.line_no)
556 diff_line = _parse_comment_line_number(comment.line_no)
541
557
542 try:
558 try:
543 old_context = old_diff_proc.get_context_of_line(
559 old_context = old_diff_proc.get_context_of_line(
544 path=comment.f_path, diff_line=diff_line)
560 path=comment.f_path, diff_line=diff_line)
545 new_context = new_diff_proc.get_context_of_line(
561 new_context = new_diff_proc.get_context_of_line(
546 path=comment.f_path, diff_line=diff_line)
562 path=comment.f_path, diff_line=diff_line)
547 except (diffs.LineNotInDiffException,
563 except (diffs.LineNotInDiffException,
548 diffs.FileNotInDiffException):
564 diffs.FileNotInDiffException):
549 comment.display_state = ChangesetComment.COMMENT_OUTDATED
565 comment.display_state = ChangesetComment.COMMENT_OUTDATED
550 return
566 return
551
567
552 if old_context == new_context:
568 if old_context == new_context:
553 return
569 return
554
570
555 if self._should_relocate_diff_line(diff_line):
571 if self._should_relocate_diff_line(diff_line):
556 new_diff_lines = new_diff_proc.find_context(
572 new_diff_lines = new_diff_proc.find_context(
557 path=comment.f_path, context=old_context,
573 path=comment.f_path, context=old_context,
558 offset=self.DIFF_CONTEXT_BEFORE)
574 offset=self.DIFF_CONTEXT_BEFORE)
559 if not new_diff_lines:
575 if not new_diff_lines:
560 comment.display_state = ChangesetComment.COMMENT_OUTDATED
576 comment.display_state = ChangesetComment.COMMENT_OUTDATED
561 else:
577 else:
562 new_diff_line = self._choose_closest_diff_line(
578 new_diff_line = self._choose_closest_diff_line(
563 diff_line, new_diff_lines)
579 diff_line, new_diff_lines)
564 comment.line_no = _diff_to_comment_line_number(new_diff_line)
580 comment.line_no = _diff_to_comment_line_number(new_diff_line)
565 else:
581 else:
566 comment.display_state = ChangesetComment.COMMENT_OUTDATED
582 comment.display_state = ChangesetComment.COMMENT_OUTDATED
567
583
568 def _should_relocate_diff_line(self, diff_line):
584 def _should_relocate_diff_line(self, diff_line):
569 """
585 """
570 Checks if relocation shall be tried for the given `diff_line`.
586 Checks if relocation shall be tried for the given `diff_line`.
571
587
572 If a comment points into the first lines, then we can have a situation
588 If a comment points into the first lines, then we can have a situation
573 that after an update another line has been added on top. In this case
589 that after an update another line has been added on top. In this case
574 we would find the context still and move the comment around. This
590 we would find the context still and move the comment around. This
575 would be wrong.
591 would be wrong.
576 """
592 """
577 should_relocate = (
593 should_relocate = (
578 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
594 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
579 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
595 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
580 return should_relocate
596 return should_relocate
581
597
582 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
598 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
583 candidate = new_diff_lines[0]
599 candidate = new_diff_lines[0]
584 best_delta = _diff_line_delta(diff_line, candidate)
600 best_delta = _diff_line_delta(diff_line, candidate)
585 for new_diff_line in new_diff_lines[1:]:
601 for new_diff_line in new_diff_lines[1:]:
586 delta = _diff_line_delta(diff_line, new_diff_line)
602 delta = _diff_line_delta(diff_line, new_diff_line)
587 if delta < best_delta:
603 if delta < best_delta:
588 candidate = new_diff_line
604 candidate = new_diff_line
589 best_delta = delta
605 best_delta = delta
590 return candidate
606 return candidate
591
607
592 def _visible_inline_comments_of_pull_request(self, pull_request):
608 def _visible_inline_comments_of_pull_request(self, pull_request):
593 comments = self._all_inline_comments_of_pull_request(pull_request)
609 comments = self._all_inline_comments_of_pull_request(pull_request)
594 comments = comments.filter(
610 comments = comments.filter(
595 coalesce(ChangesetComment.display_state, '') !=
611 coalesce(ChangesetComment.display_state, '') !=
596 ChangesetComment.COMMENT_OUTDATED)
612 ChangesetComment.COMMENT_OUTDATED)
597 return comments
613 return comments
598
614
599 def _all_inline_comments_of_pull_request(self, pull_request):
615 def _all_inline_comments_of_pull_request(self, pull_request):
600 comments = Session().query(ChangesetComment)\
616 comments = Session().query(ChangesetComment)\
601 .filter(ChangesetComment.line_no != None)\
617 .filter(ChangesetComment.line_no != None)\
602 .filter(ChangesetComment.f_path != None)\
618 .filter(ChangesetComment.f_path != None)\
603 .filter(ChangesetComment.pull_request == pull_request)
619 .filter(ChangesetComment.pull_request == pull_request)
604 return comments
620 return comments
605
621
606 def _all_general_comments_of_pull_request(self, pull_request):
622 def _all_general_comments_of_pull_request(self, pull_request):
607 comments = Session().query(ChangesetComment)\
623 comments = Session().query(ChangesetComment)\
608 .filter(ChangesetComment.line_no == None)\
624 .filter(ChangesetComment.line_no == None)\
609 .filter(ChangesetComment.f_path == None)\
625 .filter(ChangesetComment.f_path == None)\
610 .filter(ChangesetComment.pull_request == pull_request)
626 .filter(ChangesetComment.pull_request == pull_request)
611 return comments
627 return comments
612
628
613 @staticmethod
629 @staticmethod
614 def use_outdated_comments(pull_request):
630 def use_outdated_comments(pull_request):
615 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
631 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
616 settings = settings_model.get_general_settings()
632 settings = settings_model.get_general_settings()
617 return settings.get('rhodecode_use_outdated_comments', False)
633 return settings.get('rhodecode_use_outdated_comments', False)
618
634
619
635
620 def _parse_comment_line_number(line_no):
636 def _parse_comment_line_number(line_no):
621 """
637 """
622 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
638 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
623 """
639 """
624 old_line = None
640 old_line = None
625 new_line = None
641 new_line = None
626 if line_no.startswith('o'):
642 if line_no.startswith('o'):
627 old_line = int(line_no[1:])
643 old_line = int(line_no[1:])
628 elif line_no.startswith('n'):
644 elif line_no.startswith('n'):
629 new_line = int(line_no[1:])
645 new_line = int(line_no[1:])
630 else:
646 else:
631 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
647 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
632 return diffs.DiffLineNumber(old_line, new_line)
648 return diffs.DiffLineNumber(old_line, new_line)
633
649
634
650
635 def _diff_to_comment_line_number(diff_line):
651 def _diff_to_comment_line_number(diff_line):
636 if diff_line.new is not None:
652 if diff_line.new is not None:
637 return u'n{}'.format(diff_line.new)
653 return u'n{}'.format(diff_line.new)
638 elif diff_line.old is not None:
654 elif diff_line.old is not None:
639 return u'o{}'.format(diff_line.old)
655 return u'o{}'.format(diff_line.old)
640 return u''
656 return u''
641
657
642
658
643 def _diff_line_delta(a, b):
659 def _diff_line_delta(a, b):
644 if None not in (a.new, b.new):
660 if None not in (a.new, b.new):
645 return abs(a.new - b.new)
661 return abs(a.new - b.new)
646 elif None not in (a.old, b.old):
662 elif None not in (a.old, b.old):
647 return abs(a.old - b.old)
663 return abs(a.old - b.old)
648 else:
664 else:
649 raise ValueError(
665 raise ValueError(
650 "Cannot compute delta between {} and {}".format(a, b))
666 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,4031 +1,4065 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pylons.i18n.translation import lazy_ugettext as _
47 from pylons.i18n.translation import lazy_ugettext as _
48 from pyramid.threadlocal import get_current_request
48 from pyramid.threadlocal import get_current_request
49
49
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
209 def get_or_404(cls, id_, pyramid_exc=False):
210 if pyramid_exc:
210 if pyramid_exc:
211 # NOTE(marcink): backward compat, once migration to pyramid
211 # NOTE(marcink): backward compat, once migration to pyramid
212 # this should only use pyramid exceptions
212 # this should only use pyramid exceptions
213 from pyramid.httpexceptions import HTTPNotFound
213 from pyramid.httpexceptions import HTTPNotFound
214 else:
214 else:
215 from webob.exc import HTTPNotFound
215 from webob.exc import HTTPNotFound
216
216
217 try:
217 try:
218 id_ = int(id_)
218 id_ = int(id_)
219 except (TypeError, ValueError):
219 except (TypeError, ValueError):
220 raise HTTPNotFound
220 raise HTTPNotFound
221
221
222 res = cls.query().get(id_)
222 res = cls.query().get(id_)
223 if not res:
223 if not res:
224 raise HTTPNotFound
224 raise HTTPNotFound
225 return res
225 return res
226
226
227 @classmethod
227 @classmethod
228 def getAll(cls):
228 def getAll(cls):
229 # deprecated and left for backward compatibility
229 # deprecated and left for backward compatibility
230 return cls.get_all()
230 return cls.get_all()
231
231
232 @classmethod
232 @classmethod
233 def get_all(cls):
233 def get_all(cls):
234 return cls.query().all()
234 return cls.query().all()
235
235
236 @classmethod
236 @classmethod
237 def delete(cls, id_):
237 def delete(cls, id_):
238 obj = cls.query().get(id_)
238 obj = cls.query().get(id_)
239 Session().delete(obj)
239 Session().delete(obj)
240
240
241 @classmethod
241 @classmethod
242 def identity_cache(cls, session, attr_name, value):
242 def identity_cache(cls, session, attr_name, value):
243 exist_in_session = []
243 exist_in_session = []
244 for (item_cls, pkey), instance in session.identity_map.items():
244 for (item_cls, pkey), instance in session.identity_map.items():
245 if cls == item_cls and getattr(instance, attr_name) == value:
245 if cls == item_cls and getattr(instance, attr_name) == value:
246 exist_in_session.append(instance)
246 exist_in_session.append(instance)
247 if exist_in_session:
247 if exist_in_session:
248 if len(exist_in_session) == 1:
248 if len(exist_in_session) == 1:
249 return exist_in_session[0]
249 return exist_in_session[0]
250 log.exception(
250 log.exception(
251 'multiple objects with attr %s and '
251 'multiple objects with attr %s and '
252 'value %s found with same name: %r',
252 'value %s found with same name: %r',
253 attr_name, value, exist_in_session)
253 attr_name, value, exist_in_session)
254
254
255 def __repr__(self):
255 def __repr__(self):
256 if hasattr(self, '__unicode__'):
256 if hasattr(self, '__unicode__'):
257 # python repr needs to return str
257 # python repr needs to return str
258 try:
258 try:
259 return safe_str(self.__unicode__())
259 return safe_str(self.__unicode__())
260 except UnicodeDecodeError:
260 except UnicodeDecodeError:
261 pass
261 pass
262 return '<DB:%s>' % (self.__class__.__name__)
262 return '<DB:%s>' % (self.__class__.__name__)
263
263
264
264
265 class RhodeCodeSetting(Base, BaseModel):
265 class RhodeCodeSetting(Base, BaseModel):
266 __tablename__ = 'rhodecode_settings'
266 __tablename__ = 'rhodecode_settings'
267 __table_args__ = (
267 __table_args__ = (
268 UniqueConstraint('app_settings_name'),
268 UniqueConstraint('app_settings_name'),
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 )
271 )
272
272
273 SETTINGS_TYPES = {
273 SETTINGS_TYPES = {
274 'str': safe_str,
274 'str': safe_str,
275 'int': safe_int,
275 'int': safe_int,
276 'unicode': safe_unicode,
276 'unicode': safe_unicode,
277 'bool': str2bool,
277 'bool': str2bool,
278 'list': functools.partial(aslist, sep=',')
278 'list': functools.partial(aslist, sep=',')
279 }
279 }
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 GLOBAL_CONF_KEY = 'app_settings'
281 GLOBAL_CONF_KEY = 'app_settings'
282
282
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287
287
288 def __init__(self, key='', val='', type='unicode'):
288 def __init__(self, key='', val='', type='unicode'):
289 self.app_settings_name = key
289 self.app_settings_name = key
290 self.app_settings_type = type
290 self.app_settings_type = type
291 self.app_settings_value = val
291 self.app_settings_value = val
292
292
293 @validates('_app_settings_value')
293 @validates('_app_settings_value')
294 def validate_settings_value(self, key, val):
294 def validate_settings_value(self, key, val):
295 assert type(val) == unicode
295 assert type(val) == unicode
296 return val
296 return val
297
297
298 @hybrid_property
298 @hybrid_property
299 def app_settings_value(self):
299 def app_settings_value(self):
300 v = self._app_settings_value
300 v = self._app_settings_value
301 _type = self.app_settings_type
301 _type = self.app_settings_type
302 if _type:
302 if _type:
303 _type = self.app_settings_type.split('.')[0]
303 _type = self.app_settings_type.split('.')[0]
304 # decode the encrypted value
304 # decode the encrypted value
305 if 'encrypted' in self.app_settings_type:
305 if 'encrypted' in self.app_settings_type:
306 cipher = EncryptedTextValue()
306 cipher = EncryptedTextValue()
307 v = safe_unicode(cipher.process_result_value(v, None))
307 v = safe_unicode(cipher.process_result_value(v, None))
308
308
309 converter = self.SETTINGS_TYPES.get(_type) or \
309 converter = self.SETTINGS_TYPES.get(_type) or \
310 self.SETTINGS_TYPES['unicode']
310 self.SETTINGS_TYPES['unicode']
311 return converter(v)
311 return converter(v)
312
312
313 @app_settings_value.setter
313 @app_settings_value.setter
314 def app_settings_value(self, val):
314 def app_settings_value(self, val):
315 """
315 """
316 Setter that will always make sure we use unicode in app_settings_value
316 Setter that will always make sure we use unicode in app_settings_value
317
317
318 :param val:
318 :param val:
319 """
319 """
320 val = safe_unicode(val)
320 val = safe_unicode(val)
321 # encode the encrypted value
321 # encode the encrypted value
322 if 'encrypted' in self.app_settings_type:
322 if 'encrypted' in self.app_settings_type:
323 cipher = EncryptedTextValue()
323 cipher = EncryptedTextValue()
324 val = safe_unicode(cipher.process_bind_param(val, None))
324 val = safe_unicode(cipher.process_bind_param(val, None))
325 self._app_settings_value = val
325 self._app_settings_value = val
326
326
327 @hybrid_property
327 @hybrid_property
328 def app_settings_type(self):
328 def app_settings_type(self):
329 return self._app_settings_type
329 return self._app_settings_type
330
330
331 @app_settings_type.setter
331 @app_settings_type.setter
332 def app_settings_type(self, val):
332 def app_settings_type(self, val):
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 raise Exception('type must be one of %s got %s'
334 raise Exception('type must be one of %s got %s'
335 % (self.SETTINGS_TYPES.keys(), val))
335 % (self.SETTINGS_TYPES.keys(), val))
336 self._app_settings_type = val
336 self._app_settings_type = val
337
337
338 def __unicode__(self):
338 def __unicode__(self):
339 return u"<%s('%s:%s[%s]')>" % (
339 return u"<%s('%s:%s[%s]')>" % (
340 self.__class__.__name__,
340 self.__class__.__name__,
341 self.app_settings_name, self.app_settings_value,
341 self.app_settings_name, self.app_settings_value,
342 self.app_settings_type
342 self.app_settings_type
343 )
343 )
344
344
345
345
346 class RhodeCodeUi(Base, BaseModel):
346 class RhodeCodeUi(Base, BaseModel):
347 __tablename__ = 'rhodecode_ui'
347 __tablename__ = 'rhodecode_ui'
348 __table_args__ = (
348 __table_args__ = (
349 UniqueConstraint('ui_key'),
349 UniqueConstraint('ui_key'),
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 )
352 )
353
353
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 # HG
355 # HG
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PULL = 'outgoing.pull_logger'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 HOOK_PUSH = 'changegroup.push_logger'
360 HOOK_PUSH = 'changegroup.push_logger'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
362
362
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 # git part is currently hardcoded.
364 # git part is currently hardcoded.
365
365
366 # SVN PATTERNS
366 # SVN PATTERNS
367 SVN_BRANCH_ID = 'vcs_svn_branch'
367 SVN_BRANCH_ID = 'vcs_svn_branch'
368 SVN_TAG_ID = 'vcs_svn_tag'
368 SVN_TAG_ID = 'vcs_svn_tag'
369
369
370 ui_id = Column(
370 ui_id = Column(
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 primary_key=True)
372 primary_key=True)
373 ui_section = Column(
373 ui_section = Column(
374 "ui_section", String(255), nullable=True, unique=None, default=None)
374 "ui_section", String(255), nullable=True, unique=None, default=None)
375 ui_key = Column(
375 ui_key = Column(
376 "ui_key", String(255), nullable=True, unique=None, default=None)
376 "ui_key", String(255), nullable=True, unique=None, default=None)
377 ui_value = Column(
377 ui_value = Column(
378 "ui_value", String(255), nullable=True, unique=None, default=None)
378 "ui_value", String(255), nullable=True, unique=None, default=None)
379 ui_active = Column(
379 ui_active = Column(
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 self.ui_key, self.ui_value)
384 self.ui_key, self.ui_value)
385
385
386
386
387 class RepoRhodeCodeSetting(Base, BaseModel):
387 class RepoRhodeCodeSetting(Base, BaseModel):
388 __tablename__ = 'repo_rhodecode_settings'
388 __tablename__ = 'repo_rhodecode_settings'
389 __table_args__ = (
389 __table_args__ = (
390 UniqueConstraint(
390 UniqueConstraint(
391 'app_settings_name', 'repository_id',
391 'app_settings_name', 'repository_id',
392 name='uq_repo_rhodecode_setting_name_repo_id'),
392 name='uq_repo_rhodecode_setting_name_repo_id'),
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 )
395 )
396
396
397 repository_id = Column(
397 repository_id = Column(
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 nullable=False)
399 nullable=False)
400 app_settings_id = Column(
400 app_settings_id = Column(
401 "app_settings_id", Integer(), nullable=False, unique=True,
401 "app_settings_id", Integer(), nullable=False, unique=True,
402 default=None, primary_key=True)
402 default=None, primary_key=True)
403 app_settings_name = Column(
403 app_settings_name = Column(
404 "app_settings_name", String(255), nullable=True, unique=None,
404 "app_settings_name", String(255), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_value = Column(
406 _app_settings_value = Column(
407 "app_settings_value", String(4096), nullable=True, unique=None,
407 "app_settings_value", String(4096), nullable=True, unique=None,
408 default=None)
408 default=None)
409 _app_settings_type = Column(
409 _app_settings_type = Column(
410 "app_settings_type", String(255), nullable=True, unique=None,
410 "app_settings_type", String(255), nullable=True, unique=None,
411 default=None)
411 default=None)
412
412
413 repository = relationship('Repository')
413 repository = relationship('Repository')
414
414
415 def __init__(self, repository_id, key='', val='', type='unicode'):
415 def __init__(self, repository_id, key='', val='', type='unicode'):
416 self.repository_id = repository_id
416 self.repository_id = repository_id
417 self.app_settings_name = key
417 self.app_settings_name = key
418 self.app_settings_type = type
418 self.app_settings_type = type
419 self.app_settings_value = val
419 self.app_settings_value = val
420
420
421 @validates('_app_settings_value')
421 @validates('_app_settings_value')
422 def validate_settings_value(self, key, val):
422 def validate_settings_value(self, key, val):
423 assert type(val) == unicode
423 assert type(val) == unicode
424 return val
424 return val
425
425
426 @hybrid_property
426 @hybrid_property
427 def app_settings_value(self):
427 def app_settings_value(self):
428 v = self._app_settings_value
428 v = self._app_settings_value
429 type_ = self.app_settings_type
429 type_ = self.app_settings_type
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 return converter(v)
432 return converter(v)
433
433
434 @app_settings_value.setter
434 @app_settings_value.setter
435 def app_settings_value(self, val):
435 def app_settings_value(self, val):
436 """
436 """
437 Setter that will always make sure we use unicode in app_settings_value
437 Setter that will always make sure we use unicode in app_settings_value
438
438
439 :param val:
439 :param val:
440 """
440 """
441 self._app_settings_value = safe_unicode(val)
441 self._app_settings_value = safe_unicode(val)
442
442
443 @hybrid_property
443 @hybrid_property
444 def app_settings_type(self):
444 def app_settings_type(self):
445 return self._app_settings_type
445 return self._app_settings_type
446
446
447 @app_settings_type.setter
447 @app_settings_type.setter
448 def app_settings_type(self, val):
448 def app_settings_type(self, val):
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 if val not in SETTINGS_TYPES:
450 if val not in SETTINGS_TYPES:
451 raise Exception('type must be one of %s got %s'
451 raise Exception('type must be one of %s got %s'
452 % (SETTINGS_TYPES.keys(), val))
452 % (SETTINGS_TYPES.keys(), val))
453 self._app_settings_type = val
453 self._app_settings_type = val
454
454
455 def __unicode__(self):
455 def __unicode__(self):
456 return u"<%s('%s:%s:%s[%s]')>" % (
456 return u"<%s('%s:%s:%s[%s]')>" % (
457 self.__class__.__name__, self.repository.repo_name,
457 self.__class__.__name__, self.repository.repo_name,
458 self.app_settings_name, self.app_settings_value,
458 self.app_settings_name, self.app_settings_value,
459 self.app_settings_type
459 self.app_settings_type
460 )
460 )
461
461
462
462
463 class RepoRhodeCodeUi(Base, BaseModel):
463 class RepoRhodeCodeUi(Base, BaseModel):
464 __tablename__ = 'repo_rhodecode_ui'
464 __tablename__ = 'repo_rhodecode_ui'
465 __table_args__ = (
465 __table_args__ = (
466 UniqueConstraint(
466 UniqueConstraint(
467 'repository_id', 'ui_section', 'ui_key',
467 'repository_id', 'ui_section', 'ui_key',
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 )
471 )
472
472
473 repository_id = Column(
473 repository_id = Column(
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 nullable=False)
475 nullable=False)
476 ui_id = Column(
476 ui_id = Column(
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 primary_key=True)
478 primary_key=True)
479 ui_section = Column(
479 ui_section = Column(
480 "ui_section", String(255), nullable=True, unique=None, default=None)
480 "ui_section", String(255), nullable=True, unique=None, default=None)
481 ui_key = Column(
481 ui_key = Column(
482 "ui_key", String(255), nullable=True, unique=None, default=None)
482 "ui_key", String(255), nullable=True, unique=None, default=None)
483 ui_value = Column(
483 ui_value = Column(
484 "ui_value", String(255), nullable=True, unique=None, default=None)
484 "ui_value", String(255), nullable=True, unique=None, default=None)
485 ui_active = Column(
485 ui_active = Column(
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487
487
488 repository = relationship('Repository')
488 repository = relationship('Repository')
489
489
490 def __repr__(self):
490 def __repr__(self):
491 return '<%s[%s:%s]%s=>%s]>' % (
491 return '<%s[%s:%s]%s=>%s]>' % (
492 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
493 self.ui_section, self.ui_key, self.ui_value)
493 self.ui_section, self.ui_key, self.ui_value)
494
494
495
495
496 class User(Base, BaseModel):
496 class User(Base, BaseModel):
497 __tablename__ = 'users'
497 __tablename__ = 'users'
498 __table_args__ = (
498 __table_args__ = (
499 UniqueConstraint('username'), UniqueConstraint('email'),
499 UniqueConstraint('username'), UniqueConstraint('email'),
500 Index('u_username_idx', 'username'),
500 Index('u_username_idx', 'username'),
501 Index('u_email_idx', 'email'),
501 Index('u_email_idx', 'email'),
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 )
504 )
505 DEFAULT_USER = 'default'
505 DEFAULT_USER = 'default'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508
508
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519
519
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526
526
527 user_log = relationship('UserLog')
527 user_log = relationship('UserLog')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529
529
530 repositories = relationship('Repository')
530 repositories = relationship('Repository')
531 repository_groups = relationship('RepoGroup')
531 repository_groups = relationship('RepoGroup')
532 user_groups = relationship('UserGroup')
532 user_groups = relationship('UserGroup')
533
533
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536
536
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540
540
541 group_member = relationship('UserGroupMember', cascade='all')
541 group_member = relationship('UserGroupMember', cascade='all')
542
542
543 notifications = relationship('UserNotification', cascade='all')
543 notifications = relationship('UserNotification', cascade='all')
544 # notifications assigned to this user
544 # notifications assigned to this user
545 user_created_notifications = relationship('Notification', cascade='all')
545 user_created_notifications = relationship('Notification', cascade='all')
546 # comments created by this user
546 # comments created by this user
547 user_comments = relationship('ChangesetComment', cascade='all')
547 user_comments = relationship('ChangesetComment', cascade='all')
548 # user profile extra info
548 # user profile extra info
549 user_emails = relationship('UserEmailMap', cascade='all')
549 user_emails = relationship('UserEmailMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 # gists
552 # gists
553 user_gists = relationship('Gist', cascade='all')
553 user_gists = relationship('Gist', cascade='all')
554 # user pull requests
554 # user pull requests
555 user_pull_requests = relationship('PullRequest', cascade='all')
555 user_pull_requests = relationship('PullRequest', cascade='all')
556 # external identities
556 # external identities
557 extenal_identities = relationship(
557 extenal_identities = relationship(
558 'ExternalIdentity',
558 'ExternalIdentity',
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 cascade='all')
560 cascade='all')
561
561
562 def __unicode__(self):
562 def __unicode__(self):
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 self.user_id, self.username)
564 self.user_id, self.username)
565
565
566 @hybrid_property
566 @hybrid_property
567 def email(self):
567 def email(self):
568 return self._email
568 return self._email
569
569
570 @email.setter
570 @email.setter
571 def email(self, val):
571 def email(self, val):
572 self._email = val.lower() if val else None
572 self._email = val.lower() if val else None
573
573
574 @hybrid_property
574 @hybrid_property
575 def api_key(self):
575 def api_key(self):
576 """
576 """
577 Fetch if exist an auth-token with role ALL connected to this user
577 Fetch if exist an auth-token with role ALL connected to this user
578 """
578 """
579 user_auth_token = UserApiKeys.query()\
579 user_auth_token = UserApiKeys.query()\
580 .filter(UserApiKeys.user_id == self.user_id)\
580 .filter(UserApiKeys.user_id == self.user_id)\
581 .filter(or_(UserApiKeys.expires == -1,
581 .filter(or_(UserApiKeys.expires == -1,
582 UserApiKeys.expires >= time.time()))\
582 UserApiKeys.expires >= time.time()))\
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
584 if user_auth_token:
584 if user_auth_token:
585 user_auth_token = user_auth_token.api_key
585 user_auth_token = user_auth_token.api_key
586
586
587 return user_auth_token
587 return user_auth_token
588
588
589 @api_key.setter
589 @api_key.setter
590 def api_key(self, val):
590 def api_key(self, val):
591 # don't allow to set API key this is deprecated for now
591 # don't allow to set API key this is deprecated for now
592 self._api_key = None
592 self._api_key = None
593
593
594 @property
594 @property
595 def firstname(self):
595 def firstname(self):
596 # alias for future
596 # alias for future
597 return self.name
597 return self.name
598
598
599 @property
599 @property
600 def emails(self):
600 def emails(self):
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
602 return [self.email] + [x.email for x in other]
602 return [self.email] + [x.email for x in other]
603
603
604 @property
604 @property
605 def auth_tokens(self):
605 def auth_tokens(self):
606 return [x.api_key for x in self.extra_auth_tokens]
606 return [x.api_key for x in self.extra_auth_tokens]
607
607
608 @property
608 @property
609 def extra_auth_tokens(self):
609 def extra_auth_tokens(self):
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
611
611
612 @property
612 @property
613 def feed_token(self):
613 def feed_token(self):
614 return self.get_feed_token()
614 return self.get_feed_token()
615
615
616 def get_feed_token(self):
616 def get_feed_token(self):
617 feed_tokens = UserApiKeys.query()\
617 feed_tokens = UserApiKeys.query()\
618 .filter(UserApiKeys.user == self)\
618 .filter(UserApiKeys.user == self)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
620 .all()
620 .all()
621 if feed_tokens:
621 if feed_tokens:
622 return feed_tokens[0].api_key
622 return feed_tokens[0].api_key
623 return 'NO_FEED_TOKEN_AVAILABLE'
623 return 'NO_FEED_TOKEN_AVAILABLE'
624
624
625 @classmethod
625 @classmethod
626 def extra_valid_auth_tokens(cls, user, role=None):
626 def extra_valid_auth_tokens(cls, user, role=None):
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
628 .filter(or_(UserApiKeys.expires == -1,
628 .filter(or_(UserApiKeys.expires == -1,
629 UserApiKeys.expires >= time.time()))
629 UserApiKeys.expires >= time.time()))
630 if role:
630 if role:
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
633 return tokens.all()
633 return tokens.all()
634
634
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
636 from rhodecode.lib import auth
636 from rhodecode.lib import auth
637
637
638 log.debug('Trying to authenticate user: %s via auth-token, '
638 log.debug('Trying to authenticate user: %s via auth-token, '
639 'and roles: %s', self, roles)
639 'and roles: %s', self, roles)
640
640
641 if not auth_token:
641 if not auth_token:
642 return False
642 return False
643
643
644 crypto_backend = auth.crypto_backend()
644 crypto_backend = auth.crypto_backend()
645
645
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
647 tokens_q = UserApiKeys.query()\
647 tokens_q = UserApiKeys.query()\
648 .filter(UserApiKeys.user_id == self.user_id)\
648 .filter(UserApiKeys.user_id == self.user_id)\
649 .filter(or_(UserApiKeys.expires == -1,
649 .filter(or_(UserApiKeys.expires == -1,
650 UserApiKeys.expires >= time.time()))
650 UserApiKeys.expires >= time.time()))
651
651
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
653
653
654 plain_tokens = []
654 plain_tokens = []
655 hash_tokens = []
655 hash_tokens = []
656
656
657 for token in tokens_q.all():
657 for token in tokens_q.all():
658 # verify scope first
658 # verify scope first
659 if token.repo_id:
659 if token.repo_id:
660 # token has a scope, we need to verify it
660 # token has a scope, we need to verify it
661 if scope_repo_id != token.repo_id:
661 if scope_repo_id != token.repo_id:
662 log.debug(
662 log.debug(
663 'Scope mismatch: token has a set repo scope: %s, '
663 'Scope mismatch: token has a set repo scope: %s, '
664 'and calling scope is:%s, skipping further checks',
664 'and calling scope is:%s, skipping further checks',
665 token.repo, scope_repo_id)
665 token.repo, scope_repo_id)
666 # token has a scope, and it doesn't match, skip token
666 # token has a scope, and it doesn't match, skip token
667 continue
667 continue
668
668
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
670 hash_tokens.append(token.api_key)
670 hash_tokens.append(token.api_key)
671 else:
671 else:
672 plain_tokens.append(token.api_key)
672 plain_tokens.append(token.api_key)
673
673
674 is_plain_match = auth_token in plain_tokens
674 is_plain_match = auth_token in plain_tokens
675 if is_plain_match:
675 if is_plain_match:
676 return True
676 return True
677
677
678 for hashed in hash_tokens:
678 for hashed in hash_tokens:
679 # TODO(marcink): this is expensive to calculate, but most secure
679 # TODO(marcink): this is expensive to calculate, but most secure
680 match = crypto_backend.hash_check(auth_token, hashed)
680 match = crypto_backend.hash_check(auth_token, hashed)
681 if match:
681 if match:
682 return True
682 return True
683
683
684 return False
684 return False
685
685
686 @property
686 @property
687 def ip_addresses(self):
687 def ip_addresses(self):
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
689 return [x.ip_addr for x in ret]
689 return [x.ip_addr for x in ret]
690
690
691 @property
691 @property
692 def username_and_name(self):
692 def username_and_name(self):
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
694
694
695 @property
695 @property
696 def username_or_name_or_email(self):
696 def username_or_name_or_email(self):
697 full_name = self.full_name if self.full_name is not ' ' else None
697 full_name = self.full_name if self.full_name is not ' ' else None
698 return self.username or full_name or self.email
698 return self.username or full_name or self.email
699
699
700 @property
700 @property
701 def full_name(self):
701 def full_name(self):
702 return '%s %s' % (self.firstname, self.lastname)
702 return '%s %s' % (self.firstname, self.lastname)
703
703
704 @property
704 @property
705 def full_name_or_username(self):
705 def full_name_or_username(self):
706 return ('%s %s' % (self.firstname, self.lastname)
706 return ('%s %s' % (self.firstname, self.lastname)
707 if (self.firstname and self.lastname) else self.username)
707 if (self.firstname and self.lastname) else self.username)
708
708
709 @property
709 @property
710 def full_contact(self):
710 def full_contact(self):
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
712
712
713 @property
713 @property
714 def short_contact(self):
714 def short_contact(self):
715 return '%s %s' % (self.firstname, self.lastname)
715 return '%s %s' % (self.firstname, self.lastname)
716
716
717 @property
717 @property
718 def is_admin(self):
718 def is_admin(self):
719 return self.admin
719 return self.admin
720
720
721 @property
721 @property
722 def AuthUser(self):
722 def AuthUser(self):
723 """
723 """
724 Returns instance of AuthUser for this user
724 Returns instance of AuthUser for this user
725 """
725 """
726 from rhodecode.lib.auth import AuthUser
726 from rhodecode.lib.auth import AuthUser
727 return AuthUser(user_id=self.user_id, username=self.username)
727 return AuthUser(user_id=self.user_id, username=self.username)
728
728
729 @hybrid_property
729 @hybrid_property
730 def user_data(self):
730 def user_data(self):
731 if not self._user_data:
731 if not self._user_data:
732 return {}
732 return {}
733
733
734 try:
734 try:
735 return json.loads(self._user_data)
735 return json.loads(self._user_data)
736 except TypeError:
736 except TypeError:
737 return {}
737 return {}
738
738
739 @user_data.setter
739 @user_data.setter
740 def user_data(self, val):
740 def user_data(self, val):
741 if not isinstance(val, dict):
741 if not isinstance(val, dict):
742 raise Exception('user_data must be dict, got %s' % type(val))
742 raise Exception('user_data must be dict, got %s' % type(val))
743 try:
743 try:
744 self._user_data = json.dumps(val)
744 self._user_data = json.dumps(val)
745 except Exception:
745 except Exception:
746 log.error(traceback.format_exc())
746 log.error(traceback.format_exc())
747
747
748 @classmethod
748 @classmethod
749 def get_by_username(cls, username, case_insensitive=False,
749 def get_by_username(cls, username, case_insensitive=False,
750 cache=False, identity_cache=False):
750 cache=False, identity_cache=False):
751 session = Session()
751 session = Session()
752
752
753 if case_insensitive:
753 if case_insensitive:
754 q = cls.query().filter(
754 q = cls.query().filter(
755 func.lower(cls.username) == func.lower(username))
755 func.lower(cls.username) == func.lower(username))
756 else:
756 else:
757 q = cls.query().filter(cls.username == username)
757 q = cls.query().filter(cls.username == username)
758
758
759 if cache:
759 if cache:
760 if identity_cache:
760 if identity_cache:
761 val = cls.identity_cache(session, 'username', username)
761 val = cls.identity_cache(session, 'username', username)
762 if val:
762 if val:
763 return val
763 return val
764 else:
764 else:
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
766 q = q.options(
766 q = q.options(
767 FromCache("sql_cache_short", cache_key))
767 FromCache("sql_cache_short", cache_key))
768
768
769 return q.scalar()
769 return q.scalar()
770
770
771 @classmethod
771 @classmethod
772 def get_by_auth_token(cls, auth_token, cache=False):
772 def get_by_auth_token(cls, auth_token, cache=False):
773 q = UserApiKeys.query()\
773 q = UserApiKeys.query()\
774 .filter(UserApiKeys.api_key == auth_token)\
774 .filter(UserApiKeys.api_key == auth_token)\
775 .filter(or_(UserApiKeys.expires == -1,
775 .filter(or_(UserApiKeys.expires == -1,
776 UserApiKeys.expires >= time.time()))
776 UserApiKeys.expires >= time.time()))
777 if cache:
777 if cache:
778 q = q.options(
778 q = q.options(
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
780
780
781 match = q.first()
781 match = q.first()
782 if match:
782 if match:
783 return match.user
783 return match.user
784
784
785 @classmethod
785 @classmethod
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
787
787
788 if case_insensitive:
788 if case_insensitive:
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
790
790
791 else:
791 else:
792 q = cls.query().filter(cls.email == email)
792 q = cls.query().filter(cls.email == email)
793
793
794 email_key = _hash_key(email)
794 email_key = _hash_key(email)
795 if cache:
795 if cache:
796 q = q.options(
796 q = q.options(
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
798
798
799 ret = q.scalar()
799 ret = q.scalar()
800 if ret is None:
800 if ret is None:
801 q = UserEmailMap.query()
801 q = UserEmailMap.query()
802 # try fetching in alternate email map
802 # try fetching in alternate email map
803 if case_insensitive:
803 if case_insensitive:
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
805 else:
805 else:
806 q = q.filter(UserEmailMap.email == email)
806 q = q.filter(UserEmailMap.email == email)
807 q = q.options(joinedload(UserEmailMap.user))
807 q = q.options(joinedload(UserEmailMap.user))
808 if cache:
808 if cache:
809 q = q.options(
809 q = q.options(
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
811 ret = getattr(q.scalar(), 'user', None)
811 ret = getattr(q.scalar(), 'user', None)
812
812
813 return ret
813 return ret
814
814
815 @classmethod
815 @classmethod
816 def get_from_cs_author(cls, author):
816 def get_from_cs_author(cls, author):
817 """
817 """
818 Tries to get User objects out of commit author string
818 Tries to get User objects out of commit author string
819
819
820 :param author:
820 :param author:
821 """
821 """
822 from rhodecode.lib.helpers import email, author_name
822 from rhodecode.lib.helpers import email, author_name
823 # Valid email in the attribute passed, see if they're in the system
823 # Valid email in the attribute passed, see if they're in the system
824 _email = email(author)
824 _email = email(author)
825 if _email:
825 if _email:
826 user = cls.get_by_email(_email, case_insensitive=True)
826 user = cls.get_by_email(_email, case_insensitive=True)
827 if user:
827 if user:
828 return user
828 return user
829 # Maybe we can match by username?
829 # Maybe we can match by username?
830 _author = author_name(author)
830 _author = author_name(author)
831 user = cls.get_by_username(_author, case_insensitive=True)
831 user = cls.get_by_username(_author, case_insensitive=True)
832 if user:
832 if user:
833 return user
833 return user
834
834
835 def update_userdata(self, **kwargs):
835 def update_userdata(self, **kwargs):
836 usr = self
836 usr = self
837 old = usr.user_data
837 old = usr.user_data
838 old.update(**kwargs)
838 old.update(**kwargs)
839 usr.user_data = old
839 usr.user_data = old
840 Session().add(usr)
840 Session().add(usr)
841 log.debug('updated userdata with ', kwargs)
841 log.debug('updated userdata with ', kwargs)
842
842
843 def update_lastlogin(self):
843 def update_lastlogin(self):
844 """Update user lastlogin"""
844 """Update user lastlogin"""
845 self.last_login = datetime.datetime.now()
845 self.last_login = datetime.datetime.now()
846 Session().add(self)
846 Session().add(self)
847 log.debug('updated user %s lastlogin', self.username)
847 log.debug('updated user %s lastlogin', self.username)
848
848
849 def update_lastactivity(self):
849 def update_lastactivity(self):
850 """Update user lastactivity"""
850 """Update user lastactivity"""
851 self.last_activity = datetime.datetime.now()
851 self.last_activity = datetime.datetime.now()
852 Session().add(self)
852 Session().add(self)
853 log.debug('updated user %s lastactivity', self.username)
853 log.debug('updated user %s lastactivity', self.username)
854
854
855 def update_password(self, new_password):
855 def update_password(self, new_password):
856 from rhodecode.lib.auth import get_crypt_password
856 from rhodecode.lib.auth import get_crypt_password
857
857
858 self.password = get_crypt_password(new_password)
858 self.password = get_crypt_password(new_password)
859 Session().add(self)
859 Session().add(self)
860
860
861 @classmethod
861 @classmethod
862 def get_first_super_admin(cls):
862 def get_first_super_admin(cls):
863 user = User.query().filter(User.admin == true()).first()
863 user = User.query().filter(User.admin == true()).first()
864 if user is None:
864 if user is None:
865 raise Exception('FATAL: Missing administrative account!')
865 raise Exception('FATAL: Missing administrative account!')
866 return user
866 return user
867
867
868 @classmethod
868 @classmethod
869 def get_all_super_admins(cls):
869 def get_all_super_admins(cls):
870 """
870 """
871 Returns all admin accounts sorted by username
871 Returns all admin accounts sorted by username
872 """
872 """
873 return User.query().filter(User.admin == true())\
873 return User.query().filter(User.admin == true())\
874 .order_by(User.username.asc()).all()
874 .order_by(User.username.asc()).all()
875
875
876 @classmethod
876 @classmethod
877 def get_default_user(cls, cache=False, refresh=False):
877 def get_default_user(cls, cache=False, refresh=False):
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
879 if user is None:
879 if user is None:
880 raise Exception('FATAL: Missing default account!')
880 raise Exception('FATAL: Missing default account!')
881 if refresh:
881 if refresh:
882 # The default user might be based on outdated state which
882 # The default user might be based on outdated state which
883 # has been loaded from the cache.
883 # has been loaded from the cache.
884 # A call to refresh() ensures that the
884 # A call to refresh() ensures that the
885 # latest state from the database is used.
885 # latest state from the database is used.
886 Session().refresh(user)
886 Session().refresh(user)
887 return user
887 return user
888
888
889 def _get_default_perms(self, user, suffix=''):
889 def _get_default_perms(self, user, suffix=''):
890 from rhodecode.model.permission import PermissionModel
890 from rhodecode.model.permission import PermissionModel
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
892
892
893 def get_default_perms(self, suffix=''):
893 def get_default_perms(self, suffix=''):
894 return self._get_default_perms(self, suffix)
894 return self._get_default_perms(self, suffix)
895
895
896 def get_api_data(self, include_secrets=False, details='full'):
896 def get_api_data(self, include_secrets=False, details='full'):
897 """
897 """
898 Common function for generating user related data for API
898 Common function for generating user related data for API
899
899
900 :param include_secrets: By default secrets in the API data will be replaced
900 :param include_secrets: By default secrets in the API data will be replaced
901 by a placeholder value to prevent exposing this data by accident. In case
901 by a placeholder value to prevent exposing this data by accident. In case
902 this data shall be exposed, set this flag to ``True``.
902 this data shall be exposed, set this flag to ``True``.
903
903
904 :param details: details can be 'basic|full' basic gives only a subset of
904 :param details: details can be 'basic|full' basic gives only a subset of
905 the available user information that includes user_id, name and emails.
905 the available user information that includes user_id, name and emails.
906 """
906 """
907 user = self
907 user = self
908 user_data = self.user_data
908 user_data = self.user_data
909 data = {
909 data = {
910 'user_id': user.user_id,
910 'user_id': user.user_id,
911 'username': user.username,
911 'username': user.username,
912 'firstname': user.name,
912 'firstname': user.name,
913 'lastname': user.lastname,
913 'lastname': user.lastname,
914 'email': user.email,
914 'email': user.email,
915 'emails': user.emails,
915 'emails': user.emails,
916 }
916 }
917 if details == 'basic':
917 if details == 'basic':
918 return data
918 return data
919
919
920 api_key_length = 40
920 api_key_length = 40
921 api_key_replacement = '*' * api_key_length
921 api_key_replacement = '*' * api_key_length
922
922
923 extras = {
923 extras = {
924 'api_keys': [api_key_replacement],
924 'api_keys': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
926 'active': user.active,
926 'active': user.active,
927 'admin': user.admin,
927 'admin': user.admin,
928 'extern_type': user.extern_type,
928 'extern_type': user.extern_type,
929 'extern_name': user.extern_name,
929 'extern_name': user.extern_name,
930 'last_login': user.last_login,
930 'last_login': user.last_login,
931 'last_activity': user.last_activity,
931 'last_activity': user.last_activity,
932 'ip_addresses': user.ip_addresses,
932 'ip_addresses': user.ip_addresses,
933 'language': user_data.get('language')
933 'language': user_data.get('language')
934 }
934 }
935 data.update(extras)
935 data.update(extras)
936
936
937 if include_secrets:
937 if include_secrets:
938 data['api_keys'] = user.auth_tokens
938 data['api_keys'] = user.auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
940 return data
940 return data
941
941
942 def __json__(self):
942 def __json__(self):
943 data = {
943 data = {
944 'full_name': self.full_name,
944 'full_name': self.full_name,
945 'full_name_or_username': self.full_name_or_username,
945 'full_name_or_username': self.full_name_or_username,
946 'short_contact': self.short_contact,
946 'short_contact': self.short_contact,
947 'full_contact': self.full_contact,
947 'full_contact': self.full_contact,
948 }
948 }
949 data.update(self.get_api_data())
949 data.update(self.get_api_data())
950 return data
950 return data
951
951
952
952
953 class UserApiKeys(Base, BaseModel):
953 class UserApiKeys(Base, BaseModel):
954 __tablename__ = 'user_api_keys'
954 __tablename__ = 'user_api_keys'
955 __table_args__ = (
955 __table_args__ = (
956 Index('uak_api_key_idx', 'api_key'),
956 Index('uak_api_key_idx', 'api_key'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
958 UniqueConstraint('api_key'),
958 UniqueConstraint('api_key'),
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
961 )
961 )
962 __mapper_args__ = {}
962 __mapper_args__ = {}
963
963
964 # ApiKey role
964 # ApiKey role
965 ROLE_ALL = 'token_role_all'
965 ROLE_ALL = 'token_role_all'
966 ROLE_HTTP = 'token_role_http'
966 ROLE_HTTP = 'token_role_http'
967 ROLE_VCS = 'token_role_vcs'
967 ROLE_VCS = 'token_role_vcs'
968 ROLE_API = 'token_role_api'
968 ROLE_API = 'token_role_api'
969 ROLE_FEED = 'token_role_feed'
969 ROLE_FEED = 'token_role_feed'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
971
971
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
973
973
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
978 expires = Column('expires', Float(53), nullable=False)
978 expires = Column('expires', Float(53), nullable=False)
979 role = Column('role', String(255), nullable=True)
979 role = Column('role', String(255), nullable=True)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981
981
982 # scope columns
982 # scope columns
983 repo_id = Column(
983 repo_id = Column(
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
985 nullable=True, unique=None, default=None)
985 nullable=True, unique=None, default=None)
986 repo = relationship('Repository', lazy='joined')
986 repo = relationship('Repository', lazy='joined')
987
987
988 repo_group_id = Column(
988 repo_group_id = Column(
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
990 nullable=True, unique=None, default=None)
990 nullable=True, unique=None, default=None)
991 repo_group = relationship('RepoGroup', lazy='joined')
991 repo_group = relationship('RepoGroup', lazy='joined')
992
992
993 user = relationship('User', lazy='joined')
993 user = relationship('User', lazy='joined')
994
994
995 def __unicode__(self):
995 def __unicode__(self):
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
997
997
998 def __json__(self):
998 def __json__(self):
999 data = {
999 data = {
1000 'auth_token': self.api_key,
1000 'auth_token': self.api_key,
1001 'role': self.role,
1001 'role': self.role,
1002 'scope': self.scope_humanized,
1002 'scope': self.scope_humanized,
1003 'expired': self.expired
1003 'expired': self.expired
1004 }
1004 }
1005 return data
1005 return data
1006
1006
1007 @property
1007 @property
1008 def expired(self):
1008 def expired(self):
1009 if self.expires == -1:
1009 if self.expires == -1:
1010 return False
1010 return False
1011 return time.time() > self.expires
1011 return time.time() > self.expires
1012
1012
1013 @classmethod
1013 @classmethod
1014 def _get_role_name(cls, role):
1014 def _get_role_name(cls, role):
1015 return {
1015 return {
1016 cls.ROLE_ALL: _('all'),
1016 cls.ROLE_ALL: _('all'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1019 cls.ROLE_API: _('api calls'),
1019 cls.ROLE_API: _('api calls'),
1020 cls.ROLE_FEED: _('feed access'),
1020 cls.ROLE_FEED: _('feed access'),
1021 }.get(role, role)
1021 }.get(role, role)
1022
1022
1023 @property
1023 @property
1024 def role_humanized(self):
1024 def role_humanized(self):
1025 return self._get_role_name(self.role)
1025 return self._get_role_name(self.role)
1026
1026
1027 def _get_scope(self):
1027 def _get_scope(self):
1028 if self.repo:
1028 if self.repo:
1029 return repr(self.repo)
1029 return repr(self.repo)
1030 if self.repo_group:
1030 if self.repo_group:
1031 return repr(self.repo_group) + ' (recursive)'
1031 return repr(self.repo_group) + ' (recursive)'
1032 return 'global'
1032 return 'global'
1033
1033
1034 @property
1034 @property
1035 def scope_humanized(self):
1035 def scope_humanized(self):
1036 return self._get_scope()
1036 return self._get_scope()
1037
1037
1038
1038
1039 class UserEmailMap(Base, BaseModel):
1039 class UserEmailMap(Base, BaseModel):
1040 __tablename__ = 'user_email_map'
1040 __tablename__ = 'user_email_map'
1041 __table_args__ = (
1041 __table_args__ = (
1042 Index('uem_email_idx', 'email'),
1042 Index('uem_email_idx', 'email'),
1043 UniqueConstraint('email'),
1043 UniqueConstraint('email'),
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1046 )
1046 )
1047 __mapper_args__ = {}
1047 __mapper_args__ = {}
1048
1048
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1052 user = relationship('User', lazy='joined')
1052 user = relationship('User', lazy='joined')
1053
1053
1054 @validates('_email')
1054 @validates('_email')
1055 def validate_email(self, key, email):
1055 def validate_email(self, key, email):
1056 # check if this email is not main one
1056 # check if this email is not main one
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1058 if main_email is not None:
1058 if main_email is not None:
1059 raise AttributeError('email %s is present is user table' % email)
1059 raise AttributeError('email %s is present is user table' % email)
1060 return email
1060 return email
1061
1061
1062 @hybrid_property
1062 @hybrid_property
1063 def email(self):
1063 def email(self):
1064 return self._email
1064 return self._email
1065
1065
1066 @email.setter
1066 @email.setter
1067 def email(self, val):
1067 def email(self, val):
1068 self._email = val.lower() if val else None
1068 self._email = val.lower() if val else None
1069
1069
1070
1070
1071 class UserIpMap(Base, BaseModel):
1071 class UserIpMap(Base, BaseModel):
1072 __tablename__ = 'user_ip_map'
1072 __tablename__ = 'user_ip_map'
1073 __table_args__ = (
1073 __table_args__ = (
1074 UniqueConstraint('user_id', 'ip_addr'),
1074 UniqueConstraint('user_id', 'ip_addr'),
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 )
1077 )
1078 __mapper_args__ = {}
1078 __mapper_args__ = {}
1079
1079
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1085 user = relationship('User', lazy='joined')
1085 user = relationship('User', lazy='joined')
1086
1086
1087 @classmethod
1087 @classmethod
1088 def _get_ip_range(cls, ip_addr):
1088 def _get_ip_range(cls, ip_addr):
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1090 return [str(net.network_address), str(net.broadcast_address)]
1090 return [str(net.network_address), str(net.broadcast_address)]
1091
1091
1092 def __json__(self):
1092 def __json__(self):
1093 return {
1093 return {
1094 'ip_addr': self.ip_addr,
1094 'ip_addr': self.ip_addr,
1095 'ip_range': self._get_ip_range(self.ip_addr),
1095 'ip_range': self._get_ip_range(self.ip_addr),
1096 }
1096 }
1097
1097
1098 def __unicode__(self):
1098 def __unicode__(self):
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1100 self.user_id, self.ip_addr)
1100 self.user_id, self.ip_addr)
1101
1101
1102
1102
1103 class UserLog(Base, BaseModel):
1103 class UserLog(Base, BaseModel):
1104 __tablename__ = 'user_logs'
1104 __tablename__ = 'user_logs'
1105 __table_args__ = (
1105 __table_args__ = (
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1108 )
1108 )
1109 VERSION_1 = 'v1'
1109 VERSION_1 = 'v1'
1110 VERSION_2 = 'v2'
1110 VERSION_2 = 'v2'
1111 VERSIONS = [VERSION_1, VERSION_2]
1111 VERSIONS = [VERSION_1, VERSION_2]
1112
1112
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1121
1121
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1125
1125
1126 def __unicode__(self):
1126 def __unicode__(self):
1127 return u"<%s('id:%s:%s')>" % (
1127 return u"<%s('id:%s:%s')>" % (
1128 self.__class__.__name__, self.repository_name, self.action)
1128 self.__class__.__name__, self.repository_name, self.action)
1129
1129
1130 def __json__(self):
1130 def __json__(self):
1131 return {
1131 return {
1132 'user_id': self.user_id,
1132 'user_id': self.user_id,
1133 'username': self.username,
1133 'username': self.username,
1134 'repository_id': self.repository_id,
1134 'repository_id': self.repository_id,
1135 'repository_name': self.repository_name,
1135 'repository_name': self.repository_name,
1136 'user_ip': self.user_ip,
1136 'user_ip': self.user_ip,
1137 'action_date': self.action_date,
1137 'action_date': self.action_date,
1138 'action': self.action,
1138 'action': self.action,
1139 }
1139 }
1140
1140
1141 @property
1141 @property
1142 def action_as_day(self):
1142 def action_as_day(self):
1143 return datetime.date(*self.action_date.timetuple()[:3])
1143 return datetime.date(*self.action_date.timetuple()[:3])
1144
1144
1145 user = relationship('User')
1145 user = relationship('User')
1146 repository = relationship('Repository', cascade='')
1146 repository = relationship('Repository', cascade='')
1147
1147
1148
1148
1149 class UserGroup(Base, BaseModel):
1149 class UserGroup(Base, BaseModel):
1150 __tablename__ = 'users_groups'
1150 __tablename__ = 'users_groups'
1151 __table_args__ = (
1151 __table_args__ = (
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1154 )
1154 )
1155
1155
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1164
1164
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1171
1171
1172 user = relationship('User')
1172 user = relationship('User')
1173
1173
1174 @hybrid_property
1174 @hybrid_property
1175 def group_data(self):
1175 def group_data(self):
1176 if not self._group_data:
1176 if not self._group_data:
1177 return {}
1177 return {}
1178
1178
1179 try:
1179 try:
1180 return json.loads(self._group_data)
1180 return json.loads(self._group_data)
1181 except TypeError:
1181 except TypeError:
1182 return {}
1182 return {}
1183
1183
1184 @group_data.setter
1184 @group_data.setter
1185 def group_data(self, val):
1185 def group_data(self, val):
1186 try:
1186 try:
1187 self._group_data = json.dumps(val)
1187 self._group_data = json.dumps(val)
1188 except Exception:
1188 except Exception:
1189 log.error(traceback.format_exc())
1189 log.error(traceback.format_exc())
1190
1190
1191 def __unicode__(self):
1191 def __unicode__(self):
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1193 self.users_group_id,
1193 self.users_group_id,
1194 self.users_group_name)
1194 self.users_group_name)
1195
1195
1196 @classmethod
1196 @classmethod
1197 def get_by_group_name(cls, group_name, cache=False,
1197 def get_by_group_name(cls, group_name, cache=False,
1198 case_insensitive=False):
1198 case_insensitive=False):
1199 if case_insensitive:
1199 if case_insensitive:
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1201 func.lower(group_name))
1201 func.lower(group_name))
1202
1202
1203 else:
1203 else:
1204 q = cls.query().filter(cls.users_group_name == group_name)
1204 q = cls.query().filter(cls.users_group_name == group_name)
1205 if cache:
1205 if cache:
1206 q = q.options(
1206 q = q.options(
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1208 return q.scalar()
1208 return q.scalar()
1209
1209
1210 @classmethod
1210 @classmethod
1211 def get(cls, user_group_id, cache=False):
1211 def get(cls, user_group_id, cache=False):
1212 user_group = cls.query()
1212 user_group = cls.query()
1213 if cache:
1213 if cache:
1214 user_group = user_group.options(
1214 user_group = user_group.options(
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1216 return user_group.get(user_group_id)
1216 return user_group.get(user_group_id)
1217
1217
1218 def permissions(self, with_admins=True, with_owner=True):
1218 def permissions(self, with_admins=True, with_owner=True):
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1221 joinedload(UserUserGroupToPerm.user),
1221 joinedload(UserUserGroupToPerm.user),
1222 joinedload(UserUserGroupToPerm.permission),)
1222 joinedload(UserUserGroupToPerm.permission),)
1223
1223
1224 # get owners and admins and permissions. We do a trick of re-writing
1224 # get owners and admins and permissions. We do a trick of re-writing
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1226 # has a global reference and changing one object propagates to all
1226 # has a global reference and changing one object propagates to all
1227 # others. This means if admin is also an owner admin_row that change
1227 # others. This means if admin is also an owner admin_row that change
1228 # would propagate to both objects
1228 # would propagate to both objects
1229 perm_rows = []
1229 perm_rows = []
1230 for _usr in q.all():
1230 for _usr in q.all():
1231 usr = AttributeDict(_usr.user.get_dict())
1231 usr = AttributeDict(_usr.user.get_dict())
1232 usr.permission = _usr.permission.permission_name
1232 usr.permission = _usr.permission.permission_name
1233 perm_rows.append(usr)
1233 perm_rows.append(usr)
1234
1234
1235 # filter the perm rows by 'default' first and then sort them by
1235 # filter the perm rows by 'default' first and then sort them by
1236 # admin,write,read,none permissions sorted again alphabetically in
1236 # admin,write,read,none permissions sorted again alphabetically in
1237 # each group
1237 # each group
1238 perm_rows = sorted(perm_rows, key=display_sort)
1238 perm_rows = sorted(perm_rows, key=display_sort)
1239
1239
1240 _admin_perm = 'usergroup.admin'
1240 _admin_perm = 'usergroup.admin'
1241 owner_row = []
1241 owner_row = []
1242 if with_owner:
1242 if with_owner:
1243 usr = AttributeDict(self.user.get_dict())
1243 usr = AttributeDict(self.user.get_dict())
1244 usr.owner_row = True
1244 usr.owner_row = True
1245 usr.permission = _admin_perm
1245 usr.permission = _admin_perm
1246 owner_row.append(usr)
1246 owner_row.append(usr)
1247
1247
1248 super_admin_rows = []
1248 super_admin_rows = []
1249 if with_admins:
1249 if with_admins:
1250 for usr in User.get_all_super_admins():
1250 for usr in User.get_all_super_admins():
1251 # if this admin is also owner, don't double the record
1251 # if this admin is also owner, don't double the record
1252 if usr.user_id == owner_row[0].user_id:
1252 if usr.user_id == owner_row[0].user_id:
1253 owner_row[0].admin_row = True
1253 owner_row[0].admin_row = True
1254 else:
1254 else:
1255 usr = AttributeDict(usr.get_dict())
1255 usr = AttributeDict(usr.get_dict())
1256 usr.admin_row = True
1256 usr.admin_row = True
1257 usr.permission = _admin_perm
1257 usr.permission = _admin_perm
1258 super_admin_rows.append(usr)
1258 super_admin_rows.append(usr)
1259
1259
1260 return super_admin_rows + owner_row + perm_rows
1260 return super_admin_rows + owner_row + perm_rows
1261
1261
1262 def permission_user_groups(self):
1262 def permission_user_groups(self):
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1267
1267
1268 perm_rows = []
1268 perm_rows = []
1269 for _user_group in q.all():
1269 for _user_group in q.all():
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1271 usr.permission = _user_group.permission.permission_name
1271 usr.permission = _user_group.permission.permission_name
1272 perm_rows.append(usr)
1272 perm_rows.append(usr)
1273
1273
1274 return perm_rows
1274 return perm_rows
1275
1275
1276 def _get_default_perms(self, user_group, suffix=''):
1276 def _get_default_perms(self, user_group, suffix=''):
1277 from rhodecode.model.permission import PermissionModel
1277 from rhodecode.model.permission import PermissionModel
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1279
1279
1280 def get_default_perms(self, suffix=''):
1280 def get_default_perms(self, suffix=''):
1281 return self._get_default_perms(self, suffix)
1281 return self._get_default_perms(self, suffix)
1282
1282
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1284 """
1284 """
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1286 basically forwarded.
1286 basically forwarded.
1287
1287
1288 """
1288 """
1289 user_group = self
1289 user_group = self
1290 data = {
1290 data = {
1291 'users_group_id': user_group.users_group_id,
1291 'users_group_id': user_group.users_group_id,
1292 'group_name': user_group.users_group_name,
1292 'group_name': user_group.users_group_name,
1293 'group_description': user_group.user_group_description,
1293 'group_description': user_group.user_group_description,
1294 'active': user_group.users_group_active,
1294 'active': user_group.users_group_active,
1295 'owner': user_group.user.username,
1295 'owner': user_group.user.username,
1296 'owner_email': user_group.user.email,
1296 'owner_email': user_group.user.email,
1297 }
1297 }
1298
1298
1299 if with_group_members:
1299 if with_group_members:
1300 users = []
1300 users = []
1301 for user in user_group.members:
1301 for user in user_group.members:
1302 user = user.user
1302 user = user.user
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1304 data['users'] = users
1304 data['users'] = users
1305
1305
1306 return data
1306 return data
1307
1307
1308
1308
1309 class UserGroupMember(Base, BaseModel):
1309 class UserGroupMember(Base, BaseModel):
1310 __tablename__ = 'users_groups_members'
1310 __tablename__ = 'users_groups_members'
1311 __table_args__ = (
1311 __table_args__ = (
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1314 )
1314 )
1315
1315
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1319
1319
1320 user = relationship('User', lazy='joined')
1320 user = relationship('User', lazy='joined')
1321 users_group = relationship('UserGroup')
1321 users_group = relationship('UserGroup')
1322
1322
1323 def __init__(self, gr_id='', u_id=''):
1323 def __init__(self, gr_id='', u_id=''):
1324 self.users_group_id = gr_id
1324 self.users_group_id = gr_id
1325 self.user_id = u_id
1325 self.user_id = u_id
1326
1326
1327
1327
1328 class RepositoryField(Base, BaseModel):
1328 class RepositoryField(Base, BaseModel):
1329 __tablename__ = 'repositories_fields'
1329 __tablename__ = 'repositories_fields'
1330 __table_args__ = (
1330 __table_args__ = (
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1334 )
1334 )
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1336
1336
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1339 field_key = Column("field_key", String(250))
1339 field_key = Column("field_key", String(250))
1340 field_label = Column("field_label", String(1024), nullable=False)
1340 field_label = Column("field_label", String(1024), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1345
1345
1346 repository = relationship('Repository')
1346 repository = relationship('Repository')
1347
1347
1348 @property
1348 @property
1349 def field_key_prefixed(self):
1349 def field_key_prefixed(self):
1350 return 'ex_%s' % self.field_key
1350 return 'ex_%s' % self.field_key
1351
1351
1352 @classmethod
1352 @classmethod
1353 def un_prefix_key(cls, key):
1353 def un_prefix_key(cls, key):
1354 if key.startswith(cls.PREFIX):
1354 if key.startswith(cls.PREFIX):
1355 return key[len(cls.PREFIX):]
1355 return key[len(cls.PREFIX):]
1356 return key
1356 return key
1357
1357
1358 @classmethod
1358 @classmethod
1359 def get_by_key_name(cls, key, repo):
1359 def get_by_key_name(cls, key, repo):
1360 row = cls.query()\
1360 row = cls.query()\
1361 .filter(cls.repository == repo)\
1361 .filter(cls.repository == repo)\
1362 .filter(cls.field_key == key).scalar()
1362 .filter(cls.field_key == key).scalar()
1363 return row
1363 return row
1364
1364
1365
1365
1366 class Repository(Base, BaseModel):
1366 class Repository(Base, BaseModel):
1367 __tablename__ = 'repositories'
1367 __tablename__ = 'repositories'
1368 __table_args__ = (
1368 __table_args__ = (
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1372 )
1372 )
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1375
1375
1376 STATE_CREATED = 'repo_state_created'
1376 STATE_CREATED = 'repo_state_created'
1377 STATE_PENDING = 'repo_state_pending'
1377 STATE_PENDING = 'repo_state_pending'
1378 STATE_ERROR = 'repo_state_error'
1378 STATE_ERROR = 'repo_state_error'
1379
1379
1380 LOCK_AUTOMATIC = 'lock_auto'
1380 LOCK_AUTOMATIC = 'lock_auto'
1381 LOCK_API = 'lock_api'
1381 LOCK_API = 'lock_api'
1382 LOCK_WEB = 'lock_web'
1382 LOCK_WEB = 'lock_web'
1383 LOCK_PULL = 'lock_pull'
1383 LOCK_PULL = 'lock_pull'
1384
1384
1385 NAME_SEP = URL_SEP
1385 NAME_SEP = URL_SEP
1386
1386
1387 repo_id = Column(
1387 repo_id = Column(
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1389 primary_key=True)
1389 primary_key=True)
1390 _repo_name = Column(
1390 _repo_name = Column(
1391 "repo_name", Text(), nullable=False, default=None)
1391 "repo_name", Text(), nullable=False, default=None)
1392 _repo_name_hash = Column(
1392 _repo_name_hash = Column(
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1395
1395
1396 clone_uri = Column(
1396 clone_uri = Column(
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1398 default=None)
1398 default=None)
1399 repo_type = Column(
1399 repo_type = Column(
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1401 user_id = Column(
1401 user_id = Column(
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1403 unique=False, default=None)
1403 unique=False, default=None)
1404 private = Column(
1404 private = Column(
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1406 enable_statistics = Column(
1406 enable_statistics = Column(
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1408 enable_downloads = Column(
1408 enable_downloads = Column(
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1410 description = Column(
1410 description = Column(
1411 "description", String(10000), nullable=True, unique=None, default=None)
1411 "description", String(10000), nullable=True, unique=None, default=None)
1412 created_on = Column(
1412 created_on = Column(
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1414 default=datetime.datetime.now)
1414 default=datetime.datetime.now)
1415 updated_on = Column(
1415 updated_on = Column(
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1417 default=datetime.datetime.now)
1417 default=datetime.datetime.now)
1418 _landing_revision = Column(
1418 _landing_revision = Column(
1419 "landing_revision", String(255), nullable=False, unique=False,
1419 "landing_revision", String(255), nullable=False, unique=False,
1420 default=None)
1420 default=None)
1421 enable_locking = Column(
1421 enable_locking = Column(
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1423 default=False)
1423 default=False)
1424 _locked = Column(
1424 _locked = Column(
1425 "locked", String(255), nullable=True, unique=False, default=None)
1425 "locked", String(255), nullable=True, unique=False, default=None)
1426 _changeset_cache = Column(
1426 _changeset_cache = Column(
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1428
1428
1429 fork_id = Column(
1429 fork_id = Column(
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1431 nullable=True, unique=False, default=None)
1431 nullable=True, unique=False, default=None)
1432 group_id = Column(
1432 group_id = Column(
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1434 unique=False, default=None)
1434 unique=False, default=None)
1435
1435
1436 user = relationship('User', lazy='joined')
1436 user = relationship('User', lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1439 repo_to_perm = relationship(
1439 repo_to_perm = relationship(
1440 'UserRepoToPerm', cascade='all',
1440 'UserRepoToPerm', cascade='all',
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1444
1444
1445 followers = relationship(
1445 followers = relationship(
1446 'UserFollowing',
1446 'UserFollowing',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1448 cascade='all')
1448 cascade='all')
1449 extra_fields = relationship(
1449 extra_fields = relationship(
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1451 logs = relationship('UserLog')
1451 logs = relationship('UserLog')
1452 comments = relationship(
1452 comments = relationship(
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1454 pull_requests_source = relationship(
1454 pull_requests_source = relationship(
1455 'PullRequest',
1455 'PullRequest',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1457 cascade="all, delete, delete-orphan")
1457 cascade="all, delete, delete-orphan")
1458 pull_requests_target = relationship(
1458 pull_requests_target = relationship(
1459 'PullRequest',
1459 'PullRequest',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1461 cascade="all, delete, delete-orphan")
1461 cascade="all, delete, delete-orphan")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1464 integrations = relationship('Integration',
1464 integrations = relationship('Integration',
1465 cascade="all, delete, delete-orphan")
1465 cascade="all, delete, delete-orphan")
1466
1466
1467 def __unicode__(self):
1467 def __unicode__(self):
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1469 safe_unicode(self.repo_name))
1469 safe_unicode(self.repo_name))
1470
1470
1471 @hybrid_property
1471 @hybrid_property
1472 def landing_rev(self):
1472 def landing_rev(self):
1473 # always should return [rev_type, rev]
1473 # always should return [rev_type, rev]
1474 if self._landing_revision:
1474 if self._landing_revision:
1475 _rev_info = self._landing_revision.split(':')
1475 _rev_info = self._landing_revision.split(':')
1476 if len(_rev_info) < 2:
1476 if len(_rev_info) < 2:
1477 _rev_info.insert(0, 'rev')
1477 _rev_info.insert(0, 'rev')
1478 return [_rev_info[0], _rev_info[1]]
1478 return [_rev_info[0], _rev_info[1]]
1479 return [None, None]
1479 return [None, None]
1480
1480
1481 @landing_rev.setter
1481 @landing_rev.setter
1482 def landing_rev(self, val):
1482 def landing_rev(self, val):
1483 if ':' not in val:
1483 if ':' not in val:
1484 raise ValueError('value must be delimited with `:` and consist '
1484 raise ValueError('value must be delimited with `:` and consist '
1485 'of <rev_type>:<rev>, got %s instead' % val)
1485 'of <rev_type>:<rev>, got %s instead' % val)
1486 self._landing_revision = val
1486 self._landing_revision = val
1487
1487
1488 @hybrid_property
1488 @hybrid_property
1489 def locked(self):
1489 def locked(self):
1490 if self._locked:
1490 if self._locked:
1491 user_id, timelocked, reason = self._locked.split(':')
1491 user_id, timelocked, reason = self._locked.split(':')
1492 lock_values = int(user_id), timelocked, reason
1492 lock_values = int(user_id), timelocked, reason
1493 else:
1493 else:
1494 lock_values = [None, None, None]
1494 lock_values = [None, None, None]
1495 return lock_values
1495 return lock_values
1496
1496
1497 @locked.setter
1497 @locked.setter
1498 def locked(self, val):
1498 def locked(self, val):
1499 if val and isinstance(val, (list, tuple)):
1499 if val and isinstance(val, (list, tuple)):
1500 self._locked = ':'.join(map(str, val))
1500 self._locked = ':'.join(map(str, val))
1501 else:
1501 else:
1502 self._locked = None
1502 self._locked = None
1503
1503
1504 @hybrid_property
1504 @hybrid_property
1505 def changeset_cache(self):
1505 def changeset_cache(self):
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1507 dummy = EmptyCommit().__json__()
1507 dummy = EmptyCommit().__json__()
1508 if not self._changeset_cache:
1508 if not self._changeset_cache:
1509 return dummy
1509 return dummy
1510 try:
1510 try:
1511 return json.loads(self._changeset_cache)
1511 return json.loads(self._changeset_cache)
1512 except TypeError:
1512 except TypeError:
1513 return dummy
1513 return dummy
1514 except Exception:
1514 except Exception:
1515 log.error(traceback.format_exc())
1515 log.error(traceback.format_exc())
1516 return dummy
1516 return dummy
1517
1517
1518 @changeset_cache.setter
1518 @changeset_cache.setter
1519 def changeset_cache(self, val):
1519 def changeset_cache(self, val):
1520 try:
1520 try:
1521 self._changeset_cache = json.dumps(val)
1521 self._changeset_cache = json.dumps(val)
1522 except Exception:
1522 except Exception:
1523 log.error(traceback.format_exc())
1523 log.error(traceback.format_exc())
1524
1524
1525 @hybrid_property
1525 @hybrid_property
1526 def repo_name(self):
1526 def repo_name(self):
1527 return self._repo_name
1527 return self._repo_name
1528
1528
1529 @repo_name.setter
1529 @repo_name.setter
1530 def repo_name(self, value):
1530 def repo_name(self, value):
1531 self._repo_name = value
1531 self._repo_name = value
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1533
1533
1534 @classmethod
1534 @classmethod
1535 def normalize_repo_name(cls, repo_name):
1535 def normalize_repo_name(cls, repo_name):
1536 """
1536 """
1537 Normalizes os specific repo_name to the format internally stored inside
1537 Normalizes os specific repo_name to the format internally stored inside
1538 database using URL_SEP
1538 database using URL_SEP
1539
1539
1540 :param cls:
1540 :param cls:
1541 :param repo_name:
1541 :param repo_name:
1542 """
1542 """
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1544
1544
1545 @classmethod
1545 @classmethod
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1547 session = Session()
1547 session = Session()
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1549
1549
1550 if cache:
1550 if cache:
1551 if identity_cache:
1551 if identity_cache:
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1553 if val:
1553 if val:
1554 return val
1554 return val
1555 else:
1555 else:
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1557 q = q.options(
1557 q = q.options(
1558 FromCache("sql_cache_short", cache_key))
1558 FromCache("sql_cache_short", cache_key))
1559
1559
1560 return q.scalar()
1560 return q.scalar()
1561
1561
1562 @classmethod
1562 @classmethod
1563 def get_by_full_path(cls, repo_full_path):
1563 def get_by_full_path(cls, repo_full_path):
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1565 repo_name = cls.normalize_repo_name(repo_name)
1565 repo_name = cls.normalize_repo_name(repo_name)
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1567
1567
1568 @classmethod
1568 @classmethod
1569 def get_repo_forks(cls, repo_id):
1569 def get_repo_forks(cls, repo_id):
1570 return cls.query().filter(Repository.fork_id == repo_id)
1570 return cls.query().filter(Repository.fork_id == repo_id)
1571
1571
1572 @classmethod
1572 @classmethod
1573 def base_path(cls):
1573 def base_path(cls):
1574 """
1574 """
1575 Returns base path when all repos are stored
1575 Returns base path when all repos are stored
1576
1576
1577 :param cls:
1577 :param cls:
1578 """
1578 """
1579 q = Session().query(RhodeCodeUi)\
1579 q = Session().query(RhodeCodeUi)\
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 return q.one().ui_value
1582 return q.one().ui_value
1583
1583
1584 @classmethod
1584 @classmethod
1585 def is_valid(cls, repo_name):
1585 def is_valid(cls, repo_name):
1586 """
1586 """
1587 returns True if given repo name is a valid filesystem repository
1587 returns True if given repo name is a valid filesystem repository
1588
1588
1589 :param cls:
1589 :param cls:
1590 :param repo_name:
1590 :param repo_name:
1591 """
1591 """
1592 from rhodecode.lib.utils import is_valid_repo
1592 from rhodecode.lib.utils import is_valid_repo
1593
1593
1594 return is_valid_repo(repo_name, cls.base_path())
1594 return is_valid_repo(repo_name, cls.base_path())
1595
1595
1596 @classmethod
1596 @classmethod
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1598 case_insensitive=True):
1598 case_insensitive=True):
1599 q = Repository.query()
1599 q = Repository.query()
1600
1600
1601 if not isinstance(user_id, Optional):
1601 if not isinstance(user_id, Optional):
1602 q = q.filter(Repository.user_id == user_id)
1602 q = q.filter(Repository.user_id == user_id)
1603
1603
1604 if not isinstance(group_id, Optional):
1604 if not isinstance(group_id, Optional):
1605 q = q.filter(Repository.group_id == group_id)
1605 q = q.filter(Repository.group_id == group_id)
1606
1606
1607 if case_insensitive:
1607 if case_insensitive:
1608 q = q.order_by(func.lower(Repository.repo_name))
1608 q = q.order_by(func.lower(Repository.repo_name))
1609 else:
1609 else:
1610 q = q.order_by(Repository.repo_name)
1610 q = q.order_by(Repository.repo_name)
1611 return q.all()
1611 return q.all()
1612
1612
1613 @property
1613 @property
1614 def forks(self):
1614 def forks(self):
1615 """
1615 """
1616 Return forks of this repo
1616 Return forks of this repo
1617 """
1617 """
1618 return Repository.get_repo_forks(self.repo_id)
1618 return Repository.get_repo_forks(self.repo_id)
1619
1619
1620 @property
1620 @property
1621 def parent(self):
1621 def parent(self):
1622 """
1622 """
1623 Returns fork parent
1623 Returns fork parent
1624 """
1624 """
1625 return self.fork
1625 return self.fork
1626
1626
1627 @property
1627 @property
1628 def just_name(self):
1628 def just_name(self):
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1630
1630
1631 @property
1631 @property
1632 def groups_with_parents(self):
1632 def groups_with_parents(self):
1633 groups = []
1633 groups = []
1634 if self.group is None:
1634 if self.group is None:
1635 return groups
1635 return groups
1636
1636
1637 cur_gr = self.group
1637 cur_gr = self.group
1638 groups.insert(0, cur_gr)
1638 groups.insert(0, cur_gr)
1639 while 1:
1639 while 1:
1640 gr = getattr(cur_gr, 'parent_group', None)
1640 gr = getattr(cur_gr, 'parent_group', None)
1641 cur_gr = cur_gr.parent_group
1641 cur_gr = cur_gr.parent_group
1642 if gr is None:
1642 if gr is None:
1643 break
1643 break
1644 groups.insert(0, gr)
1644 groups.insert(0, gr)
1645
1645
1646 return groups
1646 return groups
1647
1647
1648 @property
1648 @property
1649 def groups_and_repo(self):
1649 def groups_and_repo(self):
1650 return self.groups_with_parents, self
1650 return self.groups_with_parents, self
1651
1651
1652 @LazyProperty
1652 @LazyProperty
1653 def repo_path(self):
1653 def repo_path(self):
1654 """
1654 """
1655 Returns base full path for that repository means where it actually
1655 Returns base full path for that repository means where it actually
1656 exists on a filesystem
1656 exists on a filesystem
1657 """
1657 """
1658 q = Session().query(RhodeCodeUi).filter(
1658 q = Session().query(RhodeCodeUi).filter(
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1661 return q.one().ui_value
1661 return q.one().ui_value
1662
1662
1663 @property
1663 @property
1664 def repo_full_path(self):
1664 def repo_full_path(self):
1665 p = [self.repo_path]
1665 p = [self.repo_path]
1666 # we need to split the name by / since this is how we store the
1666 # we need to split the name by / since this is how we store the
1667 # names in the database, but that eventually needs to be converted
1667 # names in the database, but that eventually needs to be converted
1668 # into a valid system path
1668 # into a valid system path
1669 p += self.repo_name.split(self.NAME_SEP)
1669 p += self.repo_name.split(self.NAME_SEP)
1670 return os.path.join(*map(safe_unicode, p))
1670 return os.path.join(*map(safe_unicode, p))
1671
1671
1672 @property
1672 @property
1673 def cache_keys(self):
1673 def cache_keys(self):
1674 """
1674 """
1675 Returns associated cache keys for that repo
1675 Returns associated cache keys for that repo
1676 """
1676 """
1677 return CacheKey.query()\
1677 return CacheKey.query()\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1679 .order_by(CacheKey.cache_key)\
1679 .order_by(CacheKey.cache_key)\
1680 .all()
1680 .all()
1681
1681
1682 def get_new_name(self, repo_name):
1682 def get_new_name(self, repo_name):
1683 """
1683 """
1684 returns new full repository name based on assigned group and new new
1684 returns new full repository name based on assigned group and new new
1685
1685
1686 :param group_name:
1686 :param group_name:
1687 """
1687 """
1688 path_prefix = self.group.full_path_splitted if self.group else []
1688 path_prefix = self.group.full_path_splitted if self.group else []
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1690
1690
1691 @property
1691 @property
1692 def _config(self):
1692 def _config(self):
1693 """
1693 """
1694 Returns db based config object.
1694 Returns db based config object.
1695 """
1695 """
1696 from rhodecode.lib.utils import make_db_config
1696 from rhodecode.lib.utils import make_db_config
1697 return make_db_config(clear_session=False, repo=self)
1697 return make_db_config(clear_session=False, repo=self)
1698
1698
1699 def permissions(self, with_admins=True, with_owner=True):
1699 def permissions(self, with_admins=True, with_owner=True):
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1702 joinedload(UserRepoToPerm.user),
1702 joinedload(UserRepoToPerm.user),
1703 joinedload(UserRepoToPerm.permission),)
1703 joinedload(UserRepoToPerm.permission),)
1704
1704
1705 # get owners and admins and permissions. We do a trick of re-writing
1705 # get owners and admins and permissions. We do a trick of re-writing
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1707 # has a global reference and changing one object propagates to all
1707 # has a global reference and changing one object propagates to all
1708 # others. This means if admin is also an owner admin_row that change
1708 # others. This means if admin is also an owner admin_row that change
1709 # would propagate to both objects
1709 # would propagate to both objects
1710 perm_rows = []
1710 perm_rows = []
1711 for _usr in q.all():
1711 for _usr in q.all():
1712 usr = AttributeDict(_usr.user.get_dict())
1712 usr = AttributeDict(_usr.user.get_dict())
1713 usr.permission = _usr.permission.permission_name
1713 usr.permission = _usr.permission.permission_name
1714 perm_rows.append(usr)
1714 perm_rows.append(usr)
1715
1715
1716 # filter the perm rows by 'default' first and then sort them by
1716 # filter the perm rows by 'default' first and then sort them by
1717 # admin,write,read,none permissions sorted again alphabetically in
1717 # admin,write,read,none permissions sorted again alphabetically in
1718 # each group
1718 # each group
1719 perm_rows = sorted(perm_rows, key=display_sort)
1719 perm_rows = sorted(perm_rows, key=display_sort)
1720
1720
1721 _admin_perm = 'repository.admin'
1721 _admin_perm = 'repository.admin'
1722 owner_row = []
1722 owner_row = []
1723 if with_owner:
1723 if with_owner:
1724 usr = AttributeDict(self.user.get_dict())
1724 usr = AttributeDict(self.user.get_dict())
1725 usr.owner_row = True
1725 usr.owner_row = True
1726 usr.permission = _admin_perm
1726 usr.permission = _admin_perm
1727 owner_row.append(usr)
1727 owner_row.append(usr)
1728
1728
1729 super_admin_rows = []
1729 super_admin_rows = []
1730 if with_admins:
1730 if with_admins:
1731 for usr in User.get_all_super_admins():
1731 for usr in User.get_all_super_admins():
1732 # if this admin is also owner, don't double the record
1732 # if this admin is also owner, don't double the record
1733 if usr.user_id == owner_row[0].user_id:
1733 if usr.user_id == owner_row[0].user_id:
1734 owner_row[0].admin_row = True
1734 owner_row[0].admin_row = True
1735 else:
1735 else:
1736 usr = AttributeDict(usr.get_dict())
1736 usr = AttributeDict(usr.get_dict())
1737 usr.admin_row = True
1737 usr.admin_row = True
1738 usr.permission = _admin_perm
1738 usr.permission = _admin_perm
1739 super_admin_rows.append(usr)
1739 super_admin_rows.append(usr)
1740
1740
1741 return super_admin_rows + owner_row + perm_rows
1741 return super_admin_rows + owner_row + perm_rows
1742
1742
1743 def permission_user_groups(self):
1743 def permission_user_groups(self):
1744 q = UserGroupRepoToPerm.query().filter(
1744 q = UserGroupRepoToPerm.query().filter(
1745 UserGroupRepoToPerm.repository == self)
1745 UserGroupRepoToPerm.repository == self)
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1747 joinedload(UserGroupRepoToPerm.users_group),
1747 joinedload(UserGroupRepoToPerm.users_group),
1748 joinedload(UserGroupRepoToPerm.permission),)
1748 joinedload(UserGroupRepoToPerm.permission),)
1749
1749
1750 perm_rows = []
1750 perm_rows = []
1751 for _user_group in q.all():
1751 for _user_group in q.all():
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1753 usr.permission = _user_group.permission.permission_name
1753 usr.permission = _user_group.permission.permission_name
1754 perm_rows.append(usr)
1754 perm_rows.append(usr)
1755
1755
1756 return perm_rows
1756 return perm_rows
1757
1757
1758 def get_api_data(self, include_secrets=False):
1758 def get_api_data(self, include_secrets=False):
1759 """
1759 """
1760 Common function for generating repo api data
1760 Common function for generating repo api data
1761
1761
1762 :param include_secrets: See :meth:`User.get_api_data`.
1762 :param include_secrets: See :meth:`User.get_api_data`.
1763
1763
1764 """
1764 """
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1766 # move this methods on models level.
1766 # move this methods on models level.
1767 from rhodecode.model.settings import SettingsModel
1767 from rhodecode.model.settings import SettingsModel
1768 from rhodecode.model.repo import RepoModel
1768 from rhodecode.model.repo import RepoModel
1769
1769
1770 repo = self
1770 repo = self
1771 _user_id, _time, _reason = self.locked
1771 _user_id, _time, _reason = self.locked
1772
1772
1773 data = {
1773 data = {
1774 'repo_id': repo.repo_id,
1774 'repo_id': repo.repo_id,
1775 'repo_name': repo.repo_name,
1775 'repo_name': repo.repo_name,
1776 'repo_type': repo.repo_type,
1776 'repo_type': repo.repo_type,
1777 'clone_uri': repo.clone_uri or '',
1777 'clone_uri': repo.clone_uri or '',
1778 'url': RepoModel().get_url(self),
1778 'url': RepoModel().get_url(self),
1779 'private': repo.private,
1779 'private': repo.private,
1780 'created_on': repo.created_on,
1780 'created_on': repo.created_on,
1781 'description': repo.description,
1781 'description': repo.description,
1782 'landing_rev': repo.landing_rev,
1782 'landing_rev': repo.landing_rev,
1783 'owner': repo.user.username,
1783 'owner': repo.user.username,
1784 'fork_of': repo.fork.repo_name if repo.fork else None,
1784 'fork_of': repo.fork.repo_name if repo.fork else None,
1785 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1785 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1786 'enable_statistics': repo.enable_statistics,
1786 'enable_statistics': repo.enable_statistics,
1787 'enable_locking': repo.enable_locking,
1787 'enable_locking': repo.enable_locking,
1788 'enable_downloads': repo.enable_downloads,
1788 'enable_downloads': repo.enable_downloads,
1789 'last_changeset': repo.changeset_cache,
1789 'last_changeset': repo.changeset_cache,
1790 'locked_by': User.get(_user_id).get_api_data(
1790 'locked_by': User.get(_user_id).get_api_data(
1791 include_secrets=include_secrets) if _user_id else None,
1791 include_secrets=include_secrets) if _user_id else None,
1792 'locked_date': time_to_datetime(_time) if _time else None,
1792 'locked_date': time_to_datetime(_time) if _time else None,
1793 'lock_reason': _reason if _reason else None,
1793 'lock_reason': _reason if _reason else None,
1794 }
1794 }
1795
1795
1796 # TODO: mikhail: should be per-repo settings here
1796 # TODO: mikhail: should be per-repo settings here
1797 rc_config = SettingsModel().get_all_settings()
1797 rc_config = SettingsModel().get_all_settings()
1798 repository_fields = str2bool(
1798 repository_fields = str2bool(
1799 rc_config.get('rhodecode_repository_fields'))
1799 rc_config.get('rhodecode_repository_fields'))
1800 if repository_fields:
1800 if repository_fields:
1801 for f in self.extra_fields:
1801 for f in self.extra_fields:
1802 data[f.field_key_prefixed] = f.field_value
1802 data[f.field_key_prefixed] = f.field_value
1803
1803
1804 return data
1804 return data
1805
1805
1806 @classmethod
1806 @classmethod
1807 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1807 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1808 if not lock_time:
1808 if not lock_time:
1809 lock_time = time.time()
1809 lock_time = time.time()
1810 if not lock_reason:
1810 if not lock_reason:
1811 lock_reason = cls.LOCK_AUTOMATIC
1811 lock_reason = cls.LOCK_AUTOMATIC
1812 repo.locked = [user_id, lock_time, lock_reason]
1812 repo.locked = [user_id, lock_time, lock_reason]
1813 Session().add(repo)
1813 Session().add(repo)
1814 Session().commit()
1814 Session().commit()
1815
1815
1816 @classmethod
1816 @classmethod
1817 def unlock(cls, repo):
1817 def unlock(cls, repo):
1818 repo.locked = None
1818 repo.locked = None
1819 Session().add(repo)
1819 Session().add(repo)
1820 Session().commit()
1820 Session().commit()
1821
1821
1822 @classmethod
1822 @classmethod
1823 def getlock(cls, repo):
1823 def getlock(cls, repo):
1824 return repo.locked
1824 return repo.locked
1825
1825
1826 def is_user_lock(self, user_id):
1826 def is_user_lock(self, user_id):
1827 if self.lock[0]:
1827 if self.lock[0]:
1828 lock_user_id = safe_int(self.lock[0])
1828 lock_user_id = safe_int(self.lock[0])
1829 user_id = safe_int(user_id)
1829 user_id = safe_int(user_id)
1830 # both are ints, and they are equal
1830 # both are ints, and they are equal
1831 return all([lock_user_id, user_id]) and lock_user_id == user_id
1831 return all([lock_user_id, user_id]) and lock_user_id == user_id
1832
1832
1833 return False
1833 return False
1834
1834
1835 def get_locking_state(self, action, user_id, only_when_enabled=True):
1835 def get_locking_state(self, action, user_id, only_when_enabled=True):
1836 """
1836 """
1837 Checks locking on this repository, if locking is enabled and lock is
1837 Checks locking on this repository, if locking is enabled and lock is
1838 present returns a tuple of make_lock, locked, locked_by.
1838 present returns a tuple of make_lock, locked, locked_by.
1839 make_lock can have 3 states None (do nothing) True, make lock
1839 make_lock can have 3 states None (do nothing) True, make lock
1840 False release lock, This value is later propagated to hooks, which
1840 False release lock, This value is later propagated to hooks, which
1841 do the locking. Think about this as signals passed to hooks what to do.
1841 do the locking. Think about this as signals passed to hooks what to do.
1842
1842
1843 """
1843 """
1844 # TODO: johbo: This is part of the business logic and should be moved
1844 # TODO: johbo: This is part of the business logic and should be moved
1845 # into the RepositoryModel.
1845 # into the RepositoryModel.
1846
1846
1847 if action not in ('push', 'pull'):
1847 if action not in ('push', 'pull'):
1848 raise ValueError("Invalid action value: %s" % repr(action))
1848 raise ValueError("Invalid action value: %s" % repr(action))
1849
1849
1850 # defines if locked error should be thrown to user
1850 # defines if locked error should be thrown to user
1851 currently_locked = False
1851 currently_locked = False
1852 # defines if new lock should be made, tri-state
1852 # defines if new lock should be made, tri-state
1853 make_lock = None
1853 make_lock = None
1854 repo = self
1854 repo = self
1855 user = User.get(user_id)
1855 user = User.get(user_id)
1856
1856
1857 lock_info = repo.locked
1857 lock_info = repo.locked
1858
1858
1859 if repo and (repo.enable_locking or not only_when_enabled):
1859 if repo and (repo.enable_locking or not only_when_enabled):
1860 if action == 'push':
1860 if action == 'push':
1861 # check if it's already locked !, if it is compare users
1861 # check if it's already locked !, if it is compare users
1862 locked_by_user_id = lock_info[0]
1862 locked_by_user_id = lock_info[0]
1863 if user.user_id == locked_by_user_id:
1863 if user.user_id == locked_by_user_id:
1864 log.debug(
1864 log.debug(
1865 'Got `push` action from user %s, now unlocking', user)
1865 'Got `push` action from user %s, now unlocking', user)
1866 # unlock if we have push from user who locked
1866 # unlock if we have push from user who locked
1867 make_lock = False
1867 make_lock = False
1868 else:
1868 else:
1869 # we're not the same user who locked, ban with
1869 # we're not the same user who locked, ban with
1870 # code defined in settings (default is 423 HTTP Locked) !
1870 # code defined in settings (default is 423 HTTP Locked) !
1871 log.debug('Repo %s is currently locked by %s', repo, user)
1871 log.debug('Repo %s is currently locked by %s', repo, user)
1872 currently_locked = True
1872 currently_locked = True
1873 elif action == 'pull':
1873 elif action == 'pull':
1874 # [0] user [1] date
1874 # [0] user [1] date
1875 if lock_info[0] and lock_info[1]:
1875 if lock_info[0] and lock_info[1]:
1876 log.debug('Repo %s is currently locked by %s', repo, user)
1876 log.debug('Repo %s is currently locked by %s', repo, user)
1877 currently_locked = True
1877 currently_locked = True
1878 else:
1878 else:
1879 log.debug('Setting lock on repo %s by %s', repo, user)
1879 log.debug('Setting lock on repo %s by %s', repo, user)
1880 make_lock = True
1880 make_lock = True
1881
1881
1882 else:
1882 else:
1883 log.debug('Repository %s do not have locking enabled', repo)
1883 log.debug('Repository %s do not have locking enabled', repo)
1884
1884
1885 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1885 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1886 make_lock, currently_locked, lock_info)
1886 make_lock, currently_locked, lock_info)
1887
1887
1888 from rhodecode.lib.auth import HasRepoPermissionAny
1888 from rhodecode.lib.auth import HasRepoPermissionAny
1889 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1889 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1890 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1890 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1891 # if we don't have at least write permission we cannot make a lock
1891 # if we don't have at least write permission we cannot make a lock
1892 log.debug('lock state reset back to FALSE due to lack '
1892 log.debug('lock state reset back to FALSE due to lack '
1893 'of at least read permission')
1893 'of at least read permission')
1894 make_lock = False
1894 make_lock = False
1895
1895
1896 return make_lock, currently_locked, lock_info
1896 return make_lock, currently_locked, lock_info
1897
1897
1898 @property
1898 @property
1899 def last_db_change(self):
1899 def last_db_change(self):
1900 return self.updated_on
1900 return self.updated_on
1901
1901
1902 @property
1902 @property
1903 def clone_uri_hidden(self):
1903 def clone_uri_hidden(self):
1904 clone_uri = self.clone_uri
1904 clone_uri = self.clone_uri
1905 if clone_uri:
1905 if clone_uri:
1906 import urlobject
1906 import urlobject
1907 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1907 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1908 if url_obj.password:
1908 if url_obj.password:
1909 clone_uri = url_obj.with_password('*****')
1909 clone_uri = url_obj.with_password('*****')
1910 return clone_uri
1910 return clone_uri
1911
1911
1912 def clone_url(self, **override):
1912 def clone_url(self, **override):
1913
1913
1914 uri_tmpl = None
1914 uri_tmpl = None
1915 if 'with_id' in override:
1915 if 'with_id' in override:
1916 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1916 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1917 del override['with_id']
1917 del override['with_id']
1918
1918
1919 if 'uri_tmpl' in override:
1919 if 'uri_tmpl' in override:
1920 uri_tmpl = override['uri_tmpl']
1920 uri_tmpl = override['uri_tmpl']
1921 del override['uri_tmpl']
1921 del override['uri_tmpl']
1922
1922
1923 # we didn't override our tmpl from **overrides
1923 # we didn't override our tmpl from **overrides
1924 if not uri_tmpl:
1924 if not uri_tmpl:
1925 uri_tmpl = self.DEFAULT_CLONE_URI
1925 uri_tmpl = self.DEFAULT_CLONE_URI
1926 try:
1926 try:
1927 from pylons import tmpl_context as c
1927 from pylons import tmpl_context as c
1928 uri_tmpl = c.clone_uri_tmpl
1928 uri_tmpl = c.clone_uri_tmpl
1929 except Exception:
1929 except Exception:
1930 # in any case if we call this outside of request context,
1930 # in any case if we call this outside of request context,
1931 # ie, not having tmpl_context set up
1931 # ie, not having tmpl_context set up
1932 pass
1932 pass
1933
1933
1934 request = get_current_request()
1934 request = get_current_request()
1935 return get_clone_url(request=request,
1935 return get_clone_url(request=request,
1936 uri_tmpl=uri_tmpl,
1936 uri_tmpl=uri_tmpl,
1937 repo_name=self.repo_name,
1937 repo_name=self.repo_name,
1938 repo_id=self.repo_id, **override)
1938 repo_id=self.repo_id, **override)
1939
1939
1940 def set_state(self, state):
1940 def set_state(self, state):
1941 self.repo_state = state
1941 self.repo_state = state
1942 Session().add(self)
1942 Session().add(self)
1943 #==========================================================================
1943 #==========================================================================
1944 # SCM PROPERTIES
1944 # SCM PROPERTIES
1945 #==========================================================================
1945 #==========================================================================
1946
1946
1947 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1947 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1948 return get_commit_safe(
1948 return get_commit_safe(
1949 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1949 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1950
1950
1951 def get_changeset(self, rev=None, pre_load=None):
1951 def get_changeset(self, rev=None, pre_load=None):
1952 warnings.warn("Use get_commit", DeprecationWarning)
1952 warnings.warn("Use get_commit", DeprecationWarning)
1953 commit_id = None
1953 commit_id = None
1954 commit_idx = None
1954 commit_idx = None
1955 if isinstance(rev, basestring):
1955 if isinstance(rev, basestring):
1956 commit_id = rev
1956 commit_id = rev
1957 else:
1957 else:
1958 commit_idx = rev
1958 commit_idx = rev
1959 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1959 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1960 pre_load=pre_load)
1960 pre_load=pre_load)
1961
1961
1962 def get_landing_commit(self):
1962 def get_landing_commit(self):
1963 """
1963 """
1964 Returns landing commit, or if that doesn't exist returns the tip
1964 Returns landing commit, or if that doesn't exist returns the tip
1965 """
1965 """
1966 _rev_type, _rev = self.landing_rev
1966 _rev_type, _rev = self.landing_rev
1967 commit = self.get_commit(_rev)
1967 commit = self.get_commit(_rev)
1968 if isinstance(commit, EmptyCommit):
1968 if isinstance(commit, EmptyCommit):
1969 return self.get_commit()
1969 return self.get_commit()
1970 return commit
1970 return commit
1971
1971
1972 def update_commit_cache(self, cs_cache=None, config=None):
1972 def update_commit_cache(self, cs_cache=None, config=None):
1973 """
1973 """
1974 Update cache of last changeset for repository, keys should be::
1974 Update cache of last changeset for repository, keys should be::
1975
1975
1976 short_id
1976 short_id
1977 raw_id
1977 raw_id
1978 revision
1978 revision
1979 parents
1979 parents
1980 message
1980 message
1981 date
1981 date
1982 author
1982 author
1983
1983
1984 :param cs_cache:
1984 :param cs_cache:
1985 """
1985 """
1986 from rhodecode.lib.vcs.backends.base import BaseChangeset
1986 from rhodecode.lib.vcs.backends.base import BaseChangeset
1987 if cs_cache is None:
1987 if cs_cache is None:
1988 # use no-cache version here
1988 # use no-cache version here
1989 scm_repo = self.scm_instance(cache=False, config=config)
1989 scm_repo = self.scm_instance(cache=False, config=config)
1990 if scm_repo:
1990 if scm_repo:
1991 cs_cache = scm_repo.get_commit(
1991 cs_cache = scm_repo.get_commit(
1992 pre_load=["author", "date", "message", "parents"])
1992 pre_load=["author", "date", "message", "parents"])
1993 else:
1993 else:
1994 cs_cache = EmptyCommit()
1994 cs_cache = EmptyCommit()
1995
1995
1996 if isinstance(cs_cache, BaseChangeset):
1996 if isinstance(cs_cache, BaseChangeset):
1997 cs_cache = cs_cache.__json__()
1997 cs_cache = cs_cache.__json__()
1998
1998
1999 def is_outdated(new_cs_cache):
1999 def is_outdated(new_cs_cache):
2000 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2000 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2001 new_cs_cache['revision'] != self.changeset_cache['revision']):
2001 new_cs_cache['revision'] != self.changeset_cache['revision']):
2002 return True
2002 return True
2003 return False
2003 return False
2004
2004
2005 # check if we have maybe already latest cached revision
2005 # check if we have maybe already latest cached revision
2006 if is_outdated(cs_cache) or not self.changeset_cache:
2006 if is_outdated(cs_cache) or not self.changeset_cache:
2007 _default = datetime.datetime.fromtimestamp(0)
2007 _default = datetime.datetime.fromtimestamp(0)
2008 last_change = cs_cache.get('date') or _default
2008 last_change = cs_cache.get('date') or _default
2009 log.debug('updated repo %s with new cs cache %s',
2009 log.debug('updated repo %s with new cs cache %s',
2010 self.repo_name, cs_cache)
2010 self.repo_name, cs_cache)
2011 self.updated_on = last_change
2011 self.updated_on = last_change
2012 self.changeset_cache = cs_cache
2012 self.changeset_cache = cs_cache
2013 Session().add(self)
2013 Session().add(self)
2014 Session().commit()
2014 Session().commit()
2015 else:
2015 else:
2016 log.debug('Skipping update_commit_cache for repo:`%s` '
2016 log.debug('Skipping update_commit_cache for repo:`%s` '
2017 'commit already with latest changes', self.repo_name)
2017 'commit already with latest changes', self.repo_name)
2018
2018
2019 @property
2019 @property
2020 def tip(self):
2020 def tip(self):
2021 return self.get_commit('tip')
2021 return self.get_commit('tip')
2022
2022
2023 @property
2023 @property
2024 def author(self):
2024 def author(self):
2025 return self.tip.author
2025 return self.tip.author
2026
2026
2027 @property
2027 @property
2028 def last_change(self):
2028 def last_change(self):
2029 return self.scm_instance().last_change
2029 return self.scm_instance().last_change
2030
2030
2031 def get_comments(self, revisions=None):
2031 def get_comments(self, revisions=None):
2032 """
2032 """
2033 Returns comments for this repository grouped by revisions
2033 Returns comments for this repository grouped by revisions
2034
2034
2035 :param revisions: filter query by revisions only
2035 :param revisions: filter query by revisions only
2036 """
2036 """
2037 cmts = ChangesetComment.query()\
2037 cmts = ChangesetComment.query()\
2038 .filter(ChangesetComment.repo == self)
2038 .filter(ChangesetComment.repo == self)
2039 if revisions:
2039 if revisions:
2040 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2040 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2041 grouped = collections.defaultdict(list)
2041 grouped = collections.defaultdict(list)
2042 for cmt in cmts.all():
2042 for cmt in cmts.all():
2043 grouped[cmt.revision].append(cmt)
2043 grouped[cmt.revision].append(cmt)
2044 return grouped
2044 return grouped
2045
2045
2046 def statuses(self, revisions=None):
2046 def statuses(self, revisions=None):
2047 """
2047 """
2048 Returns statuses for this repository
2048 Returns statuses for this repository
2049
2049
2050 :param revisions: list of revisions to get statuses for
2050 :param revisions: list of revisions to get statuses for
2051 """
2051 """
2052 statuses = ChangesetStatus.query()\
2052 statuses = ChangesetStatus.query()\
2053 .filter(ChangesetStatus.repo == self)\
2053 .filter(ChangesetStatus.repo == self)\
2054 .filter(ChangesetStatus.version == 0)
2054 .filter(ChangesetStatus.version == 0)
2055
2055
2056 if revisions:
2056 if revisions:
2057 # Try doing the filtering in chunks to avoid hitting limits
2057 # Try doing the filtering in chunks to avoid hitting limits
2058 size = 500
2058 size = 500
2059 status_results = []
2059 status_results = []
2060 for chunk in xrange(0, len(revisions), size):
2060 for chunk in xrange(0, len(revisions), size):
2061 status_results += statuses.filter(
2061 status_results += statuses.filter(
2062 ChangesetStatus.revision.in_(
2062 ChangesetStatus.revision.in_(
2063 revisions[chunk: chunk+size])
2063 revisions[chunk: chunk+size])
2064 ).all()
2064 ).all()
2065 else:
2065 else:
2066 status_results = statuses.all()
2066 status_results = statuses.all()
2067
2067
2068 grouped = {}
2068 grouped = {}
2069
2069
2070 # maybe we have open new pullrequest without a status?
2070 # maybe we have open new pullrequest without a status?
2071 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2071 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2072 status_lbl = ChangesetStatus.get_status_lbl(stat)
2072 status_lbl = ChangesetStatus.get_status_lbl(stat)
2073 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2073 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2074 for rev in pr.revisions:
2074 for rev in pr.revisions:
2075 pr_id = pr.pull_request_id
2075 pr_id = pr.pull_request_id
2076 pr_repo = pr.target_repo.repo_name
2076 pr_repo = pr.target_repo.repo_name
2077 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2077 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2078
2078
2079 for stat in status_results:
2079 for stat in status_results:
2080 pr_id = pr_repo = None
2080 pr_id = pr_repo = None
2081 if stat.pull_request:
2081 if stat.pull_request:
2082 pr_id = stat.pull_request.pull_request_id
2082 pr_id = stat.pull_request.pull_request_id
2083 pr_repo = stat.pull_request.target_repo.repo_name
2083 pr_repo = stat.pull_request.target_repo.repo_name
2084 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2084 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2085 pr_id, pr_repo]
2085 pr_id, pr_repo]
2086 return grouped
2086 return grouped
2087
2087
2088 # ==========================================================================
2088 # ==========================================================================
2089 # SCM CACHE INSTANCE
2089 # SCM CACHE INSTANCE
2090 # ==========================================================================
2090 # ==========================================================================
2091
2091
2092 def scm_instance(self, **kwargs):
2092 def scm_instance(self, **kwargs):
2093 import rhodecode
2093 import rhodecode
2094
2094
2095 # Passing a config will not hit the cache currently only used
2095 # Passing a config will not hit the cache currently only used
2096 # for repo2dbmapper
2096 # for repo2dbmapper
2097 config = kwargs.pop('config', None)
2097 config = kwargs.pop('config', None)
2098 cache = kwargs.pop('cache', None)
2098 cache = kwargs.pop('cache', None)
2099 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2099 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2100 # if cache is NOT defined use default global, else we have a full
2100 # if cache is NOT defined use default global, else we have a full
2101 # control over cache behaviour
2101 # control over cache behaviour
2102 if cache is None and full_cache and not config:
2102 if cache is None and full_cache and not config:
2103 return self._get_instance_cached()
2103 return self._get_instance_cached()
2104 return self._get_instance(cache=bool(cache), config=config)
2104 return self._get_instance(cache=bool(cache), config=config)
2105
2105
2106 def _get_instance_cached(self):
2106 def _get_instance_cached(self):
2107 @cache_region('long_term')
2107 @cache_region('long_term')
2108 def _get_repo(cache_key):
2108 def _get_repo(cache_key):
2109 return self._get_instance()
2109 return self._get_instance()
2110
2110
2111 invalidator_context = CacheKey.repo_context_cache(
2111 invalidator_context = CacheKey.repo_context_cache(
2112 _get_repo, self.repo_name, None, thread_scoped=True)
2112 _get_repo, self.repo_name, None, thread_scoped=True)
2113
2113
2114 with invalidator_context as context:
2114 with invalidator_context as context:
2115 context.invalidate()
2115 context.invalidate()
2116 repo = context.compute()
2116 repo = context.compute()
2117
2117
2118 return repo
2118 return repo
2119
2119
2120 def _get_instance(self, cache=True, config=None):
2120 def _get_instance(self, cache=True, config=None):
2121 config = config or self._config
2121 config = config or self._config
2122 custom_wire = {
2122 custom_wire = {
2123 'cache': cache # controls the vcs.remote cache
2123 'cache': cache # controls the vcs.remote cache
2124 }
2124 }
2125 repo = get_vcs_instance(
2125 repo = get_vcs_instance(
2126 repo_path=safe_str(self.repo_full_path),
2126 repo_path=safe_str(self.repo_full_path),
2127 config=config,
2127 config=config,
2128 with_wire=custom_wire,
2128 with_wire=custom_wire,
2129 create=False,
2129 create=False,
2130 _vcs_alias=self.repo_type)
2130 _vcs_alias=self.repo_type)
2131
2131
2132 return repo
2132 return repo
2133
2133
2134 def __json__(self):
2134 def __json__(self):
2135 return {'landing_rev': self.landing_rev}
2135 return {'landing_rev': self.landing_rev}
2136
2136
2137 def get_dict(self):
2137 def get_dict(self):
2138
2138
2139 # Since we transformed `repo_name` to a hybrid property, we need to
2139 # Since we transformed `repo_name` to a hybrid property, we need to
2140 # keep compatibility with the code which uses `repo_name` field.
2140 # keep compatibility with the code which uses `repo_name` field.
2141
2141
2142 result = super(Repository, self).get_dict()
2142 result = super(Repository, self).get_dict()
2143 result['repo_name'] = result.pop('_repo_name', None)
2143 result['repo_name'] = result.pop('_repo_name', None)
2144 return result
2144 return result
2145
2145
2146
2146
2147 class RepoGroup(Base, BaseModel):
2147 class RepoGroup(Base, BaseModel):
2148 __tablename__ = 'groups'
2148 __tablename__ = 'groups'
2149 __table_args__ = (
2149 __table_args__ = (
2150 UniqueConstraint('group_name', 'group_parent_id'),
2150 UniqueConstraint('group_name', 'group_parent_id'),
2151 CheckConstraint('group_id != group_parent_id'),
2151 CheckConstraint('group_id != group_parent_id'),
2152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2154 )
2154 )
2155 __mapper_args__ = {'order_by': 'group_name'}
2155 __mapper_args__ = {'order_by': 'group_name'}
2156
2156
2157 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2157 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2158
2158
2159 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2159 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2160 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2160 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2161 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2161 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2162 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2162 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2163 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2163 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2165 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2165 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2166 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2166 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2167
2167
2168 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2168 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2169 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2169 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2170 parent_group = relationship('RepoGroup', remote_side=group_id)
2170 parent_group = relationship('RepoGroup', remote_side=group_id)
2171 user = relationship('User')
2171 user = relationship('User')
2172 integrations = relationship('Integration',
2172 integrations = relationship('Integration',
2173 cascade="all, delete, delete-orphan")
2173 cascade="all, delete, delete-orphan")
2174
2174
2175 def __init__(self, group_name='', parent_group=None):
2175 def __init__(self, group_name='', parent_group=None):
2176 self.group_name = group_name
2176 self.group_name = group_name
2177 self.parent_group = parent_group
2177 self.parent_group = parent_group
2178
2178
2179 def __unicode__(self):
2179 def __unicode__(self):
2180 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2180 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2181 self.group_name)
2181 self.group_name)
2182
2182
2183 @classmethod
2183 @classmethod
2184 def _generate_choice(cls, repo_group):
2184 def _generate_choice(cls, repo_group):
2185 from webhelpers.html import literal as _literal
2185 from webhelpers.html import literal as _literal
2186 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2186 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2187 return repo_group.group_id, _name(repo_group.full_path_splitted)
2187 return repo_group.group_id, _name(repo_group.full_path_splitted)
2188
2188
2189 @classmethod
2189 @classmethod
2190 def groups_choices(cls, groups=None, show_empty_group=True):
2190 def groups_choices(cls, groups=None, show_empty_group=True):
2191 if not groups:
2191 if not groups:
2192 groups = cls.query().all()
2192 groups = cls.query().all()
2193
2193
2194 repo_groups = []
2194 repo_groups = []
2195 if show_empty_group:
2195 if show_empty_group:
2196 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2196 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2197
2197
2198 repo_groups.extend([cls._generate_choice(x) for x in groups])
2198 repo_groups.extend([cls._generate_choice(x) for x in groups])
2199
2199
2200 repo_groups = sorted(
2200 repo_groups = sorted(
2201 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2201 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2202 return repo_groups
2202 return repo_groups
2203
2203
2204 @classmethod
2204 @classmethod
2205 def url_sep(cls):
2205 def url_sep(cls):
2206 return URL_SEP
2206 return URL_SEP
2207
2207
2208 @classmethod
2208 @classmethod
2209 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2209 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2210 if case_insensitive:
2210 if case_insensitive:
2211 gr = cls.query().filter(func.lower(cls.group_name)
2211 gr = cls.query().filter(func.lower(cls.group_name)
2212 == func.lower(group_name))
2212 == func.lower(group_name))
2213 else:
2213 else:
2214 gr = cls.query().filter(cls.group_name == group_name)
2214 gr = cls.query().filter(cls.group_name == group_name)
2215 if cache:
2215 if cache:
2216 name_key = _hash_key(group_name)
2216 name_key = _hash_key(group_name)
2217 gr = gr.options(
2217 gr = gr.options(
2218 FromCache("sql_cache_short", "get_group_%s" % name_key))
2218 FromCache("sql_cache_short", "get_group_%s" % name_key))
2219 return gr.scalar()
2219 return gr.scalar()
2220
2220
2221 @classmethod
2221 @classmethod
2222 def get_user_personal_repo_group(cls, user_id):
2222 def get_user_personal_repo_group(cls, user_id):
2223 user = User.get(user_id)
2223 user = User.get(user_id)
2224 if user.username == User.DEFAULT_USER:
2224 if user.username == User.DEFAULT_USER:
2225 return None
2225 return None
2226
2226
2227 return cls.query()\
2227 return cls.query()\
2228 .filter(cls.personal == true()) \
2228 .filter(cls.personal == true()) \
2229 .filter(cls.user == user).scalar()
2229 .filter(cls.user == user).scalar()
2230
2230
2231 @classmethod
2231 @classmethod
2232 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2232 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2233 case_insensitive=True):
2233 case_insensitive=True):
2234 q = RepoGroup.query()
2234 q = RepoGroup.query()
2235
2235
2236 if not isinstance(user_id, Optional):
2236 if not isinstance(user_id, Optional):
2237 q = q.filter(RepoGroup.user_id == user_id)
2237 q = q.filter(RepoGroup.user_id == user_id)
2238
2238
2239 if not isinstance(group_id, Optional):
2239 if not isinstance(group_id, Optional):
2240 q = q.filter(RepoGroup.group_parent_id == group_id)
2240 q = q.filter(RepoGroup.group_parent_id == group_id)
2241
2241
2242 if case_insensitive:
2242 if case_insensitive:
2243 q = q.order_by(func.lower(RepoGroup.group_name))
2243 q = q.order_by(func.lower(RepoGroup.group_name))
2244 else:
2244 else:
2245 q = q.order_by(RepoGroup.group_name)
2245 q = q.order_by(RepoGroup.group_name)
2246 return q.all()
2246 return q.all()
2247
2247
2248 @property
2248 @property
2249 def parents(self):
2249 def parents(self):
2250 parents_recursion_limit = 10
2250 parents_recursion_limit = 10
2251 groups = []
2251 groups = []
2252 if self.parent_group is None:
2252 if self.parent_group is None:
2253 return groups
2253 return groups
2254 cur_gr = self.parent_group
2254 cur_gr = self.parent_group
2255 groups.insert(0, cur_gr)
2255 groups.insert(0, cur_gr)
2256 cnt = 0
2256 cnt = 0
2257 while 1:
2257 while 1:
2258 cnt += 1
2258 cnt += 1
2259 gr = getattr(cur_gr, 'parent_group', None)
2259 gr = getattr(cur_gr, 'parent_group', None)
2260 cur_gr = cur_gr.parent_group
2260 cur_gr = cur_gr.parent_group
2261 if gr is None:
2261 if gr is None:
2262 break
2262 break
2263 if cnt == parents_recursion_limit:
2263 if cnt == parents_recursion_limit:
2264 # this will prevent accidental infinit loops
2264 # this will prevent accidental infinit loops
2265 log.error(('more than %s parents found for group %s, stopping '
2265 log.error(('more than %s parents found for group %s, stopping '
2266 'recursive parent fetching' % (parents_recursion_limit, self)))
2266 'recursive parent fetching' % (parents_recursion_limit, self)))
2267 break
2267 break
2268
2268
2269 groups.insert(0, gr)
2269 groups.insert(0, gr)
2270 return groups
2270 return groups
2271
2271
2272 @property
2272 @property
2273 def children(self):
2273 def children(self):
2274 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2274 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2275
2275
2276 @property
2276 @property
2277 def name(self):
2277 def name(self):
2278 return self.group_name.split(RepoGroup.url_sep())[-1]
2278 return self.group_name.split(RepoGroup.url_sep())[-1]
2279
2279
2280 @property
2280 @property
2281 def full_path(self):
2281 def full_path(self):
2282 return self.group_name
2282 return self.group_name
2283
2283
2284 @property
2284 @property
2285 def full_path_splitted(self):
2285 def full_path_splitted(self):
2286 return self.group_name.split(RepoGroup.url_sep())
2286 return self.group_name.split(RepoGroup.url_sep())
2287
2287
2288 @property
2288 @property
2289 def repositories(self):
2289 def repositories(self):
2290 return Repository.query()\
2290 return Repository.query()\
2291 .filter(Repository.group == self)\
2291 .filter(Repository.group == self)\
2292 .order_by(Repository.repo_name)
2292 .order_by(Repository.repo_name)
2293
2293
2294 @property
2294 @property
2295 def repositories_recursive_count(self):
2295 def repositories_recursive_count(self):
2296 cnt = self.repositories.count()
2296 cnt = self.repositories.count()
2297
2297
2298 def children_count(group):
2298 def children_count(group):
2299 cnt = 0
2299 cnt = 0
2300 for child in group.children:
2300 for child in group.children:
2301 cnt += child.repositories.count()
2301 cnt += child.repositories.count()
2302 cnt += children_count(child)
2302 cnt += children_count(child)
2303 return cnt
2303 return cnt
2304
2304
2305 return cnt + children_count(self)
2305 return cnt + children_count(self)
2306
2306
2307 def _recursive_objects(self, include_repos=True):
2307 def _recursive_objects(self, include_repos=True):
2308 all_ = []
2308 all_ = []
2309
2309
2310 def _get_members(root_gr):
2310 def _get_members(root_gr):
2311 if include_repos:
2311 if include_repos:
2312 for r in root_gr.repositories:
2312 for r in root_gr.repositories:
2313 all_.append(r)
2313 all_.append(r)
2314 childs = root_gr.children.all()
2314 childs = root_gr.children.all()
2315 if childs:
2315 if childs:
2316 for gr in childs:
2316 for gr in childs:
2317 all_.append(gr)
2317 all_.append(gr)
2318 _get_members(gr)
2318 _get_members(gr)
2319
2319
2320 _get_members(self)
2320 _get_members(self)
2321 return [self] + all_
2321 return [self] + all_
2322
2322
2323 def recursive_groups_and_repos(self):
2323 def recursive_groups_and_repos(self):
2324 """
2324 """
2325 Recursive return all groups, with repositories in those groups
2325 Recursive return all groups, with repositories in those groups
2326 """
2326 """
2327 return self._recursive_objects()
2327 return self._recursive_objects()
2328
2328
2329 def recursive_groups(self):
2329 def recursive_groups(self):
2330 """
2330 """
2331 Returns all children groups for this group including children of children
2331 Returns all children groups for this group including children of children
2332 """
2332 """
2333 return self._recursive_objects(include_repos=False)
2333 return self._recursive_objects(include_repos=False)
2334
2334
2335 def get_new_name(self, group_name):
2335 def get_new_name(self, group_name):
2336 """
2336 """
2337 returns new full group name based on parent and new name
2337 returns new full group name based on parent and new name
2338
2338
2339 :param group_name:
2339 :param group_name:
2340 """
2340 """
2341 path_prefix = (self.parent_group.full_path_splitted if
2341 path_prefix = (self.parent_group.full_path_splitted if
2342 self.parent_group else [])
2342 self.parent_group else [])
2343 return RepoGroup.url_sep().join(path_prefix + [group_name])
2343 return RepoGroup.url_sep().join(path_prefix + [group_name])
2344
2344
2345 def permissions(self, with_admins=True, with_owner=True):
2345 def permissions(self, with_admins=True, with_owner=True):
2346 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2346 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2347 q = q.options(joinedload(UserRepoGroupToPerm.group),
2347 q = q.options(joinedload(UserRepoGroupToPerm.group),
2348 joinedload(UserRepoGroupToPerm.user),
2348 joinedload(UserRepoGroupToPerm.user),
2349 joinedload(UserRepoGroupToPerm.permission),)
2349 joinedload(UserRepoGroupToPerm.permission),)
2350
2350
2351 # get owners and admins and permissions. We do a trick of re-writing
2351 # get owners and admins and permissions. We do a trick of re-writing
2352 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2352 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2353 # has a global reference and changing one object propagates to all
2353 # has a global reference and changing one object propagates to all
2354 # others. This means if admin is also an owner admin_row that change
2354 # others. This means if admin is also an owner admin_row that change
2355 # would propagate to both objects
2355 # would propagate to both objects
2356 perm_rows = []
2356 perm_rows = []
2357 for _usr in q.all():
2357 for _usr in q.all():
2358 usr = AttributeDict(_usr.user.get_dict())
2358 usr = AttributeDict(_usr.user.get_dict())
2359 usr.permission = _usr.permission.permission_name
2359 usr.permission = _usr.permission.permission_name
2360 perm_rows.append(usr)
2360 perm_rows.append(usr)
2361
2361
2362 # filter the perm rows by 'default' first and then sort them by
2362 # filter the perm rows by 'default' first and then sort them by
2363 # admin,write,read,none permissions sorted again alphabetically in
2363 # admin,write,read,none permissions sorted again alphabetically in
2364 # each group
2364 # each group
2365 perm_rows = sorted(perm_rows, key=display_sort)
2365 perm_rows = sorted(perm_rows, key=display_sort)
2366
2366
2367 _admin_perm = 'group.admin'
2367 _admin_perm = 'group.admin'
2368 owner_row = []
2368 owner_row = []
2369 if with_owner:
2369 if with_owner:
2370 usr = AttributeDict(self.user.get_dict())
2370 usr = AttributeDict(self.user.get_dict())
2371 usr.owner_row = True
2371 usr.owner_row = True
2372 usr.permission = _admin_perm
2372 usr.permission = _admin_perm
2373 owner_row.append(usr)
2373 owner_row.append(usr)
2374
2374
2375 super_admin_rows = []
2375 super_admin_rows = []
2376 if with_admins:
2376 if with_admins:
2377 for usr in User.get_all_super_admins():
2377 for usr in User.get_all_super_admins():
2378 # if this admin is also owner, don't double the record
2378 # if this admin is also owner, don't double the record
2379 if usr.user_id == owner_row[0].user_id:
2379 if usr.user_id == owner_row[0].user_id:
2380 owner_row[0].admin_row = True
2380 owner_row[0].admin_row = True
2381 else:
2381 else:
2382 usr = AttributeDict(usr.get_dict())
2382 usr = AttributeDict(usr.get_dict())
2383 usr.admin_row = True
2383 usr.admin_row = True
2384 usr.permission = _admin_perm
2384 usr.permission = _admin_perm
2385 super_admin_rows.append(usr)
2385 super_admin_rows.append(usr)
2386
2386
2387 return super_admin_rows + owner_row + perm_rows
2387 return super_admin_rows + owner_row + perm_rows
2388
2388
2389 def permission_user_groups(self):
2389 def permission_user_groups(self):
2390 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2390 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2391 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2391 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2392 joinedload(UserGroupRepoGroupToPerm.users_group),
2392 joinedload(UserGroupRepoGroupToPerm.users_group),
2393 joinedload(UserGroupRepoGroupToPerm.permission),)
2393 joinedload(UserGroupRepoGroupToPerm.permission),)
2394
2394
2395 perm_rows = []
2395 perm_rows = []
2396 for _user_group in q.all():
2396 for _user_group in q.all():
2397 usr = AttributeDict(_user_group.users_group.get_dict())
2397 usr = AttributeDict(_user_group.users_group.get_dict())
2398 usr.permission = _user_group.permission.permission_name
2398 usr.permission = _user_group.permission.permission_name
2399 perm_rows.append(usr)
2399 perm_rows.append(usr)
2400
2400
2401 return perm_rows
2401 return perm_rows
2402
2402
2403 def get_api_data(self):
2403 def get_api_data(self):
2404 """
2404 """
2405 Common function for generating api data
2405 Common function for generating api data
2406
2406
2407 """
2407 """
2408 group = self
2408 group = self
2409 data = {
2409 data = {
2410 'group_id': group.group_id,
2410 'group_id': group.group_id,
2411 'group_name': group.group_name,
2411 'group_name': group.group_name,
2412 'group_description': group.group_description,
2412 'group_description': group.group_description,
2413 'parent_group': group.parent_group.group_name if group.parent_group else None,
2413 'parent_group': group.parent_group.group_name if group.parent_group else None,
2414 'repositories': [x.repo_name for x in group.repositories],
2414 'repositories': [x.repo_name for x in group.repositories],
2415 'owner': group.user.username,
2415 'owner': group.user.username,
2416 }
2416 }
2417 return data
2417 return data
2418
2418
2419
2419
2420 class Permission(Base, BaseModel):
2420 class Permission(Base, BaseModel):
2421 __tablename__ = 'permissions'
2421 __tablename__ = 'permissions'
2422 __table_args__ = (
2422 __table_args__ = (
2423 Index('p_perm_name_idx', 'permission_name'),
2423 Index('p_perm_name_idx', 'permission_name'),
2424 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2424 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2425 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2425 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2426 )
2426 )
2427 PERMS = [
2427 PERMS = [
2428 ('hg.admin', _('RhodeCode Super Administrator')),
2428 ('hg.admin', _('RhodeCode Super Administrator')),
2429
2429
2430 ('repository.none', _('Repository no access')),
2430 ('repository.none', _('Repository no access')),
2431 ('repository.read', _('Repository read access')),
2431 ('repository.read', _('Repository read access')),
2432 ('repository.write', _('Repository write access')),
2432 ('repository.write', _('Repository write access')),
2433 ('repository.admin', _('Repository admin access')),
2433 ('repository.admin', _('Repository admin access')),
2434
2434
2435 ('group.none', _('Repository group no access')),
2435 ('group.none', _('Repository group no access')),
2436 ('group.read', _('Repository group read access')),
2436 ('group.read', _('Repository group read access')),
2437 ('group.write', _('Repository group write access')),
2437 ('group.write', _('Repository group write access')),
2438 ('group.admin', _('Repository group admin access')),
2438 ('group.admin', _('Repository group admin access')),
2439
2439
2440 ('usergroup.none', _('User group no access')),
2440 ('usergroup.none', _('User group no access')),
2441 ('usergroup.read', _('User group read access')),
2441 ('usergroup.read', _('User group read access')),
2442 ('usergroup.write', _('User group write access')),
2442 ('usergroup.write', _('User group write access')),
2443 ('usergroup.admin', _('User group admin access')),
2443 ('usergroup.admin', _('User group admin access')),
2444
2444
2445 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2445 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2446 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2446 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2447
2447
2448 ('hg.usergroup.create.false', _('User Group creation disabled')),
2448 ('hg.usergroup.create.false', _('User Group creation disabled')),
2449 ('hg.usergroup.create.true', _('User Group creation enabled')),
2449 ('hg.usergroup.create.true', _('User Group creation enabled')),
2450
2450
2451 ('hg.create.none', _('Repository creation disabled')),
2451 ('hg.create.none', _('Repository creation disabled')),
2452 ('hg.create.repository', _('Repository creation enabled')),
2452 ('hg.create.repository', _('Repository creation enabled')),
2453 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2453 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2454 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2454 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2455
2455
2456 ('hg.fork.none', _('Repository forking disabled')),
2456 ('hg.fork.none', _('Repository forking disabled')),
2457 ('hg.fork.repository', _('Repository forking enabled')),
2457 ('hg.fork.repository', _('Repository forking enabled')),
2458
2458
2459 ('hg.register.none', _('Registration disabled')),
2459 ('hg.register.none', _('Registration disabled')),
2460 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2460 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2461 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2461 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2462
2462
2463 ('hg.password_reset.enabled', _('Password reset enabled')),
2463 ('hg.password_reset.enabled', _('Password reset enabled')),
2464 ('hg.password_reset.hidden', _('Password reset hidden')),
2464 ('hg.password_reset.hidden', _('Password reset hidden')),
2465 ('hg.password_reset.disabled', _('Password reset disabled')),
2465 ('hg.password_reset.disabled', _('Password reset disabled')),
2466
2466
2467 ('hg.extern_activate.manual', _('Manual activation of external account')),
2467 ('hg.extern_activate.manual', _('Manual activation of external account')),
2468 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2468 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2469
2469
2470 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2470 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2471 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2471 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2472 ]
2472 ]
2473
2473
2474 # definition of system default permissions for DEFAULT user
2474 # definition of system default permissions for DEFAULT user
2475 DEFAULT_USER_PERMISSIONS = [
2475 DEFAULT_USER_PERMISSIONS = [
2476 'repository.read',
2476 'repository.read',
2477 'group.read',
2477 'group.read',
2478 'usergroup.read',
2478 'usergroup.read',
2479 'hg.create.repository',
2479 'hg.create.repository',
2480 'hg.repogroup.create.false',
2480 'hg.repogroup.create.false',
2481 'hg.usergroup.create.false',
2481 'hg.usergroup.create.false',
2482 'hg.create.write_on_repogroup.true',
2482 'hg.create.write_on_repogroup.true',
2483 'hg.fork.repository',
2483 'hg.fork.repository',
2484 'hg.register.manual_activate',
2484 'hg.register.manual_activate',
2485 'hg.password_reset.enabled',
2485 'hg.password_reset.enabled',
2486 'hg.extern_activate.auto',
2486 'hg.extern_activate.auto',
2487 'hg.inherit_default_perms.true',
2487 'hg.inherit_default_perms.true',
2488 ]
2488 ]
2489
2489
2490 # defines which permissions are more important higher the more important
2490 # defines which permissions are more important higher the more important
2491 # Weight defines which permissions are more important.
2491 # Weight defines which permissions are more important.
2492 # The higher number the more important.
2492 # The higher number the more important.
2493 PERM_WEIGHTS = {
2493 PERM_WEIGHTS = {
2494 'repository.none': 0,
2494 'repository.none': 0,
2495 'repository.read': 1,
2495 'repository.read': 1,
2496 'repository.write': 3,
2496 'repository.write': 3,
2497 'repository.admin': 4,
2497 'repository.admin': 4,
2498
2498
2499 'group.none': 0,
2499 'group.none': 0,
2500 'group.read': 1,
2500 'group.read': 1,
2501 'group.write': 3,
2501 'group.write': 3,
2502 'group.admin': 4,
2502 'group.admin': 4,
2503
2503
2504 'usergroup.none': 0,
2504 'usergroup.none': 0,
2505 'usergroup.read': 1,
2505 'usergroup.read': 1,
2506 'usergroup.write': 3,
2506 'usergroup.write': 3,
2507 'usergroup.admin': 4,
2507 'usergroup.admin': 4,
2508
2508
2509 'hg.repogroup.create.false': 0,
2509 'hg.repogroup.create.false': 0,
2510 'hg.repogroup.create.true': 1,
2510 'hg.repogroup.create.true': 1,
2511
2511
2512 'hg.usergroup.create.false': 0,
2512 'hg.usergroup.create.false': 0,
2513 'hg.usergroup.create.true': 1,
2513 'hg.usergroup.create.true': 1,
2514
2514
2515 'hg.fork.none': 0,
2515 'hg.fork.none': 0,
2516 'hg.fork.repository': 1,
2516 'hg.fork.repository': 1,
2517 'hg.create.none': 0,
2517 'hg.create.none': 0,
2518 'hg.create.repository': 1
2518 'hg.create.repository': 1
2519 }
2519 }
2520
2520
2521 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2522 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2522 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2523 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2523 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2524
2524
2525 def __unicode__(self):
2525 def __unicode__(self):
2526 return u"<%s('%s:%s')>" % (
2526 return u"<%s('%s:%s')>" % (
2527 self.__class__.__name__, self.permission_id, self.permission_name
2527 self.__class__.__name__, self.permission_id, self.permission_name
2528 )
2528 )
2529
2529
2530 @classmethod
2530 @classmethod
2531 def get_by_key(cls, key):
2531 def get_by_key(cls, key):
2532 return cls.query().filter(cls.permission_name == key).scalar()
2532 return cls.query().filter(cls.permission_name == key).scalar()
2533
2533
2534 @classmethod
2534 @classmethod
2535 def get_default_repo_perms(cls, user_id, repo_id=None):
2535 def get_default_repo_perms(cls, user_id, repo_id=None):
2536 q = Session().query(UserRepoToPerm, Repository, Permission)\
2536 q = Session().query(UserRepoToPerm, Repository, Permission)\
2537 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2537 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2538 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2538 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2539 .filter(UserRepoToPerm.user_id == user_id)
2539 .filter(UserRepoToPerm.user_id == user_id)
2540 if repo_id:
2540 if repo_id:
2541 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2541 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2542 return q.all()
2542 return q.all()
2543
2543
2544 @classmethod
2544 @classmethod
2545 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2545 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2546 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2546 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2547 .join(
2547 .join(
2548 Permission,
2548 Permission,
2549 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2549 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2550 .join(
2550 .join(
2551 Repository,
2551 Repository,
2552 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2552 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2553 .join(
2553 .join(
2554 UserGroup,
2554 UserGroup,
2555 UserGroupRepoToPerm.users_group_id ==
2555 UserGroupRepoToPerm.users_group_id ==
2556 UserGroup.users_group_id)\
2556 UserGroup.users_group_id)\
2557 .join(
2557 .join(
2558 UserGroupMember,
2558 UserGroupMember,
2559 UserGroupRepoToPerm.users_group_id ==
2559 UserGroupRepoToPerm.users_group_id ==
2560 UserGroupMember.users_group_id)\
2560 UserGroupMember.users_group_id)\
2561 .filter(
2561 .filter(
2562 UserGroupMember.user_id == user_id,
2562 UserGroupMember.user_id == user_id,
2563 UserGroup.users_group_active == true())
2563 UserGroup.users_group_active == true())
2564 if repo_id:
2564 if repo_id:
2565 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2565 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2566 return q.all()
2566 return q.all()
2567
2567
2568 @classmethod
2568 @classmethod
2569 def get_default_group_perms(cls, user_id, repo_group_id=None):
2569 def get_default_group_perms(cls, user_id, repo_group_id=None):
2570 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2570 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2571 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2571 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2572 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2572 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2573 .filter(UserRepoGroupToPerm.user_id == user_id)
2573 .filter(UserRepoGroupToPerm.user_id == user_id)
2574 if repo_group_id:
2574 if repo_group_id:
2575 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2575 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2576 return q.all()
2576 return q.all()
2577
2577
2578 @classmethod
2578 @classmethod
2579 def get_default_group_perms_from_user_group(
2579 def get_default_group_perms_from_user_group(
2580 cls, user_id, repo_group_id=None):
2580 cls, user_id, repo_group_id=None):
2581 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2581 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2582 .join(
2582 .join(
2583 Permission,
2583 Permission,
2584 UserGroupRepoGroupToPerm.permission_id ==
2584 UserGroupRepoGroupToPerm.permission_id ==
2585 Permission.permission_id)\
2585 Permission.permission_id)\
2586 .join(
2586 .join(
2587 RepoGroup,
2587 RepoGroup,
2588 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2588 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2589 .join(
2589 .join(
2590 UserGroup,
2590 UserGroup,
2591 UserGroupRepoGroupToPerm.users_group_id ==
2591 UserGroupRepoGroupToPerm.users_group_id ==
2592 UserGroup.users_group_id)\
2592 UserGroup.users_group_id)\
2593 .join(
2593 .join(
2594 UserGroupMember,
2594 UserGroupMember,
2595 UserGroupRepoGroupToPerm.users_group_id ==
2595 UserGroupRepoGroupToPerm.users_group_id ==
2596 UserGroupMember.users_group_id)\
2596 UserGroupMember.users_group_id)\
2597 .filter(
2597 .filter(
2598 UserGroupMember.user_id == user_id,
2598 UserGroupMember.user_id == user_id,
2599 UserGroup.users_group_active == true())
2599 UserGroup.users_group_active == true())
2600 if repo_group_id:
2600 if repo_group_id:
2601 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2601 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2602 return q.all()
2602 return q.all()
2603
2603
2604 @classmethod
2604 @classmethod
2605 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2605 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2606 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2606 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2607 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2607 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2608 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2608 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2609 .filter(UserUserGroupToPerm.user_id == user_id)
2609 .filter(UserUserGroupToPerm.user_id == user_id)
2610 if user_group_id:
2610 if user_group_id:
2611 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2611 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2612 return q.all()
2612 return q.all()
2613
2613
2614 @classmethod
2614 @classmethod
2615 def get_default_user_group_perms_from_user_group(
2615 def get_default_user_group_perms_from_user_group(
2616 cls, user_id, user_group_id=None):
2616 cls, user_id, user_group_id=None):
2617 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2617 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2618 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2618 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2619 .join(
2619 .join(
2620 Permission,
2620 Permission,
2621 UserGroupUserGroupToPerm.permission_id ==
2621 UserGroupUserGroupToPerm.permission_id ==
2622 Permission.permission_id)\
2622 Permission.permission_id)\
2623 .join(
2623 .join(
2624 TargetUserGroup,
2624 TargetUserGroup,
2625 UserGroupUserGroupToPerm.target_user_group_id ==
2625 UserGroupUserGroupToPerm.target_user_group_id ==
2626 TargetUserGroup.users_group_id)\
2626 TargetUserGroup.users_group_id)\
2627 .join(
2627 .join(
2628 UserGroup,
2628 UserGroup,
2629 UserGroupUserGroupToPerm.user_group_id ==
2629 UserGroupUserGroupToPerm.user_group_id ==
2630 UserGroup.users_group_id)\
2630 UserGroup.users_group_id)\
2631 .join(
2631 .join(
2632 UserGroupMember,
2632 UserGroupMember,
2633 UserGroupUserGroupToPerm.user_group_id ==
2633 UserGroupUserGroupToPerm.user_group_id ==
2634 UserGroupMember.users_group_id)\
2634 UserGroupMember.users_group_id)\
2635 .filter(
2635 .filter(
2636 UserGroupMember.user_id == user_id,
2636 UserGroupMember.user_id == user_id,
2637 UserGroup.users_group_active == true())
2637 UserGroup.users_group_active == true())
2638 if user_group_id:
2638 if user_group_id:
2639 q = q.filter(
2639 q = q.filter(
2640 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2640 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2641
2641
2642 return q.all()
2642 return q.all()
2643
2643
2644
2644
2645 class UserRepoToPerm(Base, BaseModel):
2645 class UserRepoToPerm(Base, BaseModel):
2646 __tablename__ = 'repo_to_perm'
2646 __tablename__ = 'repo_to_perm'
2647 __table_args__ = (
2647 __table_args__ = (
2648 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2648 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2651 )
2651 )
2652 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2652 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2653 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2653 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2654 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2654 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2655 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2655 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2656
2656
2657 user = relationship('User')
2657 user = relationship('User')
2658 repository = relationship('Repository')
2658 repository = relationship('Repository')
2659 permission = relationship('Permission')
2659 permission = relationship('Permission')
2660
2660
2661 @classmethod
2661 @classmethod
2662 def create(cls, user, repository, permission):
2662 def create(cls, user, repository, permission):
2663 n = cls()
2663 n = cls()
2664 n.user = user
2664 n.user = user
2665 n.repository = repository
2665 n.repository = repository
2666 n.permission = permission
2666 n.permission = permission
2667 Session().add(n)
2667 Session().add(n)
2668 return n
2668 return n
2669
2669
2670 def __unicode__(self):
2670 def __unicode__(self):
2671 return u'<%s => %s >' % (self.user, self.repository)
2671 return u'<%s => %s >' % (self.user, self.repository)
2672
2672
2673
2673
2674 class UserUserGroupToPerm(Base, BaseModel):
2674 class UserUserGroupToPerm(Base, BaseModel):
2675 __tablename__ = 'user_user_group_to_perm'
2675 __tablename__ = 'user_user_group_to_perm'
2676 __table_args__ = (
2676 __table_args__ = (
2677 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2677 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2680 )
2680 )
2681 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2681 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2682 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2682 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2683 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2683 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2684 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2685
2685
2686 user = relationship('User')
2686 user = relationship('User')
2687 user_group = relationship('UserGroup')
2687 user_group = relationship('UserGroup')
2688 permission = relationship('Permission')
2688 permission = relationship('Permission')
2689
2689
2690 @classmethod
2690 @classmethod
2691 def create(cls, user, user_group, permission):
2691 def create(cls, user, user_group, permission):
2692 n = cls()
2692 n = cls()
2693 n.user = user
2693 n.user = user
2694 n.user_group = user_group
2694 n.user_group = user_group
2695 n.permission = permission
2695 n.permission = permission
2696 Session().add(n)
2696 Session().add(n)
2697 return n
2697 return n
2698
2698
2699 def __unicode__(self):
2699 def __unicode__(self):
2700 return u'<%s => %s >' % (self.user, self.user_group)
2700 return u'<%s => %s >' % (self.user, self.user_group)
2701
2701
2702
2702
2703 class UserToPerm(Base, BaseModel):
2703 class UserToPerm(Base, BaseModel):
2704 __tablename__ = 'user_to_perm'
2704 __tablename__ = 'user_to_perm'
2705 __table_args__ = (
2705 __table_args__ = (
2706 UniqueConstraint('user_id', 'permission_id'),
2706 UniqueConstraint('user_id', 'permission_id'),
2707 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2708 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2709 )
2709 )
2710 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2711 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2711 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2712 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2712 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2713
2713
2714 user = relationship('User')
2714 user = relationship('User')
2715 permission = relationship('Permission', lazy='joined')
2715 permission = relationship('Permission', lazy='joined')
2716
2716
2717 def __unicode__(self):
2717 def __unicode__(self):
2718 return u'<%s => %s >' % (self.user, self.permission)
2718 return u'<%s => %s >' % (self.user, self.permission)
2719
2719
2720
2720
2721 class UserGroupRepoToPerm(Base, BaseModel):
2721 class UserGroupRepoToPerm(Base, BaseModel):
2722 __tablename__ = 'users_group_repo_to_perm'
2722 __tablename__ = 'users_group_repo_to_perm'
2723 __table_args__ = (
2723 __table_args__ = (
2724 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2724 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2725 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2726 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2727 )
2727 )
2728 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2729 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2729 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2730 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2730 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2731 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2731 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2732
2732
2733 users_group = relationship('UserGroup')
2733 users_group = relationship('UserGroup')
2734 permission = relationship('Permission')
2734 permission = relationship('Permission')
2735 repository = relationship('Repository')
2735 repository = relationship('Repository')
2736
2736
2737 @classmethod
2737 @classmethod
2738 def create(cls, users_group, repository, permission):
2738 def create(cls, users_group, repository, permission):
2739 n = cls()
2739 n = cls()
2740 n.users_group = users_group
2740 n.users_group = users_group
2741 n.repository = repository
2741 n.repository = repository
2742 n.permission = permission
2742 n.permission = permission
2743 Session().add(n)
2743 Session().add(n)
2744 return n
2744 return n
2745
2745
2746 def __unicode__(self):
2746 def __unicode__(self):
2747 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2747 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2748
2748
2749
2749
2750 class UserGroupUserGroupToPerm(Base, BaseModel):
2750 class UserGroupUserGroupToPerm(Base, BaseModel):
2751 __tablename__ = 'user_group_user_group_to_perm'
2751 __tablename__ = 'user_group_user_group_to_perm'
2752 __table_args__ = (
2752 __table_args__ = (
2753 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2753 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2754 CheckConstraint('target_user_group_id != user_group_id'),
2754 CheckConstraint('target_user_group_id != user_group_id'),
2755 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2755 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2756 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2756 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2757 )
2757 )
2758 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2758 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2759 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2759 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2760 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2760 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2761 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2761 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2762
2762
2763 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2763 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2764 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2764 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2765 permission = relationship('Permission')
2765 permission = relationship('Permission')
2766
2766
2767 @classmethod
2767 @classmethod
2768 def create(cls, target_user_group, user_group, permission):
2768 def create(cls, target_user_group, user_group, permission):
2769 n = cls()
2769 n = cls()
2770 n.target_user_group = target_user_group
2770 n.target_user_group = target_user_group
2771 n.user_group = user_group
2771 n.user_group = user_group
2772 n.permission = permission
2772 n.permission = permission
2773 Session().add(n)
2773 Session().add(n)
2774 return n
2774 return n
2775
2775
2776 def __unicode__(self):
2776 def __unicode__(self):
2777 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2777 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2778
2778
2779
2779
2780 class UserGroupToPerm(Base, BaseModel):
2780 class UserGroupToPerm(Base, BaseModel):
2781 __tablename__ = 'users_group_to_perm'
2781 __tablename__ = 'users_group_to_perm'
2782 __table_args__ = (
2782 __table_args__ = (
2783 UniqueConstraint('users_group_id', 'permission_id',),
2783 UniqueConstraint('users_group_id', 'permission_id',),
2784 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2784 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2785 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2785 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2786 )
2786 )
2787 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2787 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2788 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2788 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2789 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2789 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2790
2790
2791 users_group = relationship('UserGroup')
2791 users_group = relationship('UserGroup')
2792 permission = relationship('Permission')
2792 permission = relationship('Permission')
2793
2793
2794
2794
2795 class UserRepoGroupToPerm(Base, BaseModel):
2795 class UserRepoGroupToPerm(Base, BaseModel):
2796 __tablename__ = 'user_repo_group_to_perm'
2796 __tablename__ = 'user_repo_group_to_perm'
2797 __table_args__ = (
2797 __table_args__ = (
2798 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2798 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2799 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2799 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2800 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2800 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2801 )
2801 )
2802
2802
2803 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2803 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2804 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2804 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2805 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2805 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2807
2807
2808 user = relationship('User')
2808 user = relationship('User')
2809 group = relationship('RepoGroup')
2809 group = relationship('RepoGroup')
2810 permission = relationship('Permission')
2810 permission = relationship('Permission')
2811
2811
2812 @classmethod
2812 @classmethod
2813 def create(cls, user, repository_group, permission):
2813 def create(cls, user, repository_group, permission):
2814 n = cls()
2814 n = cls()
2815 n.user = user
2815 n.user = user
2816 n.group = repository_group
2816 n.group = repository_group
2817 n.permission = permission
2817 n.permission = permission
2818 Session().add(n)
2818 Session().add(n)
2819 return n
2819 return n
2820
2820
2821
2821
2822 class UserGroupRepoGroupToPerm(Base, BaseModel):
2822 class UserGroupRepoGroupToPerm(Base, BaseModel):
2823 __tablename__ = 'users_group_repo_group_to_perm'
2823 __tablename__ = 'users_group_repo_group_to_perm'
2824 __table_args__ = (
2824 __table_args__ = (
2825 UniqueConstraint('users_group_id', 'group_id'),
2825 UniqueConstraint('users_group_id', 'group_id'),
2826 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2826 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2827 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2827 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2828 )
2828 )
2829
2829
2830 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2830 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2831 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2831 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2832 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2832 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2833 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2833 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2834
2834
2835 users_group = relationship('UserGroup')
2835 users_group = relationship('UserGroup')
2836 permission = relationship('Permission')
2836 permission = relationship('Permission')
2837 group = relationship('RepoGroup')
2837 group = relationship('RepoGroup')
2838
2838
2839 @classmethod
2839 @classmethod
2840 def create(cls, user_group, repository_group, permission):
2840 def create(cls, user_group, repository_group, permission):
2841 n = cls()
2841 n = cls()
2842 n.users_group = user_group
2842 n.users_group = user_group
2843 n.group = repository_group
2843 n.group = repository_group
2844 n.permission = permission
2844 n.permission = permission
2845 Session().add(n)
2845 Session().add(n)
2846 return n
2846 return n
2847
2847
2848 def __unicode__(self):
2848 def __unicode__(self):
2849 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2849 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2850
2850
2851
2851
2852 class Statistics(Base, BaseModel):
2852 class Statistics(Base, BaseModel):
2853 __tablename__ = 'statistics'
2853 __tablename__ = 'statistics'
2854 __table_args__ = (
2854 __table_args__ = (
2855 UniqueConstraint('repository_id'),
2855 UniqueConstraint('repository_id'),
2856 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2856 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2857 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2857 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2858 )
2858 )
2859 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2859 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2860 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2860 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2861 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2861 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2862 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2862 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2863 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2863 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2864 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2864 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2865
2865
2866 repository = relationship('Repository', single_parent=True)
2866 repository = relationship('Repository', single_parent=True)
2867
2867
2868
2868
2869 class UserFollowing(Base, BaseModel):
2869 class UserFollowing(Base, BaseModel):
2870 __tablename__ = 'user_followings'
2870 __tablename__ = 'user_followings'
2871 __table_args__ = (
2871 __table_args__ = (
2872 UniqueConstraint('user_id', 'follows_repository_id'),
2872 UniqueConstraint('user_id', 'follows_repository_id'),
2873 UniqueConstraint('user_id', 'follows_user_id'),
2873 UniqueConstraint('user_id', 'follows_user_id'),
2874 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2874 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2875 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2875 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2876 )
2876 )
2877
2877
2878 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2878 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2879 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2879 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2880 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2880 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2881 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2881 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2882 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2882 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2883
2883
2884 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2884 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2885
2885
2886 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2886 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2887 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2887 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2888
2888
2889 @classmethod
2889 @classmethod
2890 def get_repo_followers(cls, repo_id):
2890 def get_repo_followers(cls, repo_id):
2891 return cls.query().filter(cls.follows_repo_id == repo_id)
2891 return cls.query().filter(cls.follows_repo_id == repo_id)
2892
2892
2893
2893
2894 class CacheKey(Base, BaseModel):
2894 class CacheKey(Base, BaseModel):
2895 __tablename__ = 'cache_invalidation'
2895 __tablename__ = 'cache_invalidation'
2896 __table_args__ = (
2896 __table_args__ = (
2897 UniqueConstraint('cache_key'),
2897 UniqueConstraint('cache_key'),
2898 Index('key_idx', 'cache_key'),
2898 Index('key_idx', 'cache_key'),
2899 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2899 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2900 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2900 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2901 )
2901 )
2902 CACHE_TYPE_ATOM = 'ATOM'
2902 CACHE_TYPE_ATOM = 'ATOM'
2903 CACHE_TYPE_RSS = 'RSS'
2903 CACHE_TYPE_RSS = 'RSS'
2904 CACHE_TYPE_README = 'README'
2904 CACHE_TYPE_README = 'README'
2905
2905
2906 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2906 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2907 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2907 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2908 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2908 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2909 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2909 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2910
2910
2911 def __init__(self, cache_key, cache_args=''):
2911 def __init__(self, cache_key, cache_args=''):
2912 self.cache_key = cache_key
2912 self.cache_key = cache_key
2913 self.cache_args = cache_args
2913 self.cache_args = cache_args
2914 self.cache_active = False
2914 self.cache_active = False
2915
2915
2916 def __unicode__(self):
2916 def __unicode__(self):
2917 return u"<%s('%s:%s[%s]')>" % (
2917 return u"<%s('%s:%s[%s]')>" % (
2918 self.__class__.__name__,
2918 self.__class__.__name__,
2919 self.cache_id, self.cache_key, self.cache_active)
2919 self.cache_id, self.cache_key, self.cache_active)
2920
2920
2921 def _cache_key_partition(self):
2921 def _cache_key_partition(self):
2922 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2922 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2923 return prefix, repo_name, suffix
2923 return prefix, repo_name, suffix
2924
2924
2925 def get_prefix(self):
2925 def get_prefix(self):
2926 """
2926 """
2927 Try to extract prefix from existing cache key. The key could consist
2927 Try to extract prefix from existing cache key. The key could consist
2928 of prefix, repo_name, suffix
2928 of prefix, repo_name, suffix
2929 """
2929 """
2930 # this returns prefix, repo_name, suffix
2930 # this returns prefix, repo_name, suffix
2931 return self._cache_key_partition()[0]
2931 return self._cache_key_partition()[0]
2932
2932
2933 def get_suffix(self):
2933 def get_suffix(self):
2934 """
2934 """
2935 get suffix that might have been used in _get_cache_key to
2935 get suffix that might have been used in _get_cache_key to
2936 generate self.cache_key. Only used for informational purposes
2936 generate self.cache_key. Only used for informational purposes
2937 in repo_edit.mako.
2937 in repo_edit.mako.
2938 """
2938 """
2939 # prefix, repo_name, suffix
2939 # prefix, repo_name, suffix
2940 return self._cache_key_partition()[2]
2940 return self._cache_key_partition()[2]
2941
2941
2942 @classmethod
2942 @classmethod
2943 def delete_all_cache(cls):
2943 def delete_all_cache(cls):
2944 """
2944 """
2945 Delete all cache keys from database.
2945 Delete all cache keys from database.
2946 Should only be run when all instances are down and all entries
2946 Should only be run when all instances are down and all entries
2947 thus stale.
2947 thus stale.
2948 """
2948 """
2949 cls.query().delete()
2949 cls.query().delete()
2950 Session().commit()
2950 Session().commit()
2951
2951
2952 @classmethod
2952 @classmethod
2953 def get_cache_key(cls, repo_name, cache_type):
2953 def get_cache_key(cls, repo_name, cache_type):
2954 """
2954 """
2955
2955
2956 Generate a cache key for this process of RhodeCode instance.
2956 Generate a cache key for this process of RhodeCode instance.
2957 Prefix most likely will be process id or maybe explicitly set
2957 Prefix most likely will be process id or maybe explicitly set
2958 instance_id from .ini file.
2958 instance_id from .ini file.
2959 """
2959 """
2960 import rhodecode
2960 import rhodecode
2961 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2961 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2962
2962
2963 repo_as_unicode = safe_unicode(repo_name)
2963 repo_as_unicode = safe_unicode(repo_name)
2964 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2964 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2965 if cache_type else repo_as_unicode
2965 if cache_type else repo_as_unicode
2966
2966
2967 return u'{}{}'.format(prefix, key)
2967 return u'{}{}'.format(prefix, key)
2968
2968
2969 @classmethod
2969 @classmethod
2970 def set_invalidate(cls, repo_name, delete=False):
2970 def set_invalidate(cls, repo_name, delete=False):
2971 """
2971 """
2972 Mark all caches of a repo as invalid in the database.
2972 Mark all caches of a repo as invalid in the database.
2973 """
2973 """
2974
2974
2975 try:
2975 try:
2976 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2976 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2977 if delete:
2977 if delete:
2978 log.debug('cache objects deleted for repo %s',
2978 log.debug('cache objects deleted for repo %s',
2979 safe_str(repo_name))
2979 safe_str(repo_name))
2980 qry.delete()
2980 qry.delete()
2981 else:
2981 else:
2982 log.debug('cache objects marked as invalid for repo %s',
2982 log.debug('cache objects marked as invalid for repo %s',
2983 safe_str(repo_name))
2983 safe_str(repo_name))
2984 qry.update({"cache_active": False})
2984 qry.update({"cache_active": False})
2985
2985
2986 Session().commit()
2986 Session().commit()
2987 except Exception:
2987 except Exception:
2988 log.exception(
2988 log.exception(
2989 'Cache key invalidation failed for repository %s',
2989 'Cache key invalidation failed for repository %s',
2990 safe_str(repo_name))
2990 safe_str(repo_name))
2991 Session().rollback()
2991 Session().rollback()
2992
2992
2993 @classmethod
2993 @classmethod
2994 def get_active_cache(cls, cache_key):
2994 def get_active_cache(cls, cache_key):
2995 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2995 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2996 if inv_obj:
2996 if inv_obj:
2997 return inv_obj
2997 return inv_obj
2998 return None
2998 return None
2999
2999
3000 @classmethod
3000 @classmethod
3001 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3001 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3002 thread_scoped=False):
3002 thread_scoped=False):
3003 """
3003 """
3004 @cache_region('long_term')
3004 @cache_region('long_term')
3005 def _heavy_calculation(cache_key):
3005 def _heavy_calculation(cache_key):
3006 return 'result'
3006 return 'result'
3007
3007
3008 cache_context = CacheKey.repo_context_cache(
3008 cache_context = CacheKey.repo_context_cache(
3009 _heavy_calculation, repo_name, cache_type)
3009 _heavy_calculation, repo_name, cache_type)
3010
3010
3011 with cache_context as context:
3011 with cache_context as context:
3012 context.invalidate()
3012 context.invalidate()
3013 computed = context.compute()
3013 computed = context.compute()
3014
3014
3015 assert computed == 'result'
3015 assert computed == 'result'
3016 """
3016 """
3017 from rhodecode.lib import caches
3017 from rhodecode.lib import caches
3018 return caches.InvalidationContext(
3018 return caches.InvalidationContext(
3019 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3019 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3020
3020
3021
3021
3022 class ChangesetComment(Base, BaseModel):
3022 class ChangesetComment(Base, BaseModel):
3023 __tablename__ = 'changeset_comments'
3023 __tablename__ = 'changeset_comments'
3024 __table_args__ = (
3024 __table_args__ = (
3025 Index('cc_revision_idx', 'revision'),
3025 Index('cc_revision_idx', 'revision'),
3026 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3026 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3027 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3027 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3028 )
3028 )
3029
3029
3030 COMMENT_OUTDATED = u'comment_outdated'
3030 COMMENT_OUTDATED = u'comment_outdated'
3031 COMMENT_TYPE_NOTE = u'note'
3031 COMMENT_TYPE_NOTE = u'note'
3032 COMMENT_TYPE_TODO = u'todo'
3032 COMMENT_TYPE_TODO = u'todo'
3033 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3033 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3034
3034
3035 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3035 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3036 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3036 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3037 revision = Column('revision', String(40), nullable=True)
3037 revision = Column('revision', String(40), nullable=True)
3038 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3038 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3039 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3039 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3040 line_no = Column('line_no', Unicode(10), nullable=True)
3040 line_no = Column('line_no', Unicode(10), nullable=True)
3041 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3041 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3042 f_path = Column('f_path', Unicode(1000), nullable=True)
3042 f_path = Column('f_path', Unicode(1000), nullable=True)
3043 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3043 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3044 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3044 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3045 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3046 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3046 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3047 renderer = Column('renderer', Unicode(64), nullable=True)
3047 renderer = Column('renderer', Unicode(64), nullable=True)
3048 display_state = Column('display_state', Unicode(128), nullable=True)
3048 display_state = Column('display_state', Unicode(128), nullable=True)
3049
3049
3050 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3050 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3051 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3051 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3052 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3052 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3053 author = relationship('User', lazy='joined')
3053 author = relationship('User', lazy='joined')
3054 repo = relationship('Repository')
3054 repo = relationship('Repository')
3055 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3055 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3056 pull_request = relationship('PullRequest', lazy='joined')
3056 pull_request = relationship('PullRequest', lazy='joined')
3057 pull_request_version = relationship('PullRequestVersion')
3057 pull_request_version = relationship('PullRequestVersion')
3058
3058
3059 @classmethod
3059 @classmethod
3060 def get_users(cls, revision=None, pull_request_id=None):
3060 def get_users(cls, revision=None, pull_request_id=None):
3061 """
3061 """
3062 Returns user associated with this ChangesetComment. ie those
3062 Returns user associated with this ChangesetComment. ie those
3063 who actually commented
3063 who actually commented
3064
3064
3065 :param cls:
3065 :param cls:
3066 :param revision:
3066 :param revision:
3067 """
3067 """
3068 q = Session().query(User)\
3068 q = Session().query(User)\
3069 .join(ChangesetComment.author)
3069 .join(ChangesetComment.author)
3070 if revision:
3070 if revision:
3071 q = q.filter(cls.revision == revision)
3071 q = q.filter(cls.revision == revision)
3072 elif pull_request_id:
3072 elif pull_request_id:
3073 q = q.filter(cls.pull_request_id == pull_request_id)
3073 q = q.filter(cls.pull_request_id == pull_request_id)
3074 return q.all()
3074 return q.all()
3075
3075
3076 @classmethod
3076 @classmethod
3077 def get_index_from_version(cls, pr_version, versions):
3077 def get_index_from_version(cls, pr_version, versions):
3078 num_versions = [x.pull_request_version_id for x in versions]
3078 num_versions = [x.pull_request_version_id for x in versions]
3079 try:
3079 try:
3080 return num_versions.index(pr_version) +1
3080 return num_versions.index(pr_version) +1
3081 except (IndexError, ValueError):
3081 except (IndexError, ValueError):
3082 return
3082 return
3083
3083
3084 @property
3084 @property
3085 def outdated(self):
3085 def outdated(self):
3086 return self.display_state == self.COMMENT_OUTDATED
3086 return self.display_state == self.COMMENT_OUTDATED
3087
3087
3088 def outdated_at_version(self, version):
3088 def outdated_at_version(self, version):
3089 """
3089 """
3090 Checks if comment is outdated for given pull request version
3090 Checks if comment is outdated for given pull request version
3091 """
3091 """
3092 return self.outdated and self.pull_request_version_id != version
3092 return self.outdated and self.pull_request_version_id != version
3093
3093
3094 def older_than_version(self, version):
3094 def older_than_version(self, version):
3095 """
3095 """
3096 Checks if comment is made from previous version than given
3096 Checks if comment is made from previous version than given
3097 """
3097 """
3098 if version is None:
3098 if version is None:
3099 return self.pull_request_version_id is not None
3099 return self.pull_request_version_id is not None
3100
3100
3101 return self.pull_request_version_id < version
3101 return self.pull_request_version_id < version
3102
3102
3103 @property
3103 @property
3104 def resolved(self):
3104 def resolved(self):
3105 return self.resolved_by[0] if self.resolved_by else None
3105 return self.resolved_by[0] if self.resolved_by else None
3106
3106
3107 @property
3107 @property
3108 def is_todo(self):
3108 def is_todo(self):
3109 return self.comment_type == self.COMMENT_TYPE_TODO
3109 return self.comment_type == self.COMMENT_TYPE_TODO
3110
3110
3111 @property
3111 @property
3112 def is_inline(self):
3112 def is_inline(self):
3113 return self.line_no and self.f_path
3113 return self.line_no and self.f_path
3114
3114
3115 def get_index_version(self, versions):
3115 def get_index_version(self, versions):
3116 return self.get_index_from_version(
3116 return self.get_index_from_version(
3117 self.pull_request_version_id, versions)
3117 self.pull_request_version_id, versions)
3118
3118
3119 def __repr__(self):
3119 def __repr__(self):
3120 if self.comment_id:
3120 if self.comment_id:
3121 return '<DB:Comment #%s>' % self.comment_id
3121 return '<DB:Comment #%s>' % self.comment_id
3122 else:
3122 else:
3123 return '<DB:Comment at %#x>' % id(self)
3123 return '<DB:Comment at %#x>' % id(self)
3124
3124
3125 def get_api_data(self):
3126 comment = self
3127 data = {
3128 'comment_id': comment.comment_id,
3129 'comment_type': comment.comment_type,
3130 'comment_text': comment.text,
3131 'comment_status': comment.status_change,
3132 'comment_f_path': comment.f_path,
3133 'comment_lineno': comment.line_no,
3134 'comment_author': comment.author,
3135 'comment_created_on': comment.created_on
3136 }
3137 return data
3138
3139 def __json__(self):
3140 data = dict()
3141 data.update(self.get_api_data())
3142 return data
3143
3125
3144
3126 class ChangesetStatus(Base, BaseModel):
3145 class ChangesetStatus(Base, BaseModel):
3127 __tablename__ = 'changeset_statuses'
3146 __tablename__ = 'changeset_statuses'
3128 __table_args__ = (
3147 __table_args__ = (
3129 Index('cs_revision_idx', 'revision'),
3148 Index('cs_revision_idx', 'revision'),
3130 Index('cs_version_idx', 'version'),
3149 Index('cs_version_idx', 'version'),
3131 UniqueConstraint('repo_id', 'revision', 'version'),
3150 UniqueConstraint('repo_id', 'revision', 'version'),
3132 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3133 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3152 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3134 )
3153 )
3135 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3154 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3136 STATUS_APPROVED = 'approved'
3155 STATUS_APPROVED = 'approved'
3137 STATUS_REJECTED = 'rejected'
3156 STATUS_REJECTED = 'rejected'
3138 STATUS_UNDER_REVIEW = 'under_review'
3157 STATUS_UNDER_REVIEW = 'under_review'
3139
3158
3140 STATUSES = [
3159 STATUSES = [
3141 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3160 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3142 (STATUS_APPROVED, _("Approved")),
3161 (STATUS_APPROVED, _("Approved")),
3143 (STATUS_REJECTED, _("Rejected")),
3162 (STATUS_REJECTED, _("Rejected")),
3144 (STATUS_UNDER_REVIEW, _("Under Review")),
3163 (STATUS_UNDER_REVIEW, _("Under Review")),
3145 ]
3164 ]
3146
3165
3147 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3166 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3148 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3167 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3149 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3168 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3150 revision = Column('revision', String(40), nullable=False)
3169 revision = Column('revision', String(40), nullable=False)
3151 status = Column('status', String(128), nullable=False, default=DEFAULT)
3170 status = Column('status', String(128), nullable=False, default=DEFAULT)
3152 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3171 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3153 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3172 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3154 version = Column('version', Integer(), nullable=False, default=0)
3173 version = Column('version', Integer(), nullable=False, default=0)
3155 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3174 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3156
3175
3157 author = relationship('User', lazy='joined')
3176 author = relationship('User', lazy='joined')
3158 repo = relationship('Repository')
3177 repo = relationship('Repository')
3159 comment = relationship('ChangesetComment', lazy='joined')
3178 comment = relationship('ChangesetComment', lazy='joined')
3160 pull_request = relationship('PullRequest', lazy='joined')
3179 pull_request = relationship('PullRequest', lazy='joined')
3161
3180
3162 def __unicode__(self):
3181 def __unicode__(self):
3163 return u"<%s('%s[v%s]:%s')>" % (
3182 return u"<%s('%s[v%s]:%s')>" % (
3164 self.__class__.__name__,
3183 self.__class__.__name__,
3165 self.status, self.version, self.author
3184 self.status, self.version, self.author
3166 )
3185 )
3167
3186
3168 @classmethod
3187 @classmethod
3169 def get_status_lbl(cls, value):
3188 def get_status_lbl(cls, value):
3170 return dict(cls.STATUSES).get(value)
3189 return dict(cls.STATUSES).get(value)
3171
3190
3172 @property
3191 @property
3173 def status_lbl(self):
3192 def status_lbl(self):
3174 return ChangesetStatus.get_status_lbl(self.status)
3193 return ChangesetStatus.get_status_lbl(self.status)
3175
3194
3195 def get_api_data(self):
3196 status = self
3197 data = {
3198 'status_id': status.changeset_status_id,
3199 'status': status.status,
3200 }
3201 return data
3202
3203 def __json__(self):
3204 data = dict()
3205 data.update(self.get_api_data())
3206 return data
3207
3176
3208
3177 class _PullRequestBase(BaseModel):
3209 class _PullRequestBase(BaseModel):
3178 """
3210 """
3179 Common attributes of pull request and version entries.
3211 Common attributes of pull request and version entries.
3180 """
3212 """
3181
3213
3182 # .status values
3214 # .status values
3183 STATUS_NEW = u'new'
3215 STATUS_NEW = u'new'
3184 STATUS_OPEN = u'open'
3216 STATUS_OPEN = u'open'
3185 STATUS_CLOSED = u'closed'
3217 STATUS_CLOSED = u'closed'
3186
3218
3187 title = Column('title', Unicode(255), nullable=True)
3219 title = Column('title', Unicode(255), nullable=True)
3188 description = Column(
3220 description = Column(
3189 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3221 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3190 nullable=True)
3222 nullable=True)
3191 # new/open/closed status of pull request (not approve/reject/etc)
3223 # new/open/closed status of pull request (not approve/reject/etc)
3192 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3224 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3193 created_on = Column(
3225 created_on = Column(
3194 'created_on', DateTime(timezone=False), nullable=False,
3226 'created_on', DateTime(timezone=False), nullable=False,
3195 default=datetime.datetime.now)
3227 default=datetime.datetime.now)
3196 updated_on = Column(
3228 updated_on = Column(
3197 'updated_on', DateTime(timezone=False), nullable=False,
3229 'updated_on', DateTime(timezone=False), nullable=False,
3198 default=datetime.datetime.now)
3230 default=datetime.datetime.now)
3199
3231
3200 @declared_attr
3232 @declared_attr
3201 def user_id(cls):
3233 def user_id(cls):
3202 return Column(
3234 return Column(
3203 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3235 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3204 unique=None)
3236 unique=None)
3205
3237
3206 # 500 revisions max
3238 # 500 revisions max
3207 _revisions = Column(
3239 _revisions = Column(
3208 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3240 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3209
3241
3210 @declared_attr
3242 @declared_attr
3211 def source_repo_id(cls):
3243 def source_repo_id(cls):
3212 # TODO: dan: rename column to source_repo_id
3244 # TODO: dan: rename column to source_repo_id
3213 return Column(
3245 return Column(
3214 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3246 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3215 nullable=False)
3247 nullable=False)
3216
3248
3217 source_ref = Column('org_ref', Unicode(255), nullable=False)
3249 source_ref = Column('org_ref', Unicode(255), nullable=False)
3218
3250
3219 @declared_attr
3251 @declared_attr
3220 def target_repo_id(cls):
3252 def target_repo_id(cls):
3221 # TODO: dan: rename column to target_repo_id
3253 # TODO: dan: rename column to target_repo_id
3222 return Column(
3254 return Column(
3223 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3255 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3224 nullable=False)
3256 nullable=False)
3225
3257
3226 target_ref = Column('other_ref', Unicode(255), nullable=False)
3258 target_ref = Column('other_ref', Unicode(255), nullable=False)
3227 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3259 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3228
3260
3229 # TODO: dan: rename column to last_merge_source_rev
3261 # TODO: dan: rename column to last_merge_source_rev
3230 _last_merge_source_rev = Column(
3262 _last_merge_source_rev = Column(
3231 'last_merge_org_rev', String(40), nullable=True)
3263 'last_merge_org_rev', String(40), nullable=True)
3232 # TODO: dan: rename column to last_merge_target_rev
3264 # TODO: dan: rename column to last_merge_target_rev
3233 _last_merge_target_rev = Column(
3265 _last_merge_target_rev = Column(
3234 'last_merge_other_rev', String(40), nullable=True)
3266 'last_merge_other_rev', String(40), nullable=True)
3235 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3267 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3236 merge_rev = Column('merge_rev', String(40), nullable=True)
3268 merge_rev = Column('merge_rev', String(40), nullable=True)
3237
3269
3238 reviewer_data = Column(
3270 reviewer_data = Column(
3239 'reviewer_data_json', MutationObj.as_mutable(
3271 'reviewer_data_json', MutationObj.as_mutable(
3240 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3272 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3241
3273
3242 @property
3274 @property
3243 def reviewer_data_json(self):
3275 def reviewer_data_json(self):
3244 return json.dumps(self.reviewer_data)
3276 return json.dumps(self.reviewer_data)
3245
3277
3246 @hybrid_property
3278 @hybrid_property
3247 def revisions(self):
3279 def revisions(self):
3248 return self._revisions.split(':') if self._revisions else []
3280 return self._revisions.split(':') if self._revisions else []
3249
3281
3250 @revisions.setter
3282 @revisions.setter
3251 def revisions(self, val):
3283 def revisions(self, val):
3252 self._revisions = ':'.join(val)
3284 self._revisions = ':'.join(val)
3253
3285
3254 @declared_attr
3286 @declared_attr
3255 def author(cls):
3287 def author(cls):
3256 return relationship('User', lazy='joined')
3288 return relationship('User', lazy='joined')
3257
3289
3258 @declared_attr
3290 @declared_attr
3259 def source_repo(cls):
3291 def source_repo(cls):
3260 return relationship(
3292 return relationship(
3261 'Repository',
3293 'Repository',
3262 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3294 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3263
3295
3264 @property
3296 @property
3265 def source_ref_parts(self):
3297 def source_ref_parts(self):
3266 return self.unicode_to_reference(self.source_ref)
3298 return self.unicode_to_reference(self.source_ref)
3267
3299
3268 @declared_attr
3300 @declared_attr
3269 def target_repo(cls):
3301 def target_repo(cls):
3270 return relationship(
3302 return relationship(
3271 'Repository',
3303 'Repository',
3272 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3304 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3273
3305
3274 @property
3306 @property
3275 def target_ref_parts(self):
3307 def target_ref_parts(self):
3276 return self.unicode_to_reference(self.target_ref)
3308 return self.unicode_to_reference(self.target_ref)
3277
3309
3278 @property
3310 @property
3279 def shadow_merge_ref(self):
3311 def shadow_merge_ref(self):
3280 return self.unicode_to_reference(self._shadow_merge_ref)
3312 return self.unicode_to_reference(self._shadow_merge_ref)
3281
3313
3282 @shadow_merge_ref.setter
3314 @shadow_merge_ref.setter
3283 def shadow_merge_ref(self, ref):
3315 def shadow_merge_ref(self, ref):
3284 self._shadow_merge_ref = self.reference_to_unicode(ref)
3316 self._shadow_merge_ref = self.reference_to_unicode(ref)
3285
3317
3286 def unicode_to_reference(self, raw):
3318 def unicode_to_reference(self, raw):
3287 """
3319 """
3288 Convert a unicode (or string) to a reference object.
3320 Convert a unicode (or string) to a reference object.
3289 If unicode evaluates to False it returns None.
3321 If unicode evaluates to False it returns None.
3290 """
3322 """
3291 if raw:
3323 if raw:
3292 refs = raw.split(':')
3324 refs = raw.split(':')
3293 return Reference(*refs)
3325 return Reference(*refs)
3294 else:
3326 else:
3295 return None
3327 return None
3296
3328
3297 def reference_to_unicode(self, ref):
3329 def reference_to_unicode(self, ref):
3298 """
3330 """
3299 Convert a reference object to unicode.
3331 Convert a reference object to unicode.
3300 If reference is None it returns None.
3332 If reference is None it returns None.
3301 """
3333 """
3302 if ref:
3334 if ref:
3303 return u':'.join(ref)
3335 return u':'.join(ref)
3304 else:
3336 else:
3305 return None
3337 return None
3306
3338
3307 def get_api_data(self):
3339 def get_api_data(self, with_merge_state=True):
3308 from pylons import url
3309 from rhodecode.model.pull_request import PullRequestModel
3340 from rhodecode.model.pull_request import PullRequestModel
3341
3310 pull_request = self
3342 pull_request = self
3343 if with_merge_state:
3311 merge_status = PullRequestModel().merge_status(pull_request)
3344 merge_status = PullRequestModel().merge_status(pull_request)
3312
3345 merge_state = {
3313 pull_request_url = url(
3346 'status': merge_status[0],
3314 'pullrequest_show', repo_name=self.target_repo.repo_name,
3347 'message': safe_unicode(merge_status[1]),
3315 pull_request_id=self.pull_request_id, qualified=True)
3348 }
3349 else:
3350 merge_state = {'status': 'not_available',
3351 'message': 'not_available'}
3316
3352
3317 merge_data = {
3353 merge_data = {
3318 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3354 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3319 'reference': (
3355 'reference': (
3320 pull_request.shadow_merge_ref._asdict()
3356 pull_request.shadow_merge_ref._asdict()
3321 if pull_request.shadow_merge_ref else None),
3357 if pull_request.shadow_merge_ref else None),
3322 }
3358 }
3323
3359
3324 data = {
3360 data = {
3325 'pull_request_id': pull_request.pull_request_id,
3361 'pull_request_id': pull_request.pull_request_id,
3326 'url': pull_request_url,
3362 'url': PullRequestModel().get_url(pull_request),
3327 'title': pull_request.title,
3363 'title': pull_request.title,
3328 'description': pull_request.description,
3364 'description': pull_request.description,
3329 'status': pull_request.status,
3365 'status': pull_request.status,
3330 'created_on': pull_request.created_on,
3366 'created_on': pull_request.created_on,
3331 'updated_on': pull_request.updated_on,
3367 'updated_on': pull_request.updated_on,
3332 'commit_ids': pull_request.revisions,
3368 'commit_ids': pull_request.revisions,
3333 'review_status': pull_request.calculated_review_status(),
3369 'review_status': pull_request.calculated_review_status(),
3334 'mergeable': {
3370 'mergeable': merge_state,
3335 'status': merge_status[0],
3336 'message': unicode(merge_status[1]),
3337 },
3338 'source': {
3371 'source': {
3339 'clone_url': pull_request.source_repo.clone_url(),
3372 'clone_url': pull_request.source_repo.clone_url(),
3340 'repository': pull_request.source_repo.repo_name,
3373 'repository': pull_request.source_repo.repo_name,
3341 'reference': {
3374 'reference': {
3342 'name': pull_request.source_ref_parts.name,
3375 'name': pull_request.source_ref_parts.name,
3343 'type': pull_request.source_ref_parts.type,
3376 'type': pull_request.source_ref_parts.type,
3344 'commit_id': pull_request.source_ref_parts.commit_id,
3377 'commit_id': pull_request.source_ref_parts.commit_id,
3345 },
3378 },
3346 },
3379 },
3347 'target': {
3380 'target': {
3348 'clone_url': pull_request.target_repo.clone_url(),
3381 'clone_url': pull_request.target_repo.clone_url(),
3349 'repository': pull_request.target_repo.repo_name,
3382 'repository': pull_request.target_repo.repo_name,
3350 'reference': {
3383 'reference': {
3351 'name': pull_request.target_ref_parts.name,
3384 'name': pull_request.target_ref_parts.name,
3352 'type': pull_request.target_ref_parts.type,
3385 'type': pull_request.target_ref_parts.type,
3353 'commit_id': pull_request.target_ref_parts.commit_id,
3386 'commit_id': pull_request.target_ref_parts.commit_id,
3354 },
3387 },
3355 },
3388 },
3356 'merge': merge_data,
3389 'merge': merge_data,
3357 'author': pull_request.author.get_api_data(include_secrets=False,
3390 'author': pull_request.author.get_api_data(include_secrets=False,
3358 details='basic'),
3391 details='basic'),
3359 'reviewers': [
3392 'reviewers': [
3360 {
3393 {
3361 'user': reviewer.get_api_data(include_secrets=False,
3394 'user': reviewer.get_api_data(include_secrets=False,
3362 details='basic'),
3395 details='basic'),
3363 'reasons': reasons,
3396 'reasons': reasons,
3364 'review_status': st[0][1].status if st else 'not_reviewed',
3397 'review_status': st[0][1].status if st else 'not_reviewed',
3365 }
3398 }
3366 for reviewer, reasons, mandatory, st in
3399 for reviewer, reasons, mandatory, st in
3367 pull_request.reviewers_statuses()
3400 pull_request.reviewers_statuses()
3368 ]
3401 ]
3369 }
3402 }
3370
3403
3371 return data
3404 return data
3372
3405
3373
3406
3374 class PullRequest(Base, _PullRequestBase):
3407 class PullRequest(Base, _PullRequestBase):
3375 __tablename__ = 'pull_requests'
3408 __tablename__ = 'pull_requests'
3376 __table_args__ = (
3409 __table_args__ = (
3377 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3410 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3378 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3411 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3379 )
3412 )
3380
3413
3381 pull_request_id = Column(
3414 pull_request_id = Column(
3382 'pull_request_id', Integer(), nullable=False, primary_key=True)
3415 'pull_request_id', Integer(), nullable=False, primary_key=True)
3383
3416
3384 def __repr__(self):
3417 def __repr__(self):
3385 if self.pull_request_id:
3418 if self.pull_request_id:
3386 return '<DB:PullRequest #%s>' % self.pull_request_id
3419 return '<DB:PullRequest #%s>' % self.pull_request_id
3387 else:
3420 else:
3388 return '<DB:PullRequest at %#x>' % id(self)
3421 return '<DB:PullRequest at %#x>' % id(self)
3389
3422
3390 reviewers = relationship('PullRequestReviewers',
3423 reviewers = relationship('PullRequestReviewers',
3391 cascade="all, delete, delete-orphan")
3424 cascade="all, delete, delete-orphan")
3392 statuses = relationship('ChangesetStatus')
3425 statuses = relationship('ChangesetStatus',
3426 cascade="all, delete, delete-orphan")
3393 comments = relationship('ChangesetComment',
3427 comments = relationship('ChangesetComment',
3394 cascade="all, delete, delete-orphan")
3428 cascade="all, delete, delete-orphan")
3395 versions = relationship('PullRequestVersion',
3429 versions = relationship('PullRequestVersion',
3396 cascade="all, delete, delete-orphan",
3430 cascade="all, delete, delete-orphan",
3397 lazy='dynamic')
3431 lazy='dynamic')
3398
3432
3399 @classmethod
3433 @classmethod
3400 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3434 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3401 internal_methods=None):
3435 internal_methods=None):
3402
3436
3403 class PullRequestDisplay(object):
3437 class PullRequestDisplay(object):
3404 """
3438 """
3405 Special object wrapper for showing PullRequest data via Versions
3439 Special object wrapper for showing PullRequest data via Versions
3406 It mimics PR object as close as possible. This is read only object
3440 It mimics PR object as close as possible. This is read only object
3407 just for display
3441 just for display
3408 """
3442 """
3409
3443
3410 def __init__(self, attrs, internal=None):
3444 def __init__(self, attrs, internal=None):
3411 self.attrs = attrs
3445 self.attrs = attrs
3412 # internal have priority over the given ones via attrs
3446 # internal have priority over the given ones via attrs
3413 self.internal = internal or ['versions']
3447 self.internal = internal or ['versions']
3414
3448
3415 def __getattr__(self, item):
3449 def __getattr__(self, item):
3416 if item in self.internal:
3450 if item in self.internal:
3417 return getattr(self, item)
3451 return getattr(self, item)
3418 try:
3452 try:
3419 return self.attrs[item]
3453 return self.attrs[item]
3420 except KeyError:
3454 except KeyError:
3421 raise AttributeError(
3455 raise AttributeError(
3422 '%s object has no attribute %s' % (self, item))
3456 '%s object has no attribute %s' % (self, item))
3423
3457
3424 def __repr__(self):
3458 def __repr__(self):
3425 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3459 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3426
3460
3427 def versions(self):
3461 def versions(self):
3428 return pull_request_obj.versions.order_by(
3462 return pull_request_obj.versions.order_by(
3429 PullRequestVersion.pull_request_version_id).all()
3463 PullRequestVersion.pull_request_version_id).all()
3430
3464
3431 def is_closed(self):
3465 def is_closed(self):
3432 return pull_request_obj.is_closed()
3466 return pull_request_obj.is_closed()
3433
3467
3434 @property
3468 @property
3435 def pull_request_version_id(self):
3469 def pull_request_version_id(self):
3436 return getattr(pull_request_obj, 'pull_request_version_id', None)
3470 return getattr(pull_request_obj, 'pull_request_version_id', None)
3437
3471
3438 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3472 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3439
3473
3440 attrs.author = StrictAttributeDict(
3474 attrs.author = StrictAttributeDict(
3441 pull_request_obj.author.get_api_data())
3475 pull_request_obj.author.get_api_data())
3442 if pull_request_obj.target_repo:
3476 if pull_request_obj.target_repo:
3443 attrs.target_repo = StrictAttributeDict(
3477 attrs.target_repo = StrictAttributeDict(
3444 pull_request_obj.target_repo.get_api_data())
3478 pull_request_obj.target_repo.get_api_data())
3445 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3479 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3446
3480
3447 if pull_request_obj.source_repo:
3481 if pull_request_obj.source_repo:
3448 attrs.source_repo = StrictAttributeDict(
3482 attrs.source_repo = StrictAttributeDict(
3449 pull_request_obj.source_repo.get_api_data())
3483 pull_request_obj.source_repo.get_api_data())
3450 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3484 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3451
3485
3452 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3486 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3453 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3487 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3454 attrs.revisions = pull_request_obj.revisions
3488 attrs.revisions = pull_request_obj.revisions
3455
3489
3456 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3490 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3457 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3491 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3458 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3492 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3459
3493
3460 return PullRequestDisplay(attrs, internal=internal_methods)
3494 return PullRequestDisplay(attrs, internal=internal_methods)
3461
3495
3462 def is_closed(self):
3496 def is_closed(self):
3463 return self.status == self.STATUS_CLOSED
3497 return self.status == self.STATUS_CLOSED
3464
3498
3465 def __json__(self):
3499 def __json__(self):
3466 return {
3500 return {
3467 'revisions': self.revisions,
3501 'revisions': self.revisions,
3468 }
3502 }
3469
3503
3470 def calculated_review_status(self):
3504 def calculated_review_status(self):
3471 from rhodecode.model.changeset_status import ChangesetStatusModel
3505 from rhodecode.model.changeset_status import ChangesetStatusModel
3472 return ChangesetStatusModel().calculated_review_status(self)
3506 return ChangesetStatusModel().calculated_review_status(self)
3473
3507
3474 def reviewers_statuses(self):
3508 def reviewers_statuses(self):
3475 from rhodecode.model.changeset_status import ChangesetStatusModel
3509 from rhodecode.model.changeset_status import ChangesetStatusModel
3476 return ChangesetStatusModel().reviewers_statuses(self)
3510 return ChangesetStatusModel().reviewers_statuses(self)
3477
3511
3478 @property
3512 @property
3479 def workspace_id(self):
3513 def workspace_id(self):
3480 from rhodecode.model.pull_request import PullRequestModel
3514 from rhodecode.model.pull_request import PullRequestModel
3481 return PullRequestModel()._workspace_id(self)
3515 return PullRequestModel()._workspace_id(self)
3482
3516
3483 def get_shadow_repo(self):
3517 def get_shadow_repo(self):
3484 workspace_id = self.workspace_id
3518 workspace_id = self.workspace_id
3485 vcs_obj = self.target_repo.scm_instance()
3519 vcs_obj = self.target_repo.scm_instance()
3486 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3520 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3487 workspace_id)
3521 workspace_id)
3488 return vcs_obj._get_shadow_instance(shadow_repository_path)
3522 return vcs_obj._get_shadow_instance(shadow_repository_path)
3489
3523
3490
3524
3491 class PullRequestVersion(Base, _PullRequestBase):
3525 class PullRequestVersion(Base, _PullRequestBase):
3492 __tablename__ = 'pull_request_versions'
3526 __tablename__ = 'pull_request_versions'
3493 __table_args__ = (
3527 __table_args__ = (
3494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3529 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3496 )
3530 )
3497
3531
3498 pull_request_version_id = Column(
3532 pull_request_version_id = Column(
3499 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3533 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3500 pull_request_id = Column(
3534 pull_request_id = Column(
3501 'pull_request_id', Integer(),
3535 'pull_request_id', Integer(),
3502 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3536 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3503 pull_request = relationship('PullRequest')
3537 pull_request = relationship('PullRequest')
3504
3538
3505 def __repr__(self):
3539 def __repr__(self):
3506 if self.pull_request_version_id:
3540 if self.pull_request_version_id:
3507 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3541 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3508 else:
3542 else:
3509 return '<DB:PullRequestVersion at %#x>' % id(self)
3543 return '<DB:PullRequestVersion at %#x>' % id(self)
3510
3544
3511 @property
3545 @property
3512 def reviewers(self):
3546 def reviewers(self):
3513 return self.pull_request.reviewers
3547 return self.pull_request.reviewers
3514
3548
3515 @property
3549 @property
3516 def versions(self):
3550 def versions(self):
3517 return self.pull_request.versions
3551 return self.pull_request.versions
3518
3552
3519 def is_closed(self):
3553 def is_closed(self):
3520 # calculate from original
3554 # calculate from original
3521 return self.pull_request.status == self.STATUS_CLOSED
3555 return self.pull_request.status == self.STATUS_CLOSED
3522
3556
3523 def calculated_review_status(self):
3557 def calculated_review_status(self):
3524 return self.pull_request.calculated_review_status()
3558 return self.pull_request.calculated_review_status()
3525
3559
3526 def reviewers_statuses(self):
3560 def reviewers_statuses(self):
3527 return self.pull_request.reviewers_statuses()
3561 return self.pull_request.reviewers_statuses()
3528
3562
3529
3563
3530 class PullRequestReviewers(Base, BaseModel):
3564 class PullRequestReviewers(Base, BaseModel):
3531 __tablename__ = 'pull_request_reviewers'
3565 __tablename__ = 'pull_request_reviewers'
3532 __table_args__ = (
3566 __table_args__ = (
3533 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3567 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3534 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3568 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3535 )
3569 )
3536
3570
3537 @hybrid_property
3571 @hybrid_property
3538 def reasons(self):
3572 def reasons(self):
3539 if not self._reasons:
3573 if not self._reasons:
3540 return []
3574 return []
3541 return self._reasons
3575 return self._reasons
3542
3576
3543 @reasons.setter
3577 @reasons.setter
3544 def reasons(self, val):
3578 def reasons(self, val):
3545 val = val or []
3579 val = val or []
3546 if any(not isinstance(x, basestring) for x in val):
3580 if any(not isinstance(x, basestring) for x in val):
3547 raise Exception('invalid reasons type, must be list of strings')
3581 raise Exception('invalid reasons type, must be list of strings')
3548 self._reasons = val
3582 self._reasons = val
3549
3583
3550 pull_requests_reviewers_id = Column(
3584 pull_requests_reviewers_id = Column(
3551 'pull_requests_reviewers_id', Integer(), nullable=False,
3585 'pull_requests_reviewers_id', Integer(), nullable=False,
3552 primary_key=True)
3586 primary_key=True)
3553 pull_request_id = Column(
3587 pull_request_id = Column(
3554 "pull_request_id", Integer(),
3588 "pull_request_id", Integer(),
3555 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3589 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3556 user_id = Column(
3590 user_id = Column(
3557 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3591 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3558 _reasons = Column(
3592 _reasons = Column(
3559 'reason', MutationList.as_mutable(
3593 'reason', MutationList.as_mutable(
3560 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3594 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3561 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3595 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3562 user = relationship('User')
3596 user = relationship('User')
3563 pull_request = relationship('PullRequest')
3597 pull_request = relationship('PullRequest')
3564
3598
3565
3599
3566 class Notification(Base, BaseModel):
3600 class Notification(Base, BaseModel):
3567 __tablename__ = 'notifications'
3601 __tablename__ = 'notifications'
3568 __table_args__ = (
3602 __table_args__ = (
3569 Index('notification_type_idx', 'type'),
3603 Index('notification_type_idx', 'type'),
3570 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3604 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3571 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3605 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3572 )
3606 )
3573
3607
3574 TYPE_CHANGESET_COMMENT = u'cs_comment'
3608 TYPE_CHANGESET_COMMENT = u'cs_comment'
3575 TYPE_MESSAGE = u'message'
3609 TYPE_MESSAGE = u'message'
3576 TYPE_MENTION = u'mention'
3610 TYPE_MENTION = u'mention'
3577 TYPE_REGISTRATION = u'registration'
3611 TYPE_REGISTRATION = u'registration'
3578 TYPE_PULL_REQUEST = u'pull_request'
3612 TYPE_PULL_REQUEST = u'pull_request'
3579 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3613 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3580
3614
3581 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3615 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3582 subject = Column('subject', Unicode(512), nullable=True)
3616 subject = Column('subject', Unicode(512), nullable=True)
3583 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3617 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3584 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3618 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3585 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3619 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3586 type_ = Column('type', Unicode(255))
3620 type_ = Column('type', Unicode(255))
3587
3621
3588 created_by_user = relationship('User')
3622 created_by_user = relationship('User')
3589 notifications_to_users = relationship('UserNotification', lazy='joined',
3623 notifications_to_users = relationship('UserNotification', lazy='joined',
3590 cascade="all, delete, delete-orphan")
3624 cascade="all, delete, delete-orphan")
3591
3625
3592 @property
3626 @property
3593 def recipients(self):
3627 def recipients(self):
3594 return [x.user for x in UserNotification.query()\
3628 return [x.user for x in UserNotification.query()\
3595 .filter(UserNotification.notification == self)\
3629 .filter(UserNotification.notification == self)\
3596 .order_by(UserNotification.user_id.asc()).all()]
3630 .order_by(UserNotification.user_id.asc()).all()]
3597
3631
3598 @classmethod
3632 @classmethod
3599 def create(cls, created_by, subject, body, recipients, type_=None):
3633 def create(cls, created_by, subject, body, recipients, type_=None):
3600 if type_ is None:
3634 if type_ is None:
3601 type_ = Notification.TYPE_MESSAGE
3635 type_ = Notification.TYPE_MESSAGE
3602
3636
3603 notification = cls()
3637 notification = cls()
3604 notification.created_by_user = created_by
3638 notification.created_by_user = created_by
3605 notification.subject = subject
3639 notification.subject = subject
3606 notification.body = body
3640 notification.body = body
3607 notification.type_ = type_
3641 notification.type_ = type_
3608 notification.created_on = datetime.datetime.now()
3642 notification.created_on = datetime.datetime.now()
3609
3643
3610 for u in recipients:
3644 for u in recipients:
3611 assoc = UserNotification()
3645 assoc = UserNotification()
3612 assoc.notification = notification
3646 assoc.notification = notification
3613
3647
3614 # if created_by is inside recipients mark his notification
3648 # if created_by is inside recipients mark his notification
3615 # as read
3649 # as read
3616 if u.user_id == created_by.user_id:
3650 if u.user_id == created_by.user_id:
3617 assoc.read = True
3651 assoc.read = True
3618
3652
3619 u.notifications.append(assoc)
3653 u.notifications.append(assoc)
3620 Session().add(notification)
3654 Session().add(notification)
3621
3655
3622 return notification
3656 return notification
3623
3657
3624 @property
3658 @property
3625 def description(self):
3659 def description(self):
3626 from rhodecode.model.notification import NotificationModel
3660 from rhodecode.model.notification import NotificationModel
3627 return NotificationModel().make_description(self)
3661 return NotificationModel().make_description(self)
3628
3662
3629
3663
3630 class UserNotification(Base, BaseModel):
3664 class UserNotification(Base, BaseModel):
3631 __tablename__ = 'user_to_notification'
3665 __tablename__ = 'user_to_notification'
3632 __table_args__ = (
3666 __table_args__ = (
3633 UniqueConstraint('user_id', 'notification_id'),
3667 UniqueConstraint('user_id', 'notification_id'),
3634 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3668 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3635 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3669 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3636 )
3670 )
3637 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3671 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3638 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3672 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3639 read = Column('read', Boolean, default=False)
3673 read = Column('read', Boolean, default=False)
3640 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3674 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3641
3675
3642 user = relationship('User', lazy="joined")
3676 user = relationship('User', lazy="joined")
3643 notification = relationship('Notification', lazy="joined",
3677 notification = relationship('Notification', lazy="joined",
3644 order_by=lambda: Notification.created_on.desc(),)
3678 order_by=lambda: Notification.created_on.desc(),)
3645
3679
3646 def mark_as_read(self):
3680 def mark_as_read(self):
3647 self.read = True
3681 self.read = True
3648 Session().add(self)
3682 Session().add(self)
3649
3683
3650
3684
3651 class Gist(Base, BaseModel):
3685 class Gist(Base, BaseModel):
3652 __tablename__ = 'gists'
3686 __tablename__ = 'gists'
3653 __table_args__ = (
3687 __table_args__ = (
3654 Index('g_gist_access_id_idx', 'gist_access_id'),
3688 Index('g_gist_access_id_idx', 'gist_access_id'),
3655 Index('g_created_on_idx', 'created_on'),
3689 Index('g_created_on_idx', 'created_on'),
3656 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3690 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3657 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3691 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3658 )
3692 )
3659 GIST_PUBLIC = u'public'
3693 GIST_PUBLIC = u'public'
3660 GIST_PRIVATE = u'private'
3694 GIST_PRIVATE = u'private'
3661 DEFAULT_FILENAME = u'gistfile1.txt'
3695 DEFAULT_FILENAME = u'gistfile1.txt'
3662
3696
3663 ACL_LEVEL_PUBLIC = u'acl_public'
3697 ACL_LEVEL_PUBLIC = u'acl_public'
3664 ACL_LEVEL_PRIVATE = u'acl_private'
3698 ACL_LEVEL_PRIVATE = u'acl_private'
3665
3699
3666 gist_id = Column('gist_id', Integer(), primary_key=True)
3700 gist_id = Column('gist_id', Integer(), primary_key=True)
3667 gist_access_id = Column('gist_access_id', Unicode(250))
3701 gist_access_id = Column('gist_access_id', Unicode(250))
3668 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3702 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3669 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3703 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3670 gist_expires = Column('gist_expires', Float(53), nullable=False)
3704 gist_expires = Column('gist_expires', Float(53), nullable=False)
3671 gist_type = Column('gist_type', Unicode(128), nullable=False)
3705 gist_type = Column('gist_type', Unicode(128), nullable=False)
3672 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3706 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3673 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3707 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3674 acl_level = Column('acl_level', Unicode(128), nullable=True)
3708 acl_level = Column('acl_level', Unicode(128), nullable=True)
3675
3709
3676 owner = relationship('User')
3710 owner = relationship('User')
3677
3711
3678 def __repr__(self):
3712 def __repr__(self):
3679 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3713 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3680
3714
3681 @classmethod
3715 @classmethod
3682 def get_or_404(cls, id_, pyramid_exc=False):
3716 def get_or_404(cls, id_, pyramid_exc=False):
3683
3717
3684 if pyramid_exc:
3718 if pyramid_exc:
3685 from pyramid.httpexceptions import HTTPNotFound
3719 from pyramid.httpexceptions import HTTPNotFound
3686 else:
3720 else:
3687 from webob.exc import HTTPNotFound
3721 from webob.exc import HTTPNotFound
3688
3722
3689 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3723 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3690 if not res:
3724 if not res:
3691 raise HTTPNotFound
3725 raise HTTPNotFound
3692 return res
3726 return res
3693
3727
3694 @classmethod
3728 @classmethod
3695 def get_by_access_id(cls, gist_access_id):
3729 def get_by_access_id(cls, gist_access_id):
3696 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3730 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3697
3731
3698 def gist_url(self):
3732 def gist_url(self):
3699 import rhodecode
3733 import rhodecode
3700 from pylons import url
3734 from pylons import url
3701
3735
3702 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3736 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3703 if alias_url:
3737 if alias_url:
3704 return alias_url.replace('{gistid}', self.gist_access_id)
3738 return alias_url.replace('{gistid}', self.gist_access_id)
3705
3739
3706 return url('gist', gist_id=self.gist_access_id, qualified=True)
3740 return url('gist', gist_id=self.gist_access_id, qualified=True)
3707
3741
3708 @classmethod
3742 @classmethod
3709 def base_path(cls):
3743 def base_path(cls):
3710 """
3744 """
3711 Returns base path when all gists are stored
3745 Returns base path when all gists are stored
3712
3746
3713 :param cls:
3747 :param cls:
3714 """
3748 """
3715 from rhodecode.model.gist import GIST_STORE_LOC
3749 from rhodecode.model.gist import GIST_STORE_LOC
3716 q = Session().query(RhodeCodeUi)\
3750 q = Session().query(RhodeCodeUi)\
3717 .filter(RhodeCodeUi.ui_key == URL_SEP)
3751 .filter(RhodeCodeUi.ui_key == URL_SEP)
3718 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3752 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3719 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3753 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3720
3754
3721 def get_api_data(self):
3755 def get_api_data(self):
3722 """
3756 """
3723 Common function for generating gist related data for API
3757 Common function for generating gist related data for API
3724 """
3758 """
3725 gist = self
3759 gist = self
3726 data = {
3760 data = {
3727 'gist_id': gist.gist_id,
3761 'gist_id': gist.gist_id,
3728 'type': gist.gist_type,
3762 'type': gist.gist_type,
3729 'access_id': gist.gist_access_id,
3763 'access_id': gist.gist_access_id,
3730 'description': gist.gist_description,
3764 'description': gist.gist_description,
3731 'url': gist.gist_url(),
3765 'url': gist.gist_url(),
3732 'expires': gist.gist_expires,
3766 'expires': gist.gist_expires,
3733 'created_on': gist.created_on,
3767 'created_on': gist.created_on,
3734 'modified_at': gist.modified_at,
3768 'modified_at': gist.modified_at,
3735 'content': None,
3769 'content': None,
3736 'acl_level': gist.acl_level,
3770 'acl_level': gist.acl_level,
3737 }
3771 }
3738 return data
3772 return data
3739
3773
3740 def __json__(self):
3774 def __json__(self):
3741 data = dict(
3775 data = dict(
3742 )
3776 )
3743 data.update(self.get_api_data())
3777 data.update(self.get_api_data())
3744 return data
3778 return data
3745 # SCM functions
3779 # SCM functions
3746
3780
3747 def scm_instance(self, **kwargs):
3781 def scm_instance(self, **kwargs):
3748 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3782 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3749 return get_vcs_instance(
3783 return get_vcs_instance(
3750 repo_path=safe_str(full_repo_path), create=False)
3784 repo_path=safe_str(full_repo_path), create=False)
3751
3785
3752
3786
3753 class ExternalIdentity(Base, BaseModel):
3787 class ExternalIdentity(Base, BaseModel):
3754 __tablename__ = 'external_identities'
3788 __tablename__ = 'external_identities'
3755 __table_args__ = (
3789 __table_args__ = (
3756 Index('local_user_id_idx', 'local_user_id'),
3790 Index('local_user_id_idx', 'local_user_id'),
3757 Index('external_id_idx', 'external_id'),
3791 Index('external_id_idx', 'external_id'),
3758 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3792 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3759 'mysql_charset': 'utf8'})
3793 'mysql_charset': 'utf8'})
3760
3794
3761 external_id = Column('external_id', Unicode(255), default=u'',
3795 external_id = Column('external_id', Unicode(255), default=u'',
3762 primary_key=True)
3796 primary_key=True)
3763 external_username = Column('external_username', Unicode(1024), default=u'')
3797 external_username = Column('external_username', Unicode(1024), default=u'')
3764 local_user_id = Column('local_user_id', Integer(),
3798 local_user_id = Column('local_user_id', Integer(),
3765 ForeignKey('users.user_id'), primary_key=True)
3799 ForeignKey('users.user_id'), primary_key=True)
3766 provider_name = Column('provider_name', Unicode(255), default=u'',
3800 provider_name = Column('provider_name', Unicode(255), default=u'',
3767 primary_key=True)
3801 primary_key=True)
3768 access_token = Column('access_token', String(1024), default=u'')
3802 access_token = Column('access_token', String(1024), default=u'')
3769 alt_token = Column('alt_token', String(1024), default=u'')
3803 alt_token = Column('alt_token', String(1024), default=u'')
3770 token_secret = Column('token_secret', String(1024), default=u'')
3804 token_secret = Column('token_secret', String(1024), default=u'')
3771
3805
3772 @classmethod
3806 @classmethod
3773 def by_external_id_and_provider(cls, external_id, provider_name,
3807 def by_external_id_and_provider(cls, external_id, provider_name,
3774 local_user_id=None):
3808 local_user_id=None):
3775 """
3809 """
3776 Returns ExternalIdentity instance based on search params
3810 Returns ExternalIdentity instance based on search params
3777
3811
3778 :param external_id:
3812 :param external_id:
3779 :param provider_name:
3813 :param provider_name:
3780 :return: ExternalIdentity
3814 :return: ExternalIdentity
3781 """
3815 """
3782 query = cls.query()
3816 query = cls.query()
3783 query = query.filter(cls.external_id == external_id)
3817 query = query.filter(cls.external_id == external_id)
3784 query = query.filter(cls.provider_name == provider_name)
3818 query = query.filter(cls.provider_name == provider_name)
3785 if local_user_id:
3819 if local_user_id:
3786 query = query.filter(cls.local_user_id == local_user_id)
3820 query = query.filter(cls.local_user_id == local_user_id)
3787 return query.first()
3821 return query.first()
3788
3822
3789 @classmethod
3823 @classmethod
3790 def user_by_external_id_and_provider(cls, external_id, provider_name):
3824 def user_by_external_id_and_provider(cls, external_id, provider_name):
3791 """
3825 """
3792 Returns User instance based on search params
3826 Returns User instance based on search params
3793
3827
3794 :param external_id:
3828 :param external_id:
3795 :param provider_name:
3829 :param provider_name:
3796 :return: User
3830 :return: User
3797 """
3831 """
3798 query = User.query()
3832 query = User.query()
3799 query = query.filter(cls.external_id == external_id)
3833 query = query.filter(cls.external_id == external_id)
3800 query = query.filter(cls.provider_name == provider_name)
3834 query = query.filter(cls.provider_name == provider_name)
3801 query = query.filter(User.user_id == cls.local_user_id)
3835 query = query.filter(User.user_id == cls.local_user_id)
3802 return query.first()
3836 return query.first()
3803
3837
3804 @classmethod
3838 @classmethod
3805 def by_local_user_id(cls, local_user_id):
3839 def by_local_user_id(cls, local_user_id):
3806 """
3840 """
3807 Returns all tokens for user
3841 Returns all tokens for user
3808
3842
3809 :param local_user_id:
3843 :param local_user_id:
3810 :return: ExternalIdentity
3844 :return: ExternalIdentity
3811 """
3845 """
3812 query = cls.query()
3846 query = cls.query()
3813 query = query.filter(cls.local_user_id == local_user_id)
3847 query = query.filter(cls.local_user_id == local_user_id)
3814 return query
3848 return query
3815
3849
3816
3850
3817 class Integration(Base, BaseModel):
3851 class Integration(Base, BaseModel):
3818 __tablename__ = 'integrations'
3852 __tablename__ = 'integrations'
3819 __table_args__ = (
3853 __table_args__ = (
3820 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3821 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3822 )
3856 )
3823
3857
3824 integration_id = Column('integration_id', Integer(), primary_key=True)
3858 integration_id = Column('integration_id', Integer(), primary_key=True)
3825 integration_type = Column('integration_type', String(255))
3859 integration_type = Column('integration_type', String(255))
3826 enabled = Column('enabled', Boolean(), nullable=False)
3860 enabled = Column('enabled', Boolean(), nullable=False)
3827 name = Column('name', String(255), nullable=False)
3861 name = Column('name', String(255), nullable=False)
3828 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3862 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3829 default=False)
3863 default=False)
3830
3864
3831 settings = Column(
3865 settings = Column(
3832 'settings_json', MutationObj.as_mutable(
3866 'settings_json', MutationObj.as_mutable(
3833 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3867 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3834 repo_id = Column(
3868 repo_id = Column(
3835 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3869 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3836 nullable=True, unique=None, default=None)
3870 nullable=True, unique=None, default=None)
3837 repo = relationship('Repository', lazy='joined')
3871 repo = relationship('Repository', lazy='joined')
3838
3872
3839 repo_group_id = Column(
3873 repo_group_id = Column(
3840 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3874 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3841 nullable=True, unique=None, default=None)
3875 nullable=True, unique=None, default=None)
3842 repo_group = relationship('RepoGroup', lazy='joined')
3876 repo_group = relationship('RepoGroup', lazy='joined')
3843
3877
3844 @property
3878 @property
3845 def scope(self):
3879 def scope(self):
3846 if self.repo:
3880 if self.repo:
3847 return repr(self.repo)
3881 return repr(self.repo)
3848 if self.repo_group:
3882 if self.repo_group:
3849 if self.child_repos_only:
3883 if self.child_repos_only:
3850 return repr(self.repo_group) + ' (child repos only)'
3884 return repr(self.repo_group) + ' (child repos only)'
3851 else:
3885 else:
3852 return repr(self.repo_group) + ' (recursive)'
3886 return repr(self.repo_group) + ' (recursive)'
3853 if self.child_repos_only:
3887 if self.child_repos_only:
3854 return 'root_repos'
3888 return 'root_repos'
3855 return 'global'
3889 return 'global'
3856
3890
3857 def __repr__(self):
3891 def __repr__(self):
3858 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3892 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3859
3893
3860
3894
3861 class RepoReviewRuleUser(Base, BaseModel):
3895 class RepoReviewRuleUser(Base, BaseModel):
3862 __tablename__ = 'repo_review_rules_users'
3896 __tablename__ = 'repo_review_rules_users'
3863 __table_args__ = (
3897 __table_args__ = (
3864 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3898 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3865 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3899 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3866 )
3900 )
3867 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3901 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3868 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3902 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3869 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3903 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3870 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3904 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3871 user = relationship('User')
3905 user = relationship('User')
3872
3906
3873 def rule_data(self):
3907 def rule_data(self):
3874 return {
3908 return {
3875 'mandatory': self.mandatory
3909 'mandatory': self.mandatory
3876 }
3910 }
3877
3911
3878
3912
3879 class RepoReviewRuleUserGroup(Base, BaseModel):
3913 class RepoReviewRuleUserGroup(Base, BaseModel):
3880 __tablename__ = 'repo_review_rules_users_groups'
3914 __tablename__ = 'repo_review_rules_users_groups'
3881 __table_args__ = (
3915 __table_args__ = (
3882 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3916 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3883 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3917 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3884 )
3918 )
3885 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3919 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3886 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3920 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3887 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3921 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3888 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3922 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3889 users_group = relationship('UserGroup')
3923 users_group = relationship('UserGroup')
3890
3924
3891 def rule_data(self):
3925 def rule_data(self):
3892 return {
3926 return {
3893 'mandatory': self.mandatory
3927 'mandatory': self.mandatory
3894 }
3928 }
3895
3929
3896
3930
3897 class RepoReviewRule(Base, BaseModel):
3931 class RepoReviewRule(Base, BaseModel):
3898 __tablename__ = 'repo_review_rules'
3932 __tablename__ = 'repo_review_rules'
3899 __table_args__ = (
3933 __table_args__ = (
3900 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3934 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3901 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3935 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3902 )
3936 )
3903
3937
3904 repo_review_rule_id = Column(
3938 repo_review_rule_id = Column(
3905 'repo_review_rule_id', Integer(), primary_key=True)
3939 'repo_review_rule_id', Integer(), primary_key=True)
3906 repo_id = Column(
3940 repo_id = Column(
3907 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3941 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3908 repo = relationship('Repository', backref='review_rules')
3942 repo = relationship('Repository', backref='review_rules')
3909
3943
3910 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3944 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3911 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3945 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3912
3946
3913 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3947 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3914 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3948 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3915 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3949 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3916 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3950 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3917
3951
3918 rule_users = relationship('RepoReviewRuleUser')
3952 rule_users = relationship('RepoReviewRuleUser')
3919 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3953 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3920
3954
3921 @hybrid_property
3955 @hybrid_property
3922 def branch_pattern(self):
3956 def branch_pattern(self):
3923 return self._branch_pattern or '*'
3957 return self._branch_pattern or '*'
3924
3958
3925 def _validate_glob(self, value):
3959 def _validate_glob(self, value):
3926 re.compile('^' + glob2re(value) + '$')
3960 re.compile('^' + glob2re(value) + '$')
3927
3961
3928 @branch_pattern.setter
3962 @branch_pattern.setter
3929 def branch_pattern(self, value):
3963 def branch_pattern(self, value):
3930 self._validate_glob(value)
3964 self._validate_glob(value)
3931 self._branch_pattern = value or '*'
3965 self._branch_pattern = value or '*'
3932
3966
3933 @hybrid_property
3967 @hybrid_property
3934 def file_pattern(self):
3968 def file_pattern(self):
3935 return self._file_pattern or '*'
3969 return self._file_pattern or '*'
3936
3970
3937 @file_pattern.setter
3971 @file_pattern.setter
3938 def file_pattern(self, value):
3972 def file_pattern(self, value):
3939 self._validate_glob(value)
3973 self._validate_glob(value)
3940 self._file_pattern = value or '*'
3974 self._file_pattern = value or '*'
3941
3975
3942 def matches(self, branch, files_changed):
3976 def matches(self, branch, files_changed):
3943 """
3977 """
3944 Check if this review rule matches a branch/files in a pull request
3978 Check if this review rule matches a branch/files in a pull request
3945
3979
3946 :param branch: branch name for the commit
3980 :param branch: branch name for the commit
3947 :param files_changed: list of file paths changed in the pull request
3981 :param files_changed: list of file paths changed in the pull request
3948 """
3982 """
3949
3983
3950 branch = branch or ''
3984 branch = branch or ''
3951 files_changed = files_changed or []
3985 files_changed = files_changed or []
3952
3986
3953 branch_matches = True
3987 branch_matches = True
3954 if branch:
3988 if branch:
3955 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3989 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3956 branch_matches = bool(branch_regex.search(branch))
3990 branch_matches = bool(branch_regex.search(branch))
3957
3991
3958 files_matches = True
3992 files_matches = True
3959 if self.file_pattern != '*':
3993 if self.file_pattern != '*':
3960 files_matches = False
3994 files_matches = False
3961 file_regex = re.compile(glob2re(self.file_pattern))
3995 file_regex = re.compile(glob2re(self.file_pattern))
3962 for filename in files_changed:
3996 for filename in files_changed:
3963 if file_regex.search(filename):
3997 if file_regex.search(filename):
3964 files_matches = True
3998 files_matches = True
3965 break
3999 break
3966
4000
3967 return branch_matches and files_matches
4001 return branch_matches and files_matches
3968
4002
3969 @property
4003 @property
3970 def review_users(self):
4004 def review_users(self):
3971 """ Returns the users which this rule applies to """
4005 """ Returns the users which this rule applies to """
3972
4006
3973 users = collections.OrderedDict()
4007 users = collections.OrderedDict()
3974
4008
3975 for rule_user in self.rule_users:
4009 for rule_user in self.rule_users:
3976 if rule_user.user.active:
4010 if rule_user.user.active:
3977 if rule_user.user not in users:
4011 if rule_user.user not in users:
3978 users[rule_user.user.username] = {
4012 users[rule_user.user.username] = {
3979 'user': rule_user.user,
4013 'user': rule_user.user,
3980 'source': 'user',
4014 'source': 'user',
3981 'source_data': {},
4015 'source_data': {},
3982 'data': rule_user.rule_data()
4016 'data': rule_user.rule_data()
3983 }
4017 }
3984
4018
3985 for rule_user_group in self.rule_user_groups:
4019 for rule_user_group in self.rule_user_groups:
3986 source_data = {
4020 source_data = {
3987 'name': rule_user_group.users_group.users_group_name,
4021 'name': rule_user_group.users_group.users_group_name,
3988 'members': len(rule_user_group.users_group.members)
4022 'members': len(rule_user_group.users_group.members)
3989 }
4023 }
3990 for member in rule_user_group.users_group.members:
4024 for member in rule_user_group.users_group.members:
3991 if member.user.active:
4025 if member.user.active:
3992 users[member.user.username] = {
4026 users[member.user.username] = {
3993 'user': member.user,
4027 'user': member.user,
3994 'source': 'user_group',
4028 'source': 'user_group',
3995 'source_data': source_data,
4029 'source_data': source_data,
3996 'data': rule_user_group.rule_data()
4030 'data': rule_user_group.rule_data()
3997 }
4031 }
3998
4032
3999 return users
4033 return users
4000
4034
4001 def __repr__(self):
4035 def __repr__(self):
4002 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4036 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4003 self.repo_review_rule_id, self.repo)
4037 self.repo_review_rule_id, self.repo)
4004
4038
4005
4039
4006 class DbMigrateVersion(Base, BaseModel):
4040 class DbMigrateVersion(Base, BaseModel):
4007 __tablename__ = 'db_migrate_version'
4041 __tablename__ = 'db_migrate_version'
4008 __table_args__ = (
4042 __table_args__ = (
4009 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4043 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4010 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4044 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4011 )
4045 )
4012 repository_id = Column('repository_id', String(250), primary_key=True)
4046 repository_id = Column('repository_id', String(250), primary_key=True)
4013 repository_path = Column('repository_path', Text)
4047 repository_path = Column('repository_path', Text)
4014 version = Column('version', Integer)
4048 version = Column('version', Integer)
4015
4049
4016
4050
4017 class DbSession(Base, BaseModel):
4051 class DbSession(Base, BaseModel):
4018 __tablename__ = 'db_session'
4052 __tablename__ = 'db_session'
4019 __table_args__ = (
4053 __table_args__ = (
4020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4054 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4055 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4022 )
4056 )
4023
4057
4024 def __repr__(self):
4058 def __repr__(self):
4025 return '<DB:DbSession({})>'.format(self.id)
4059 return '<DB:DbSession({})>'.format(self.id)
4026
4060
4027 id = Column('id', Integer())
4061 id = Column('id', Integer())
4028 namespace = Column('namespace', String(255), primary_key=True)
4062 namespace = Column('namespace', String(255), primary_key=True)
4029 accessed = Column('accessed', DateTime, nullable=False)
4063 accessed = Column('accessed', DateTime, nullable=False)
4030 created = Column('created', DateTime, nullable=False)
4064 created = Column('created', DateTime, nullable=False)
4031 data = Column('data', PickleType, nullable=False)
4065 data = Column('data', PickleType, nullable=False)
@@ -1,1528 +1,1554 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils import action_logger
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
477
473 return pull_request
478 return pull_request
474
479
475 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
476 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
477 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
478 if action == 'create':
483 if action == 'create':
479 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
480 elif action == 'merge':
485 elif action == 'merge':
481 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
482 elif action == 'close':
487 elif action == 'close':
483 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
484 elif action == 'review_status_change':
489 elif action == 'review_status_change':
485 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
486 elif action == 'update':
491 elif action == 'update':
487 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
488 else:
493 else:
489 return
494 return
490
495
491 trigger_hook(
496 trigger_hook(
492 username=user.username,
497 username=user.username,
493 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
494 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
495 pull_request=pull_request)
500 pull_request=pull_request)
496
501
497 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
498 """
503 """
499 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
500
505
501 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
502 nor with the implicit target updates.
507 nor with the implicit target updates.
503 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
504 will be reported anyways.
509 will be reported anyways.
505 """
510 """
506 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
507 if merge_rev is None:
512 if merge_rev is None:
508 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
509
514
510 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
511 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
512 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
513
518
514 return commit_ids
519 return commit_ids
515
520
516 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
517 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
518 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
519 if merge_state.executed:
524 if merge_state.executed:
520 log.debug(
525 log.debug(
521 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
522 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
523 self._log_action('user_merged_pull_request', user, pull_request)
528
529 self._log_audit_action(
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
533
524 else:
534 else:
525 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
526 return merge_state
536 return merge_state
527
537
528 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
529 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
530 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
531 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
532 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
533
543
534 message = _(
544 message = _(
535 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
536 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
537 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
538 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
539 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
540 'pr_title': pull_request.title
550 'pr_title': pull_request.title
541 }
551 }
542
552
543 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
544 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
545
555
546 callback_daemon, extras = prepare_callback_daemon(
556 callback_daemon, extras = prepare_callback_daemon(
547 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
548 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
549
559
550 with callback_daemon:
560 with callback_daemon:
551 # TODO: johbo: Implement a clean way to run a config_override
561 # TODO: johbo: Implement a clean way to run a config_override
552 # for a single call.
562 # for a single call.
553 target_vcs.config.set(
563 target_vcs.config.set(
554 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
555 merge_state = target_vcs.merge(
565 merge_state = target_vcs.merge(
556 target_ref, source_vcs, pull_request.source_ref_parts,
566 target_ref, source_vcs, pull_request.source_ref_parts,
557 workspace_id, user_name=user.username,
567 workspace_id, user_name=user.username,
558 user_email=user.email, message=message, use_rebase=use_rebase)
568 user_email=user.email, message=message, use_rebase=use_rebase)
559 return merge_state
569 return merge_state
560
570
561 def _comment_and_close_pr(self, pull_request, user, merge_state):
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
562 pull_request.merge_rev = merge_state.merge_ref.commit_id
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
563 pull_request.updated_on = datetime.datetime.now()
573 pull_request.updated_on = datetime.datetime.now()
564
574
565 CommentsModel().create(
575 CommentsModel().create(
566 text=unicode(_('Pull request merged and closed')),
576 text=unicode(_('Pull request merged and closed')),
567 repo=pull_request.target_repo.repo_id,
577 repo=pull_request.target_repo.repo_id,
568 user=user.user_id,
578 user=user.user_id,
569 pull_request=pull_request.pull_request_id,
579 pull_request=pull_request.pull_request_id,
570 f_path=None,
580 f_path=None,
571 line_no=None,
581 line_no=None,
572 closing_pr=True
582 closing_pr=True
573 )
583 )
574
584
575 Session().add(pull_request)
585 Session().add(pull_request)
576 Session().flush()
586 Session().flush()
577 # TODO: paris: replace invalidation with less radical solution
587 # TODO: paris: replace invalidation with less radical solution
578 ScmModel().mark_for_invalidation(
588 ScmModel().mark_for_invalidation(
579 pull_request.target_repo.repo_name)
589 pull_request.target_repo.repo_name)
580 self._trigger_pull_request_hook(pull_request, user, 'merge')
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
581
591
582 def has_valid_update_type(self, pull_request):
592 def has_valid_update_type(self, pull_request):
583 source_ref_type = pull_request.source_ref_parts.type
593 source_ref_type = pull_request.source_ref_parts.type
584 return source_ref_type in ['book', 'branch', 'tag']
594 return source_ref_type in ['book', 'branch', 'tag']
585
595
586 def update_commits(self, pull_request):
596 def update_commits(self, pull_request):
587 """
597 """
588 Get the updated list of commits for the pull request
598 Get the updated list of commits for the pull request
589 and return the new pull request version and the list
599 and return the new pull request version and the list
590 of commits processed by this update action
600 of commits processed by this update action
591 """
601 """
592 pull_request = self.__get_pull_request(pull_request)
602 pull_request = self.__get_pull_request(pull_request)
593 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
594 source_ref_name = pull_request.source_ref_parts.name
604 source_ref_name = pull_request.source_ref_parts.name
595 source_ref_id = pull_request.source_ref_parts.commit_id
605 source_ref_id = pull_request.source_ref_parts.commit_id
596
606
597 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
598 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
599 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
600
610
601 if not self.has_valid_update_type(pull_request):
611 if not self.has_valid_update_type(pull_request):
602 log.debug(
612 log.debug(
603 "Skipping update of pull request %s due to ref type: %s",
613 "Skipping update of pull request %s due to ref type: %s",
604 pull_request, source_ref_type)
614 pull_request, source_ref_type)
605 return UpdateResponse(
615 return UpdateResponse(
606 executed=False,
616 executed=False,
607 reason=UpdateFailureReason.WRONG_REF_TYPE,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
608 old=pull_request, new=None, changes=None,
618 old=pull_request, new=None, changes=None,
609 source_changed=False, target_changed=False)
619 source_changed=False, target_changed=False)
610
620
611 # source repo
621 # source repo
612 source_repo = pull_request.source_repo.scm_instance()
622 source_repo = pull_request.source_repo.scm_instance()
613 try:
623 try:
614 source_commit = source_repo.get_commit(commit_id=source_ref_name)
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
615 except CommitDoesNotExistError:
625 except CommitDoesNotExistError:
616 return UpdateResponse(
626 return UpdateResponse(
617 executed=False,
627 executed=False,
618 reason=UpdateFailureReason.MISSING_SOURCE_REF,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
619 old=pull_request, new=None, changes=None,
629 old=pull_request, new=None, changes=None,
620 source_changed=False, target_changed=False)
630 source_changed=False, target_changed=False)
621
631
622 source_changed = source_ref_id != source_commit.raw_id
632 source_changed = source_ref_id != source_commit.raw_id
623
633
624 # target repo
634 # target repo
625 target_repo = pull_request.target_repo.scm_instance()
635 target_repo = pull_request.target_repo.scm_instance()
626 try:
636 try:
627 target_commit = target_repo.get_commit(commit_id=target_ref_name)
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
628 except CommitDoesNotExistError:
638 except CommitDoesNotExistError:
629 return UpdateResponse(
639 return UpdateResponse(
630 executed=False,
640 executed=False,
631 reason=UpdateFailureReason.MISSING_TARGET_REF,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
632 old=pull_request, new=None, changes=None,
642 old=pull_request, new=None, changes=None,
633 source_changed=False, target_changed=False)
643 source_changed=False, target_changed=False)
634 target_changed = target_ref_id != target_commit.raw_id
644 target_changed = target_ref_id != target_commit.raw_id
635
645
636 if not (source_changed or target_changed):
646 if not (source_changed or target_changed):
637 log.debug("Nothing changed in pull request %s", pull_request)
647 log.debug("Nothing changed in pull request %s", pull_request)
638 return UpdateResponse(
648 return UpdateResponse(
639 executed=False,
649 executed=False,
640 reason=UpdateFailureReason.NO_CHANGE,
650 reason=UpdateFailureReason.NO_CHANGE,
641 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
642 source_changed=target_changed, target_changed=source_changed)
652 source_changed=target_changed, target_changed=source_changed)
643
653
644 change_in_found = 'target repo' if target_changed else 'source repo'
654 change_in_found = 'target repo' if target_changed else 'source repo'
645 log.debug('Updating pull request because of change in %s detected',
655 log.debug('Updating pull request because of change in %s detected',
646 change_in_found)
656 change_in_found)
647
657
648 # Finally there is a need for an update, in case of source change
658 # Finally there is a need for an update, in case of source change
649 # we create a new version, else just an update
659 # we create a new version, else just an update
650 if source_changed:
660 if source_changed:
651 pull_request_version = self._create_version_from_snapshot(pull_request)
661 pull_request_version = self._create_version_from_snapshot(pull_request)
652 self._link_comments_to_version(pull_request_version)
662 self._link_comments_to_version(pull_request_version)
653 else:
663 else:
654 try:
664 try:
655 ver = pull_request.versions[-1]
665 ver = pull_request.versions[-1]
656 except IndexError:
666 except IndexError:
657 ver = None
667 ver = None
658
668
659 pull_request.pull_request_version_id = \
669 pull_request.pull_request_version_id = \
660 ver.pull_request_version_id if ver else None
670 ver.pull_request_version_id if ver else None
661 pull_request_version = pull_request
671 pull_request_version = pull_request
662
672
663 try:
673 try:
664 if target_ref_type in ('tag', 'branch', 'book'):
674 if target_ref_type in ('tag', 'branch', 'book'):
665 target_commit = target_repo.get_commit(target_ref_name)
675 target_commit = target_repo.get_commit(target_ref_name)
666 else:
676 else:
667 target_commit = target_repo.get_commit(target_ref_id)
677 target_commit = target_repo.get_commit(target_ref_id)
668 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
669 return UpdateResponse(
679 return UpdateResponse(
670 executed=False,
680 executed=False,
671 reason=UpdateFailureReason.MISSING_TARGET_REF,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
672 old=pull_request, new=None, changes=None,
682 old=pull_request, new=None, changes=None,
673 source_changed=source_changed, target_changed=target_changed)
683 source_changed=source_changed, target_changed=target_changed)
674
684
675 # re-compute commit ids
685 # re-compute commit ids
676 old_commit_ids = pull_request.revisions
686 old_commit_ids = pull_request.revisions
677 pre_load = ["author", "branch", "date", "message"]
687 pre_load = ["author", "branch", "date", "message"]
678 commit_ranges = target_repo.compare(
688 commit_ranges = target_repo.compare(
679 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
680 pre_load=pre_load)
690 pre_load=pre_load)
681
691
682 ancestor = target_repo.get_common_ancestor(
692 ancestor = target_repo.get_common_ancestor(
683 target_commit.raw_id, source_commit.raw_id, source_repo)
693 target_commit.raw_id, source_commit.raw_id, source_repo)
684
694
685 pull_request.source_ref = '%s:%s:%s' % (
695 pull_request.source_ref = '%s:%s:%s' % (
686 source_ref_type, source_ref_name, source_commit.raw_id)
696 source_ref_type, source_ref_name, source_commit.raw_id)
687 pull_request.target_ref = '%s:%s:%s' % (
697 pull_request.target_ref = '%s:%s:%s' % (
688 target_ref_type, target_ref_name, ancestor)
698 target_ref_type, target_ref_name, ancestor)
689
699
690 pull_request.revisions = [
700 pull_request.revisions = [
691 commit.raw_id for commit in reversed(commit_ranges)]
701 commit.raw_id for commit in reversed(commit_ranges)]
692 pull_request.updated_on = datetime.datetime.now()
702 pull_request.updated_on = datetime.datetime.now()
693 Session().add(pull_request)
703 Session().add(pull_request)
694 new_commit_ids = pull_request.revisions
704 new_commit_ids = pull_request.revisions
695
705
696 old_diff_data, new_diff_data = self._generate_update_diffs(
706 old_diff_data, new_diff_data = self._generate_update_diffs(
697 pull_request, pull_request_version)
707 pull_request, pull_request_version)
698
708
699 # calculate commit and file changes
709 # calculate commit and file changes
700 changes = self._calculate_commit_id_changes(
710 changes = self._calculate_commit_id_changes(
701 old_commit_ids, new_commit_ids)
711 old_commit_ids, new_commit_ids)
702 file_changes = self._calculate_file_changes(
712 file_changes = self._calculate_file_changes(
703 old_diff_data, new_diff_data)
713 old_diff_data, new_diff_data)
704
714
705 # set comments as outdated if DIFFS changed
715 # set comments as outdated if DIFFS changed
706 CommentsModel().outdate_comments(
716 CommentsModel().outdate_comments(
707 pull_request, old_diff_data=old_diff_data,
717 pull_request, old_diff_data=old_diff_data,
708 new_diff_data=new_diff_data)
718 new_diff_data=new_diff_data)
709
719
710 commit_changes = (changes.added or changes.removed)
720 commit_changes = (changes.added or changes.removed)
711 file_node_changes = (
721 file_node_changes = (
712 file_changes.added or file_changes.modified or file_changes.removed)
722 file_changes.added or file_changes.modified or file_changes.removed)
713 pr_has_changes = commit_changes or file_node_changes
723 pr_has_changes = commit_changes or file_node_changes
714
724
715 # Add an automatic comment to the pull request, in case
725 # Add an automatic comment to the pull request, in case
716 # anything has changed
726 # anything has changed
717 if pr_has_changes:
727 if pr_has_changes:
718 update_comment = CommentsModel().create(
728 update_comment = CommentsModel().create(
719 text=self._render_update_message(changes, file_changes),
729 text=self._render_update_message(changes, file_changes),
720 repo=pull_request.target_repo,
730 repo=pull_request.target_repo,
721 user=pull_request.author,
731 user=pull_request.author,
722 pull_request=pull_request,
732 pull_request=pull_request,
723 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
724
734
725 # Update status to "Under Review" for added commits
735 # Update status to "Under Review" for added commits
726 for commit_id in changes.added:
736 for commit_id in changes.added:
727 ChangesetStatusModel().set_status(
737 ChangesetStatusModel().set_status(
728 repo=pull_request.source_repo,
738 repo=pull_request.source_repo,
729 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
730 comment=update_comment,
740 comment=update_comment,
731 user=pull_request.author,
741 user=pull_request.author,
732 pull_request=pull_request,
742 pull_request=pull_request,
733 revision=commit_id)
743 revision=commit_id)
734
744
735 log.debug(
745 log.debug(
736 'Updated pull request %s, added_ids: %s, common_ids: %s, '
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
737 'removed_ids: %s', pull_request.pull_request_id,
747 'removed_ids: %s', pull_request.pull_request_id,
738 changes.added, changes.common, changes.removed)
748 changes.added, changes.common, changes.removed)
739 log.debug(
749 log.debug(
740 'Updated pull request with the following file changes: %s',
750 'Updated pull request with the following file changes: %s',
741 file_changes)
751 file_changes)
742
752
743 log.info(
753 log.info(
744 "Updated pull request %s from commit %s to commit %s, "
754 "Updated pull request %s from commit %s to commit %s, "
745 "stored new version %s of this pull request.",
755 "stored new version %s of this pull request.",
746 pull_request.pull_request_id, source_ref_id,
756 pull_request.pull_request_id, source_ref_id,
747 pull_request.source_ref_parts.commit_id,
757 pull_request.source_ref_parts.commit_id,
748 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
749 Session().commit()
759 Session().commit()
750 self._trigger_pull_request_hook(
760 self._trigger_pull_request_hook(
751 pull_request, pull_request.author, 'update')
761 pull_request, pull_request.author, 'update')
752
762
753 return UpdateResponse(
763 return UpdateResponse(
754 executed=True, reason=UpdateFailureReason.NONE,
764 executed=True, reason=UpdateFailureReason.NONE,
755 old=pull_request, new=pull_request_version, changes=changes,
765 old=pull_request, new=pull_request_version, changes=changes,
756 source_changed=source_changed, target_changed=target_changed)
766 source_changed=source_changed, target_changed=target_changed)
757
767
758 def _create_version_from_snapshot(self, pull_request):
768 def _create_version_from_snapshot(self, pull_request):
759 version = PullRequestVersion()
769 version = PullRequestVersion()
760 version.title = pull_request.title
770 version.title = pull_request.title
761 version.description = pull_request.description
771 version.description = pull_request.description
762 version.status = pull_request.status
772 version.status = pull_request.status
763 version.created_on = datetime.datetime.now()
773 version.created_on = datetime.datetime.now()
764 version.updated_on = pull_request.updated_on
774 version.updated_on = pull_request.updated_on
765 version.user_id = pull_request.user_id
775 version.user_id = pull_request.user_id
766 version.source_repo = pull_request.source_repo
776 version.source_repo = pull_request.source_repo
767 version.source_ref = pull_request.source_ref
777 version.source_ref = pull_request.source_ref
768 version.target_repo = pull_request.target_repo
778 version.target_repo = pull_request.target_repo
769 version.target_ref = pull_request.target_ref
779 version.target_ref = pull_request.target_ref
770
780
771 version._last_merge_source_rev = pull_request._last_merge_source_rev
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
772 version._last_merge_target_rev = pull_request._last_merge_target_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
773 version._last_merge_status = pull_request._last_merge_status
783 version._last_merge_status = pull_request._last_merge_status
774 version.shadow_merge_ref = pull_request.shadow_merge_ref
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
775 version.merge_rev = pull_request.merge_rev
785 version.merge_rev = pull_request.merge_rev
776 version.reviewer_data = pull_request.reviewer_data
786 version.reviewer_data = pull_request.reviewer_data
777
787
778 version.revisions = pull_request.revisions
788 version.revisions = pull_request.revisions
779 version.pull_request = pull_request
789 version.pull_request = pull_request
780 Session().add(version)
790 Session().add(version)
781 Session().flush()
791 Session().flush()
782
792
783 return version
793 return version
784
794
785 def _generate_update_diffs(self, pull_request, pull_request_version):
795 def _generate_update_diffs(self, pull_request, pull_request_version):
786
796
787 diff_context = (
797 diff_context = (
788 self.DIFF_CONTEXT +
798 self.DIFF_CONTEXT +
789 CommentsModel.needed_extra_diff_context())
799 CommentsModel.needed_extra_diff_context())
790
800
791 source_repo = pull_request_version.source_repo
801 source_repo = pull_request_version.source_repo
792 source_ref_id = pull_request_version.source_ref_parts.commit_id
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
793 target_ref_id = pull_request_version.target_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
794 old_diff = self._get_diff_from_pr_or_version(
804 old_diff = self._get_diff_from_pr_or_version(
795 source_repo, source_ref_id, target_ref_id, context=diff_context)
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
796
806
797 source_repo = pull_request.source_repo
807 source_repo = pull_request.source_repo
798 source_ref_id = pull_request.source_ref_parts.commit_id
808 source_ref_id = pull_request.source_ref_parts.commit_id
799 target_ref_id = pull_request.target_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
800
810
801 new_diff = self._get_diff_from_pr_or_version(
811 new_diff = self._get_diff_from_pr_or_version(
802 source_repo, source_ref_id, target_ref_id, context=diff_context)
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
803
813
804 old_diff_data = diffs.DiffProcessor(old_diff)
814 old_diff_data = diffs.DiffProcessor(old_diff)
805 old_diff_data.prepare()
815 old_diff_data.prepare()
806 new_diff_data = diffs.DiffProcessor(new_diff)
816 new_diff_data = diffs.DiffProcessor(new_diff)
807 new_diff_data.prepare()
817 new_diff_data.prepare()
808
818
809 return old_diff_data, new_diff_data
819 return old_diff_data, new_diff_data
810
820
811 def _link_comments_to_version(self, pull_request_version):
821 def _link_comments_to_version(self, pull_request_version):
812 """
822 """
813 Link all unlinked comments of this pull request to the given version.
823 Link all unlinked comments of this pull request to the given version.
814
824
815 :param pull_request_version: The `PullRequestVersion` to which
825 :param pull_request_version: The `PullRequestVersion` to which
816 the comments shall be linked.
826 the comments shall be linked.
817
827
818 """
828 """
819 pull_request = pull_request_version.pull_request
829 pull_request = pull_request_version.pull_request
820 comments = ChangesetComment.query()\
830 comments = ChangesetComment.query()\
821 .filter(
831 .filter(
822 # TODO: johbo: Should we query for the repo at all here?
832 # TODO: johbo: Should we query for the repo at all here?
823 # Pending decision on how comments of PRs are to be related
833 # Pending decision on how comments of PRs are to be related
824 # to either the source repo, the target repo or no repo at all.
834 # to either the source repo, the target repo or no repo at all.
825 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
826 ChangesetComment.pull_request == pull_request,
836 ChangesetComment.pull_request == pull_request,
827 ChangesetComment.pull_request_version == None)\
837 ChangesetComment.pull_request_version == None)\
828 .order_by(ChangesetComment.comment_id.asc())
838 .order_by(ChangesetComment.comment_id.asc())
829
839
830 # TODO: johbo: Find out why this breaks if it is done in a bulk
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
831 # operation.
841 # operation.
832 for comment in comments:
842 for comment in comments:
833 comment.pull_request_version_id = (
843 comment.pull_request_version_id = (
834 pull_request_version.pull_request_version_id)
844 pull_request_version.pull_request_version_id)
835 Session().add(comment)
845 Session().add(comment)
836
846
837 def _calculate_commit_id_changes(self, old_ids, new_ids):
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
838 added = [x for x in new_ids if x not in old_ids]
848 added = [x for x in new_ids if x not in old_ids]
839 common = [x for x in new_ids if x in old_ids]
849 common = [x for x in new_ids if x in old_ids]
840 removed = [x for x in old_ids if x not in new_ids]
850 removed = [x for x in old_ids if x not in new_ids]
841 total = new_ids
851 total = new_ids
842 return ChangeTuple(added, common, removed, total)
852 return ChangeTuple(added, common, removed, total)
843
853
844 def _calculate_file_changes(self, old_diff_data, new_diff_data):
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
845
855
846 old_files = OrderedDict()
856 old_files = OrderedDict()
847 for diff_data in old_diff_data.parsed_diff:
857 for diff_data in old_diff_data.parsed_diff:
848 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
849
859
850 added_files = []
860 added_files = []
851 modified_files = []
861 modified_files = []
852 removed_files = []
862 removed_files = []
853 for diff_data in new_diff_data.parsed_diff:
863 for diff_data in new_diff_data.parsed_diff:
854 new_filename = diff_data['filename']
864 new_filename = diff_data['filename']
855 new_hash = md5_safe(diff_data['raw_diff'])
865 new_hash = md5_safe(diff_data['raw_diff'])
856
866
857 old_hash = old_files.get(new_filename)
867 old_hash = old_files.get(new_filename)
858 if not old_hash:
868 if not old_hash:
859 # file is not present in old diff, means it's added
869 # file is not present in old diff, means it's added
860 added_files.append(new_filename)
870 added_files.append(new_filename)
861 else:
871 else:
862 if new_hash != old_hash:
872 if new_hash != old_hash:
863 modified_files.append(new_filename)
873 modified_files.append(new_filename)
864 # now remove a file from old, since we have seen it already
874 # now remove a file from old, since we have seen it already
865 del old_files[new_filename]
875 del old_files[new_filename]
866
876
867 # removed files is when there are present in old, but not in NEW,
877 # removed files is when there are present in old, but not in NEW,
868 # since we remove old files that are present in new diff, left-overs
878 # since we remove old files that are present in new diff, left-overs
869 # if any should be the removed files
879 # if any should be the removed files
870 removed_files.extend(old_files.keys())
880 removed_files.extend(old_files.keys())
871
881
872 return FileChangeTuple(added_files, modified_files, removed_files)
882 return FileChangeTuple(added_files, modified_files, removed_files)
873
883
874 def _render_update_message(self, changes, file_changes):
884 def _render_update_message(self, changes, file_changes):
875 """
885 """
876 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
877 so it's always looking the same disregarding on which default
887 so it's always looking the same disregarding on which default
878 renderer system is using.
888 renderer system is using.
879
889
880 :param changes: changes named tuple
890 :param changes: changes named tuple
881 :param file_changes: file changes named tuple
891 :param file_changes: file changes named tuple
882
892
883 """
893 """
884 new_status = ChangesetStatus.get_status_lbl(
894 new_status = ChangesetStatus.get_status_lbl(
885 ChangesetStatus.STATUS_UNDER_REVIEW)
895 ChangesetStatus.STATUS_UNDER_REVIEW)
886
896
887 changed_files = (
897 changed_files = (
888 file_changes.added + file_changes.modified + file_changes.removed)
898 file_changes.added + file_changes.modified + file_changes.removed)
889
899
890 params = {
900 params = {
891 'under_review_label': new_status,
901 'under_review_label': new_status,
892 'added_commits': changes.added,
902 'added_commits': changes.added,
893 'removed_commits': changes.removed,
903 'removed_commits': changes.removed,
894 'changed_files': changed_files,
904 'changed_files': changed_files,
895 'added_files': file_changes.added,
905 'added_files': file_changes.added,
896 'modified_files': file_changes.modified,
906 'modified_files': file_changes.modified,
897 'removed_files': file_changes.removed,
907 'removed_files': file_changes.removed,
898 }
908 }
899 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
900 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
901
911
902 def edit(self, pull_request, title, description):
912 def edit(self, pull_request, title, description, user):
903 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
904 if pull_request.is_closed():
915 if pull_request.is_closed():
905 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
906 if title:
917 if title:
907 pull_request.title = title
918 pull_request.title = title
908 pull_request.description = description
919 pull_request.description = description
909 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
910 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
911
925
912 def update_reviewers(self, pull_request, reviewer_data):
926 def update_reviewers(self, pull_request, reviewer_data, user):
913 """
927 """
914 Update the reviewers in the pull request
928 Update the reviewers in the pull request
915
929
916 :param pull_request: the pr to update
930 :param pull_request: the pr to update
917 :param reviewer_data: list of tuples
931 :param reviewer_data: list of tuples
918 [(user, ['reason1', 'reason2'], mandatory_flag)]
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
919 """
933 """
920
934
921 reviewers = {}
935 reviewers = {}
922 for user_id, reasons, mandatory in reviewer_data:
936 for user_id, reasons, mandatory in reviewer_data:
923 if isinstance(user_id, (int, basestring)):
937 if isinstance(user_id, (int, basestring)):
924 user_id = self._get_user(user_id).user_id
938 user_id = self._get_user(user_id).user_id
925 reviewers[user_id] = {
939 reviewers[user_id] = {
926 'reasons': reasons, 'mandatory': mandatory}
940 'reasons': reasons, 'mandatory': mandatory}
927
941
928 reviewers_ids = set(reviewers.keys())
942 reviewers_ids = set(reviewers.keys())
929 pull_request = self.__get_pull_request(pull_request)
943 pull_request = self.__get_pull_request(pull_request)
930 current_reviewers = PullRequestReviewers.query()\
944 current_reviewers = PullRequestReviewers.query()\
931 .filter(PullRequestReviewers.pull_request ==
945 .filter(PullRequestReviewers.pull_request ==
932 pull_request).all()
946 pull_request).all()
933 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
934
948
935 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
936 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
937
951
938 log.debug("Adding %s reviewers", ids_to_add)
952 log.debug("Adding %s reviewers", ids_to_add)
939 log.debug("Removing %s reviewers", ids_to_remove)
953 log.debug("Removing %s reviewers", ids_to_remove)
940 changed = False
954 changed = False
941 for uid in ids_to_add:
955 for uid in ids_to_add:
942 changed = True
956 changed = True
943 _usr = self._get_user(uid)
957 _usr = self._get_user(uid)
944 reviewer = PullRequestReviewers()
958 reviewer = PullRequestReviewers()
945 reviewer.user = _usr
959 reviewer.user = _usr
946 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
947 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
948 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
949 #reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
950 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
951
968
952 for uid in ids_to_remove:
969 for uid in ids_to_remove:
953 changed = True
970 changed = True
954 reviewers = PullRequestReviewers.query()\
971 reviewers = PullRequestReviewers.query()\
955 .filter(PullRequestReviewers.user_id == uid,
972 .filter(PullRequestReviewers.user_id == uid,
956 PullRequestReviewers.pull_request == pull_request)\
973 PullRequestReviewers.pull_request == pull_request)\
957 .all()
974 .all()
958 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
959 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
960 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
961 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
962
983
963 if changed:
984 if changed:
964 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
965 Session().add(pull_request)
986 Session().add(pull_request)
966
987
967 self.notify_reviewers(pull_request, ids_to_add)
988 self.notify_reviewers(pull_request, ids_to_add)
968 return ids_to_add, ids_to_remove
989 return ids_to_add, ids_to_remove
969
990
970 def get_url(self, pull_request, request=None, permalink=False):
991 def get_url(self, pull_request, request=None, permalink=False):
971 if not request:
992 if not request:
972 request = get_current_request()
993 request = get_current_request()
973
994
974 if permalink:
995 if permalink:
975 return request.route_url(
996 return request.route_url(
976 'pull_requests_global',
997 'pull_requests_global',
977 pull_request_id=pull_request.pull_request_id,)
998 pull_request_id=pull_request.pull_request_id,)
978 else:
999 else:
979 return request.route_url(
1000 return request.route_url(
980 'pullrequest_show',
1001 'pullrequest_show',
981 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 repo_name=safe_str(pull_request.target_repo.repo_name),
982 pull_request_id=pull_request.pull_request_id,)
1003 pull_request_id=pull_request.pull_request_id,)
983
1004
984 def get_shadow_clone_url(self, pull_request):
1005 def get_shadow_clone_url(self, pull_request):
985 """
1006 """
986 Returns qualified url pointing to the shadow repository. If this pull
1007 Returns qualified url pointing to the shadow repository. If this pull
987 request is closed there is no shadow repository and ``None`` will be
1008 request is closed there is no shadow repository and ``None`` will be
988 returned.
1009 returned.
989 """
1010 """
990 if pull_request.is_closed():
1011 if pull_request.is_closed():
991 return None
1012 return None
992 else:
1013 else:
993 pr_url = urllib.unquote(self.get_url(pull_request))
1014 pr_url = urllib.unquote(self.get_url(pull_request))
994 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
995
1016
996 def notify_reviewers(self, pull_request, reviewers_ids):
1017 def notify_reviewers(self, pull_request, reviewers_ids):
997 # notification to reviewers
1018 # notification to reviewers
998 if not reviewers_ids:
1019 if not reviewers_ids:
999 return
1020 return
1000
1021
1001 pull_request_obj = pull_request
1022 pull_request_obj = pull_request
1002 # get the current participants of this pull request
1023 # get the current participants of this pull request
1003 recipients = reviewers_ids
1024 recipients = reviewers_ids
1004 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1005
1026
1006 pr_source_repo = pull_request_obj.source_repo
1027 pr_source_repo = pull_request_obj.source_repo
1007 pr_target_repo = pull_request_obj.target_repo
1028 pr_target_repo = pull_request_obj.target_repo
1008
1029
1009 pr_url = h.url(
1030 pr_url = h.url(
1010 'pullrequest_show',
1031 'pullrequest_show',
1011 repo_name=pr_target_repo.repo_name,
1032 repo_name=pr_target_repo.repo_name,
1012 pull_request_id=pull_request_obj.pull_request_id,
1033 pull_request_id=pull_request_obj.pull_request_id,
1013 qualified=True,)
1034 qualified=True,)
1014
1035
1015 # set some variables for email notification
1036 # set some variables for email notification
1016 pr_target_repo_url = h.route_url(
1037 pr_target_repo_url = h.route_url(
1017 'repo_summary', repo_name=pr_target_repo.repo_name)
1038 'repo_summary', repo_name=pr_target_repo.repo_name)
1018
1039
1019 pr_source_repo_url = h.route_url(
1040 pr_source_repo_url = h.route_url(
1020 'repo_summary', repo_name=pr_source_repo.repo_name)
1041 'repo_summary', repo_name=pr_source_repo.repo_name)
1021
1042
1022 # pull request specifics
1043 # pull request specifics
1023 pull_request_commits = [
1044 pull_request_commits = [
1024 (x.raw_id, x.message)
1045 (x.raw_id, x.message)
1025 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1046 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1026
1047
1027 kwargs = {
1048 kwargs = {
1028 'user': pull_request.author,
1049 'user': pull_request.author,
1029 'pull_request': pull_request_obj,
1050 'pull_request': pull_request_obj,
1030 'pull_request_commits': pull_request_commits,
1051 'pull_request_commits': pull_request_commits,
1031
1052
1032 'pull_request_target_repo': pr_target_repo,
1053 'pull_request_target_repo': pr_target_repo,
1033 'pull_request_target_repo_url': pr_target_repo_url,
1054 'pull_request_target_repo_url': pr_target_repo_url,
1034
1055
1035 'pull_request_source_repo': pr_source_repo,
1056 'pull_request_source_repo': pr_source_repo,
1036 'pull_request_source_repo_url': pr_source_repo_url,
1057 'pull_request_source_repo_url': pr_source_repo_url,
1037
1058
1038 'pull_request_url': pr_url,
1059 'pull_request_url': pr_url,
1039 }
1060 }
1040
1061
1041 # pre-generate the subject for notification itself
1062 # pre-generate the subject for notification itself
1042 (subject,
1063 (subject,
1043 _h, _e, # we don't care about those
1064 _h, _e, # we don't care about those
1044 body_plaintext) = EmailNotificationModel().render_email(
1065 body_plaintext) = EmailNotificationModel().render_email(
1045 notification_type, **kwargs)
1066 notification_type, **kwargs)
1046
1067
1047 # create notification objects, and emails
1068 # create notification objects, and emails
1048 NotificationModel().create(
1069 NotificationModel().create(
1049 created_by=pull_request.author,
1070 created_by=pull_request.author,
1050 notification_subject=subject,
1071 notification_subject=subject,
1051 notification_body=body_plaintext,
1072 notification_body=body_plaintext,
1052 notification_type=notification_type,
1073 notification_type=notification_type,
1053 recipients=recipients,
1074 recipients=recipients,
1054 email_kwargs=kwargs,
1075 email_kwargs=kwargs,
1055 )
1076 )
1056
1077
1057 def delete(self, pull_request):
1078 def delete(self, pull_request, user):
1058 pull_request = self.__get_pull_request(pull_request)
1079 pull_request = self.__get_pull_request(pull_request)
1080 old_data = pull_request.get_api_data(with_merge_state=False)
1059 self._cleanup_merge_workspace(pull_request)
1081 self._cleanup_merge_workspace(pull_request)
1082 self._log_audit_action(
1083 'repo.pull_request.delete', {'old_data': old_data},
1084 user, pull_request)
1060 Session().delete(pull_request)
1085 Session().delete(pull_request)
1061
1086
1062 def close_pull_request(self, pull_request, user):
1087 def close_pull_request(self, pull_request, user):
1063 pull_request = self.__get_pull_request(pull_request)
1088 pull_request = self.__get_pull_request(pull_request)
1064 self._cleanup_merge_workspace(pull_request)
1089 self._cleanup_merge_workspace(pull_request)
1065 pull_request.status = PullRequest.STATUS_CLOSED
1090 pull_request.status = PullRequest.STATUS_CLOSED
1066 pull_request.updated_on = datetime.datetime.now()
1091 pull_request.updated_on = datetime.datetime.now()
1067 Session().add(pull_request)
1092 Session().add(pull_request)
1068 self._trigger_pull_request_hook(
1093 self._trigger_pull_request_hook(
1069 pull_request, pull_request.author, 'close')
1094 pull_request, pull_request.author, 'close')
1070 self._log_action('user_closed_pull_request', user, pull_request)
1095 self._log_audit_action(
1096 'repo.pull_request.close', {}, user, pull_request)
1071
1097
1072 def close_pull_request_with_comment(
1098 def close_pull_request_with_comment(
1073 self, pull_request, user, repo, message=None):
1099 self, pull_request, user, repo, message=None):
1074
1100
1075 pull_request_review_status = pull_request.calculated_review_status()
1101 pull_request_review_status = pull_request.calculated_review_status()
1076
1102
1077 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1103 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1078 # approved only if we have voting consent
1104 # approved only if we have voting consent
1079 status = ChangesetStatus.STATUS_APPROVED
1105 status = ChangesetStatus.STATUS_APPROVED
1080 else:
1106 else:
1081 status = ChangesetStatus.STATUS_REJECTED
1107 status = ChangesetStatus.STATUS_REJECTED
1082 status_lbl = ChangesetStatus.get_status_lbl(status)
1108 status_lbl = ChangesetStatus.get_status_lbl(status)
1083
1109
1084 default_message = (
1110 default_message = (
1085 _('Closing with status change {transition_icon} {status}.')
1111 _('Closing with status change {transition_icon} {status}.')
1086 ).format(transition_icon='>', status=status_lbl)
1112 ).format(transition_icon='>', status=status_lbl)
1087 text = message or default_message
1113 text = message or default_message
1088
1114
1089 # create a comment, and link it to new status
1115 # create a comment, and link it to new status
1090 comment = CommentsModel().create(
1116 comment = CommentsModel().create(
1091 text=text,
1117 text=text,
1092 repo=repo.repo_id,
1118 repo=repo.repo_id,
1093 user=user.user_id,
1119 user=user.user_id,
1094 pull_request=pull_request.pull_request_id,
1120 pull_request=pull_request.pull_request_id,
1095 status_change=status_lbl,
1121 status_change=status_lbl,
1096 status_change_type=status,
1122 status_change_type=status,
1097 closing_pr=True
1123 closing_pr=True
1098 )
1124 )
1099
1125
1100 # calculate old status before we change it
1126 # calculate old status before we change it
1101 old_calculated_status = pull_request.calculated_review_status()
1127 old_calculated_status = pull_request.calculated_review_status()
1102 ChangesetStatusModel().set_status(
1128 ChangesetStatusModel().set_status(
1103 repo.repo_id,
1129 repo.repo_id,
1104 status,
1130 status,
1105 user.user_id,
1131 user.user_id,
1106 comment=comment,
1132 comment=comment,
1107 pull_request=pull_request.pull_request_id
1133 pull_request=pull_request.pull_request_id
1108 )
1134 )
1109
1135
1110 Session().flush()
1136 Session().flush()
1111 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1137 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1112 # we now calculate the status of pull request again, and based on that
1138 # we now calculate the status of pull request again, and based on that
1113 # calculation trigger status change. This might happen in cases
1139 # calculation trigger status change. This might happen in cases
1114 # that non-reviewer admin closes a pr, which means his vote doesn't
1140 # that non-reviewer admin closes a pr, which means his vote doesn't
1115 # change the status, while if he's a reviewer this might change it.
1141 # change the status, while if he's a reviewer this might change it.
1116 calculated_status = pull_request.calculated_review_status()
1142 calculated_status = pull_request.calculated_review_status()
1117 if old_calculated_status != calculated_status:
1143 if old_calculated_status != calculated_status:
1118 self._trigger_pull_request_hook(
1144 self._trigger_pull_request_hook(
1119 pull_request, user, 'review_status_change')
1145 pull_request, user, 'review_status_change')
1120
1146
1121 # finally close the PR
1147 # finally close the PR
1122 PullRequestModel().close_pull_request(
1148 PullRequestModel().close_pull_request(
1123 pull_request.pull_request_id, user)
1149 pull_request.pull_request_id, user)
1124
1150
1125 return comment, status
1151 return comment, status
1126
1152
1127 def merge_status(self, pull_request):
1153 def merge_status(self, pull_request):
1128 if not self._is_merge_enabled(pull_request):
1154 if not self._is_merge_enabled(pull_request):
1129 return False, _('Server-side pull request merging is disabled.')
1155 return False, _('Server-side pull request merging is disabled.')
1130 if pull_request.is_closed():
1156 if pull_request.is_closed():
1131 return False, _('This pull request is closed.')
1157 return False, _('This pull request is closed.')
1132 merge_possible, msg = self._check_repo_requirements(
1158 merge_possible, msg = self._check_repo_requirements(
1133 target=pull_request.target_repo, source=pull_request.source_repo)
1159 target=pull_request.target_repo, source=pull_request.source_repo)
1134 if not merge_possible:
1160 if not merge_possible:
1135 return merge_possible, msg
1161 return merge_possible, msg
1136
1162
1137 try:
1163 try:
1138 resp = self._try_merge(pull_request)
1164 resp = self._try_merge(pull_request)
1139 log.debug("Merge response: %s", resp)
1165 log.debug("Merge response: %s", resp)
1140 status = resp.possible, self.merge_status_message(
1166 status = resp.possible, self.merge_status_message(
1141 resp.failure_reason)
1167 resp.failure_reason)
1142 except NotImplementedError:
1168 except NotImplementedError:
1143 status = False, _('Pull request merging is not supported.')
1169 status = False, _('Pull request merging is not supported.')
1144
1170
1145 return status
1171 return status
1146
1172
1147 def _check_repo_requirements(self, target, source):
1173 def _check_repo_requirements(self, target, source):
1148 """
1174 """
1149 Check if `target` and `source` have compatible requirements.
1175 Check if `target` and `source` have compatible requirements.
1150
1176
1151 Currently this is just checking for largefiles.
1177 Currently this is just checking for largefiles.
1152 """
1178 """
1153 target_has_largefiles = self._has_largefiles(target)
1179 target_has_largefiles = self._has_largefiles(target)
1154 source_has_largefiles = self._has_largefiles(source)
1180 source_has_largefiles = self._has_largefiles(source)
1155 merge_possible = True
1181 merge_possible = True
1156 message = u''
1182 message = u''
1157
1183
1158 if target_has_largefiles != source_has_largefiles:
1184 if target_has_largefiles != source_has_largefiles:
1159 merge_possible = False
1185 merge_possible = False
1160 if source_has_largefiles:
1186 if source_has_largefiles:
1161 message = _(
1187 message = _(
1162 'Target repository large files support is disabled.')
1188 'Target repository large files support is disabled.')
1163 else:
1189 else:
1164 message = _(
1190 message = _(
1165 'Source repository large files support is disabled.')
1191 'Source repository large files support is disabled.')
1166
1192
1167 return merge_possible, message
1193 return merge_possible, message
1168
1194
1169 def _has_largefiles(self, repo):
1195 def _has_largefiles(self, repo):
1170 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1196 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1171 'extensions', 'largefiles')
1197 'extensions', 'largefiles')
1172 return largefiles_ui and largefiles_ui[0].active
1198 return largefiles_ui and largefiles_ui[0].active
1173
1199
1174 def _try_merge(self, pull_request):
1200 def _try_merge(self, pull_request):
1175 """
1201 """
1176 Try to merge the pull request and return the merge status.
1202 Try to merge the pull request and return the merge status.
1177 """
1203 """
1178 log.debug(
1204 log.debug(
1179 "Trying out if the pull request %s can be merged.",
1205 "Trying out if the pull request %s can be merged.",
1180 pull_request.pull_request_id)
1206 pull_request.pull_request_id)
1181 target_vcs = pull_request.target_repo.scm_instance()
1207 target_vcs = pull_request.target_repo.scm_instance()
1182
1208
1183 # Refresh the target reference.
1209 # Refresh the target reference.
1184 try:
1210 try:
1185 target_ref = self._refresh_reference(
1211 target_ref = self._refresh_reference(
1186 pull_request.target_ref_parts, target_vcs)
1212 pull_request.target_ref_parts, target_vcs)
1187 except CommitDoesNotExistError:
1213 except CommitDoesNotExistError:
1188 merge_state = MergeResponse(
1214 merge_state = MergeResponse(
1189 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1215 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1190 return merge_state
1216 return merge_state
1191
1217
1192 target_locked = pull_request.target_repo.locked
1218 target_locked = pull_request.target_repo.locked
1193 if target_locked and target_locked[0]:
1219 if target_locked and target_locked[0]:
1194 log.debug("The target repository is locked.")
1220 log.debug("The target repository is locked.")
1195 merge_state = MergeResponse(
1221 merge_state = MergeResponse(
1196 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1222 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1197 elif self._needs_merge_state_refresh(pull_request, target_ref):
1223 elif self._needs_merge_state_refresh(pull_request, target_ref):
1198 log.debug("Refreshing the merge status of the repository.")
1224 log.debug("Refreshing the merge status of the repository.")
1199 merge_state = self._refresh_merge_state(
1225 merge_state = self._refresh_merge_state(
1200 pull_request, target_vcs, target_ref)
1226 pull_request, target_vcs, target_ref)
1201 else:
1227 else:
1202 possible = pull_request.\
1228 possible = pull_request.\
1203 _last_merge_status == MergeFailureReason.NONE
1229 _last_merge_status == MergeFailureReason.NONE
1204 merge_state = MergeResponse(
1230 merge_state = MergeResponse(
1205 possible, False, None, pull_request._last_merge_status)
1231 possible, False, None, pull_request._last_merge_status)
1206
1232
1207 return merge_state
1233 return merge_state
1208
1234
1209 def _refresh_reference(self, reference, vcs_repository):
1235 def _refresh_reference(self, reference, vcs_repository):
1210 if reference.type in ('branch', 'book'):
1236 if reference.type in ('branch', 'book'):
1211 name_or_id = reference.name
1237 name_or_id = reference.name
1212 else:
1238 else:
1213 name_or_id = reference.commit_id
1239 name_or_id = reference.commit_id
1214 refreshed_commit = vcs_repository.get_commit(name_or_id)
1240 refreshed_commit = vcs_repository.get_commit(name_or_id)
1215 refreshed_reference = Reference(
1241 refreshed_reference = Reference(
1216 reference.type, reference.name, refreshed_commit.raw_id)
1242 reference.type, reference.name, refreshed_commit.raw_id)
1217 return refreshed_reference
1243 return refreshed_reference
1218
1244
1219 def _needs_merge_state_refresh(self, pull_request, target_reference):
1245 def _needs_merge_state_refresh(self, pull_request, target_reference):
1220 return not(
1246 return not(
1221 pull_request.revisions and
1247 pull_request.revisions and
1222 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1248 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1223 target_reference.commit_id == pull_request._last_merge_target_rev)
1249 target_reference.commit_id == pull_request._last_merge_target_rev)
1224
1250
1225 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1251 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1226 workspace_id = self._workspace_id(pull_request)
1252 workspace_id = self._workspace_id(pull_request)
1227 source_vcs = pull_request.source_repo.scm_instance()
1253 source_vcs = pull_request.source_repo.scm_instance()
1228 use_rebase = self._use_rebase_for_merging(pull_request)
1254 use_rebase = self._use_rebase_for_merging(pull_request)
1229 merge_state = target_vcs.merge(
1255 merge_state = target_vcs.merge(
1230 target_reference, source_vcs, pull_request.source_ref_parts,
1256 target_reference, source_vcs, pull_request.source_ref_parts,
1231 workspace_id, dry_run=True, use_rebase=use_rebase)
1257 workspace_id, dry_run=True, use_rebase=use_rebase)
1232
1258
1233 # Do not store the response if there was an unknown error.
1259 # Do not store the response if there was an unknown error.
1234 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1260 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1235 pull_request._last_merge_source_rev = \
1261 pull_request._last_merge_source_rev = \
1236 pull_request.source_ref_parts.commit_id
1262 pull_request.source_ref_parts.commit_id
1237 pull_request._last_merge_target_rev = target_reference.commit_id
1263 pull_request._last_merge_target_rev = target_reference.commit_id
1238 pull_request._last_merge_status = merge_state.failure_reason
1264 pull_request._last_merge_status = merge_state.failure_reason
1239 pull_request.shadow_merge_ref = merge_state.merge_ref
1265 pull_request.shadow_merge_ref = merge_state.merge_ref
1240 Session().add(pull_request)
1266 Session().add(pull_request)
1241 Session().commit()
1267 Session().commit()
1242
1268
1243 return merge_state
1269 return merge_state
1244
1270
1245 def _workspace_id(self, pull_request):
1271 def _workspace_id(self, pull_request):
1246 workspace_id = 'pr-%s' % pull_request.pull_request_id
1272 workspace_id = 'pr-%s' % pull_request.pull_request_id
1247 return workspace_id
1273 return workspace_id
1248
1274
1249 def merge_status_message(self, status_code):
1275 def merge_status_message(self, status_code):
1250 """
1276 """
1251 Return a human friendly error message for the given merge status code.
1277 Return a human friendly error message for the given merge status code.
1252 """
1278 """
1253 return self.MERGE_STATUS_MESSAGES[status_code]
1279 return self.MERGE_STATUS_MESSAGES[status_code]
1254
1280
1255 def generate_repo_data(self, repo, commit_id=None, branch=None,
1281 def generate_repo_data(self, repo, commit_id=None, branch=None,
1256 bookmark=None):
1282 bookmark=None):
1257 all_refs, selected_ref = \
1283 all_refs, selected_ref = \
1258 self._get_repo_pullrequest_sources(
1284 self._get_repo_pullrequest_sources(
1259 repo.scm_instance(), commit_id=commit_id,
1285 repo.scm_instance(), commit_id=commit_id,
1260 branch=branch, bookmark=bookmark)
1286 branch=branch, bookmark=bookmark)
1261
1287
1262 refs_select2 = []
1288 refs_select2 = []
1263 for element in all_refs:
1289 for element in all_refs:
1264 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1290 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1265 refs_select2.append({'text': element[1], 'children': children})
1291 refs_select2.append({'text': element[1], 'children': children})
1266
1292
1267 return {
1293 return {
1268 'user': {
1294 'user': {
1269 'user_id': repo.user.user_id,
1295 'user_id': repo.user.user_id,
1270 'username': repo.user.username,
1296 'username': repo.user.username,
1271 'firstname': repo.user.firstname,
1297 'firstname': repo.user.firstname,
1272 'lastname': repo.user.lastname,
1298 'lastname': repo.user.lastname,
1273 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1299 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1274 },
1300 },
1275 'description': h.chop_at_smart(repo.description, '\n'),
1301 'description': h.chop_at_smart(repo.description, '\n'),
1276 'refs': {
1302 'refs': {
1277 'all_refs': all_refs,
1303 'all_refs': all_refs,
1278 'selected_ref': selected_ref,
1304 'selected_ref': selected_ref,
1279 'select2_refs': refs_select2
1305 'select2_refs': refs_select2
1280 }
1306 }
1281 }
1307 }
1282
1308
1283 def generate_pullrequest_title(self, source, source_ref, target):
1309 def generate_pullrequest_title(self, source, source_ref, target):
1284 return u'{source}#{at_ref} to {target}'.format(
1310 return u'{source}#{at_ref} to {target}'.format(
1285 source=source,
1311 source=source,
1286 at_ref=source_ref,
1312 at_ref=source_ref,
1287 target=target,
1313 target=target,
1288 )
1314 )
1289
1315
1290 def _cleanup_merge_workspace(self, pull_request):
1316 def _cleanup_merge_workspace(self, pull_request):
1291 # Merging related cleanup
1317 # Merging related cleanup
1292 target_scm = pull_request.target_repo.scm_instance()
1318 target_scm = pull_request.target_repo.scm_instance()
1293 workspace_id = 'pr-%s' % pull_request.pull_request_id
1319 workspace_id = 'pr-%s' % pull_request.pull_request_id
1294
1320
1295 try:
1321 try:
1296 target_scm.cleanup_merge_workspace(workspace_id)
1322 target_scm.cleanup_merge_workspace(workspace_id)
1297 except NotImplementedError:
1323 except NotImplementedError:
1298 pass
1324 pass
1299
1325
1300 def _get_repo_pullrequest_sources(
1326 def _get_repo_pullrequest_sources(
1301 self, repo, commit_id=None, branch=None, bookmark=None):
1327 self, repo, commit_id=None, branch=None, bookmark=None):
1302 """
1328 """
1303 Return a structure with repo's interesting commits, suitable for
1329 Return a structure with repo's interesting commits, suitable for
1304 the selectors in pullrequest controller
1330 the selectors in pullrequest controller
1305
1331
1306 :param commit_id: a commit that must be in the list somehow
1332 :param commit_id: a commit that must be in the list somehow
1307 and selected by default
1333 and selected by default
1308 :param branch: a branch that must be in the list and selected
1334 :param branch: a branch that must be in the list and selected
1309 by default - even if closed
1335 by default - even if closed
1310 :param bookmark: a bookmark that must be in the list and selected
1336 :param bookmark: a bookmark that must be in the list and selected
1311 """
1337 """
1312
1338
1313 commit_id = safe_str(commit_id) if commit_id else None
1339 commit_id = safe_str(commit_id) if commit_id else None
1314 branch = safe_str(branch) if branch else None
1340 branch = safe_str(branch) if branch else None
1315 bookmark = safe_str(bookmark) if bookmark else None
1341 bookmark = safe_str(bookmark) if bookmark else None
1316
1342
1317 selected = None
1343 selected = None
1318
1344
1319 # order matters: first source that has commit_id in it will be selected
1345 # order matters: first source that has commit_id in it will be selected
1320 sources = []
1346 sources = []
1321 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1347 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1322 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1348 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1323
1349
1324 if commit_id:
1350 if commit_id:
1325 ref_commit = (h.short_id(commit_id), commit_id)
1351 ref_commit = (h.short_id(commit_id), commit_id)
1326 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1352 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1327
1353
1328 sources.append(
1354 sources.append(
1329 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1355 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1330 )
1356 )
1331
1357
1332 groups = []
1358 groups = []
1333 for group_key, ref_list, group_name, match in sources:
1359 for group_key, ref_list, group_name, match in sources:
1334 group_refs = []
1360 group_refs = []
1335 for ref_name, ref_id in ref_list:
1361 for ref_name, ref_id in ref_list:
1336 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1362 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1337 group_refs.append((ref_key, ref_name))
1363 group_refs.append((ref_key, ref_name))
1338
1364
1339 if not selected:
1365 if not selected:
1340 if set([commit_id, match]) & set([ref_id, ref_name]):
1366 if set([commit_id, match]) & set([ref_id, ref_name]):
1341 selected = ref_key
1367 selected = ref_key
1342
1368
1343 if group_refs:
1369 if group_refs:
1344 groups.append((group_refs, group_name))
1370 groups.append((group_refs, group_name))
1345
1371
1346 if not selected:
1372 if not selected:
1347 ref = commit_id or branch or bookmark
1373 ref = commit_id or branch or bookmark
1348 if ref:
1374 if ref:
1349 raise CommitDoesNotExistError(
1375 raise CommitDoesNotExistError(
1350 'No commit refs could be found matching: %s' % ref)
1376 'No commit refs could be found matching: %s' % ref)
1351 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1377 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1352 selected = 'branch:%s:%s' % (
1378 selected = 'branch:%s:%s' % (
1353 repo.DEFAULT_BRANCH_NAME,
1379 repo.DEFAULT_BRANCH_NAME,
1354 repo.branches[repo.DEFAULT_BRANCH_NAME]
1380 repo.branches[repo.DEFAULT_BRANCH_NAME]
1355 )
1381 )
1356 elif repo.commit_ids:
1382 elif repo.commit_ids:
1357 rev = repo.commit_ids[0]
1383 rev = repo.commit_ids[0]
1358 selected = 'rev:%s:%s' % (rev, rev)
1384 selected = 'rev:%s:%s' % (rev, rev)
1359 else:
1385 else:
1360 raise EmptyRepositoryError()
1386 raise EmptyRepositoryError()
1361 return groups, selected
1387 return groups, selected
1362
1388
1363 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1389 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1364 return self._get_diff_from_pr_or_version(
1390 return self._get_diff_from_pr_or_version(
1365 source_repo, source_ref_id, target_ref_id, context=context)
1391 source_repo, source_ref_id, target_ref_id, context=context)
1366
1392
1367 def _get_diff_from_pr_or_version(
1393 def _get_diff_from_pr_or_version(
1368 self, source_repo, source_ref_id, target_ref_id, context):
1394 self, source_repo, source_ref_id, target_ref_id, context):
1369 target_commit = source_repo.get_commit(
1395 target_commit = source_repo.get_commit(
1370 commit_id=safe_str(target_ref_id))
1396 commit_id=safe_str(target_ref_id))
1371 source_commit = source_repo.get_commit(
1397 source_commit = source_repo.get_commit(
1372 commit_id=safe_str(source_ref_id))
1398 commit_id=safe_str(source_ref_id))
1373 if isinstance(source_repo, Repository):
1399 if isinstance(source_repo, Repository):
1374 vcs_repo = source_repo.scm_instance()
1400 vcs_repo = source_repo.scm_instance()
1375 else:
1401 else:
1376 vcs_repo = source_repo
1402 vcs_repo = source_repo
1377
1403
1378 # TODO: johbo: In the context of an update, we cannot reach
1404 # TODO: johbo: In the context of an update, we cannot reach
1379 # the old commit anymore with our normal mechanisms. It needs
1405 # the old commit anymore with our normal mechanisms. It needs
1380 # some sort of special support in the vcs layer to avoid this
1406 # some sort of special support in the vcs layer to avoid this
1381 # workaround.
1407 # workaround.
1382 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1408 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1383 vcs_repo.alias == 'git'):
1409 vcs_repo.alias == 'git'):
1384 source_commit.raw_id = safe_str(source_ref_id)
1410 source_commit.raw_id = safe_str(source_ref_id)
1385
1411
1386 log.debug('calculating diff between '
1412 log.debug('calculating diff between '
1387 'source_ref:%s and target_ref:%s for repo `%s`',
1413 'source_ref:%s and target_ref:%s for repo `%s`',
1388 target_ref_id, source_ref_id,
1414 target_ref_id, source_ref_id,
1389 safe_unicode(vcs_repo.path))
1415 safe_unicode(vcs_repo.path))
1390
1416
1391 vcs_diff = vcs_repo.get_diff(
1417 vcs_diff = vcs_repo.get_diff(
1392 commit1=target_commit, commit2=source_commit, context=context)
1418 commit1=target_commit, commit2=source_commit, context=context)
1393 return vcs_diff
1419 return vcs_diff
1394
1420
1395 def _is_merge_enabled(self, pull_request):
1421 def _is_merge_enabled(self, pull_request):
1396 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1422 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1397 settings = settings_model.get_general_settings()
1423 settings = settings_model.get_general_settings()
1398 return settings.get('rhodecode_pr_merge_enabled', False)
1424 return settings.get('rhodecode_pr_merge_enabled', False)
1399
1425
1400 def _use_rebase_for_merging(self, pull_request):
1426 def _use_rebase_for_merging(self, pull_request):
1401 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1427 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1402 settings = settings_model.get_general_settings()
1428 settings = settings_model.get_general_settings()
1403 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1429 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1404
1430
1405 def _log_action(self, action, user, pull_request):
1431 def _log_audit_action(self, action, action_data, user, pull_request):
1406 action_logger(
1432 audit_logger.store(
1407 user,
1433 action=action,
1408 '{action}:{pr_id}'.format(
1434 action_data=action_data,
1409 action=action, pr_id=pull_request.pull_request_id),
1435 user=user,
1410 pull_request.target_repo)
1436 repo=pull_request.target_repo)
1411
1437
1412 def get_reviewer_functions(self):
1438 def get_reviewer_functions(self):
1413 """
1439 """
1414 Fetches functions for validation and fetching default reviewers.
1440 Fetches functions for validation and fetching default reviewers.
1415 If available we use the EE package, else we fallback to CE
1441 If available we use the EE package, else we fallback to CE
1416 package functions
1442 package functions
1417 """
1443 """
1418 try:
1444 try:
1419 from rc_reviewers.utils import get_default_reviewers_data
1445 from rc_reviewers.utils import get_default_reviewers_data
1420 from rc_reviewers.utils import validate_default_reviewers
1446 from rc_reviewers.utils import validate_default_reviewers
1421 except ImportError:
1447 except ImportError:
1422 from rhodecode.apps.repository.utils import \
1448 from rhodecode.apps.repository.utils import \
1423 get_default_reviewers_data
1449 get_default_reviewers_data
1424 from rhodecode.apps.repository.utils import \
1450 from rhodecode.apps.repository.utils import \
1425 validate_default_reviewers
1451 validate_default_reviewers
1426
1452
1427 return get_default_reviewers_data, validate_default_reviewers
1453 return get_default_reviewers_data, validate_default_reviewers
1428
1454
1429
1455
1430 class MergeCheck(object):
1456 class MergeCheck(object):
1431 """
1457 """
1432 Perform Merge Checks and returns a check object which stores information
1458 Perform Merge Checks and returns a check object which stores information
1433 about merge errors, and merge conditions
1459 about merge errors, and merge conditions
1434 """
1460 """
1435 TODO_CHECK = 'todo'
1461 TODO_CHECK = 'todo'
1436 PERM_CHECK = 'perm'
1462 PERM_CHECK = 'perm'
1437 REVIEW_CHECK = 'review'
1463 REVIEW_CHECK = 'review'
1438 MERGE_CHECK = 'merge'
1464 MERGE_CHECK = 'merge'
1439
1465
1440 def __init__(self):
1466 def __init__(self):
1441 self.review_status = None
1467 self.review_status = None
1442 self.merge_possible = None
1468 self.merge_possible = None
1443 self.merge_msg = ''
1469 self.merge_msg = ''
1444 self.failed = None
1470 self.failed = None
1445 self.errors = []
1471 self.errors = []
1446 self.error_details = OrderedDict()
1472 self.error_details = OrderedDict()
1447
1473
1448 def push_error(self, error_type, message, error_key, details):
1474 def push_error(self, error_type, message, error_key, details):
1449 self.failed = True
1475 self.failed = True
1450 self.errors.append([error_type, message])
1476 self.errors.append([error_type, message])
1451 self.error_details[error_key] = dict(
1477 self.error_details[error_key] = dict(
1452 details=details,
1478 details=details,
1453 error_type=error_type,
1479 error_type=error_type,
1454 message=message
1480 message=message
1455 )
1481 )
1456
1482
1457 @classmethod
1483 @classmethod
1458 def validate(cls, pull_request, user, fail_early=False, translator=None):
1484 def validate(cls, pull_request, user, fail_early=False, translator=None):
1459 # if migrated to pyramid...
1485 # if migrated to pyramid...
1460 # _ = lambda: translator or _ # use passed in translator if any
1486 # _ = lambda: translator or _ # use passed in translator if any
1461
1487
1462 merge_check = cls()
1488 merge_check = cls()
1463
1489
1464 # permissions to merge
1490 # permissions to merge
1465 user_allowed_to_merge = PullRequestModel().check_user_merge(
1491 user_allowed_to_merge = PullRequestModel().check_user_merge(
1466 pull_request, user)
1492 pull_request, user)
1467 if not user_allowed_to_merge:
1493 if not user_allowed_to_merge:
1468 log.debug("MergeCheck: cannot merge, approval is pending.")
1494 log.debug("MergeCheck: cannot merge, approval is pending.")
1469
1495
1470 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1496 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1471 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1497 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1472 if fail_early:
1498 if fail_early:
1473 return merge_check
1499 return merge_check
1474
1500
1475 # review status, must be always present
1501 # review status, must be always present
1476 review_status = pull_request.calculated_review_status()
1502 review_status = pull_request.calculated_review_status()
1477 merge_check.review_status = review_status
1503 merge_check.review_status = review_status
1478
1504
1479 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1505 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1480 if not status_approved:
1506 if not status_approved:
1481 log.debug("MergeCheck: cannot merge, approval is pending.")
1507 log.debug("MergeCheck: cannot merge, approval is pending.")
1482
1508
1483 msg = _('Pull request reviewer approval is pending.')
1509 msg = _('Pull request reviewer approval is pending.')
1484
1510
1485 merge_check.push_error(
1511 merge_check.push_error(
1486 'warning', msg, cls.REVIEW_CHECK, review_status)
1512 'warning', msg, cls.REVIEW_CHECK, review_status)
1487
1513
1488 if fail_early:
1514 if fail_early:
1489 return merge_check
1515 return merge_check
1490
1516
1491 # left over TODOs
1517 # left over TODOs
1492 todos = CommentsModel().get_unresolved_todos(pull_request)
1518 todos = CommentsModel().get_unresolved_todos(pull_request)
1493 if todos:
1519 if todos:
1494 log.debug("MergeCheck: cannot merge, {} "
1520 log.debug("MergeCheck: cannot merge, {} "
1495 "unresolved todos left.".format(len(todos)))
1521 "unresolved todos left.".format(len(todos)))
1496
1522
1497 if len(todos) == 1:
1523 if len(todos) == 1:
1498 msg = _('Cannot merge, {} TODO still not resolved.').format(
1524 msg = _('Cannot merge, {} TODO still not resolved.').format(
1499 len(todos))
1525 len(todos))
1500 else:
1526 else:
1501 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1527 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1502 len(todos))
1528 len(todos))
1503
1529
1504 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1530 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1505
1531
1506 if fail_early:
1532 if fail_early:
1507 return merge_check
1533 return merge_check
1508
1534
1509 # merge possible
1535 # merge possible
1510 merge_status, msg = PullRequestModel().merge_status(pull_request)
1536 merge_status, msg = PullRequestModel().merge_status(pull_request)
1511 merge_check.merge_possible = merge_status
1537 merge_check.merge_possible = merge_status
1512 merge_check.merge_msg = msg
1538 merge_check.merge_msg = msg
1513 if not merge_status:
1539 if not merge_status:
1514 log.debug(
1540 log.debug(
1515 "MergeCheck: cannot merge, pull request merge not possible.")
1541 "MergeCheck: cannot merge, pull request merge not possible.")
1516 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1542 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1517
1543
1518 if fail_early:
1544 if fail_early:
1519 return merge_check
1545 return merge_check
1520
1546
1521 return merge_check
1547 return merge_check
1522
1548
1523
1549
1524 ChangeTuple = namedtuple('ChangeTuple',
1550 ChangeTuple = namedtuple('ChangeTuple',
1525 ['added', 'common', 'removed', 'total'])
1551 ['added', 'common', 'removed', 'total'])
1526
1552
1527 FileChangeTuple = namedtuple('FileChangeTuple',
1553 FileChangeTuple = namedtuple('FileChangeTuple',
1528 ['added', 'modified', 'removed'])
1554 ['added', 'modified', 'removed'])
@@ -1,210 +1,213 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-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
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
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
20
21 import pytest
21 import pytest
22 from rhodecode.model.db import User, UserIpMap
22 from rhodecode.model.db import User, UserIpMap
23 from rhodecode.model.permission import PermissionModel
23 from rhodecode.model.permission import PermissionModel
24 from rhodecode.tests import (
24 from rhodecode.tests import (
25 TestController, url, clear_all_caches, assert_session_flash)
25 TestController, url, clear_all_caches, assert_session_flash)
26
26
27
27
28 class TestAdminPermissionsController(TestController):
28 class TestAdminPermissionsController(TestController):
29
29
30 @pytest.fixture(scope='class', autouse=True)
30 @pytest.fixture(scope='class', autouse=True)
31 def prepare(self, request):
31 def prepare(self, request):
32 # cleanup and reset to default permissions after
32 # cleanup and reset to default permissions after
33 @request.addfinalizer
33 @request.addfinalizer
34 def cleanup():
34 def cleanup():
35 PermissionModel().create_default_user_permissions(
35 PermissionModel().create_default_user_permissions(
36 User.get_default_user(), force=True)
36 User.get_default_user(), force=True)
37
37
38 def test_index_application(self):
38 def test_index_application(self):
39 self.log_user()
39 self.log_user()
40 self.app.get(url('admin_permissions_application'))
40 self.app.get(url('admin_permissions_application'))
41
41
42 @pytest.mark.parametrize(
42 @pytest.mark.parametrize(
43 'anonymous, default_register, default_register_message, default_password_reset,'
43 'anonymous, default_register, default_register_message, default_password_reset,'
44 'default_extern_activate, expect_error, expect_form_error', [
44 'default_extern_activate, expect_error, expect_form_error', [
45 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
45 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
46 False, False),
46 False, False),
47 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
47 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
48 False, False),
48 False, False),
49 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
49 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
50 False, False),
50 False, False),
51 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
51 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
52 False, False),
52 False, False),
53 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
53 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
54 False, True),
54 False, True),
55 (True, '', '', 'hg.password_reset.enabled', '', True, False),
55 (True, '', '', 'hg.password_reset.enabled', '', True, False),
56 ])
56 ])
57 def test_update_application_permissions(
57 def test_update_application_permissions(
58 self, anonymous, default_register, default_register_message, default_password_reset,
58 self, anonymous, default_register, default_register_message, default_password_reset,
59 default_extern_activate, expect_error, expect_form_error):
59 default_extern_activate, expect_error, expect_form_error):
60
60
61 self.log_user()
61 self.log_user()
62
62
63 # TODO: anonymous access set here to False, breaks some other tests
63 # TODO: anonymous access set here to False, breaks some other tests
64 params = {
64 params = {
65 'csrf_token': self.csrf_token,
65 'csrf_token': self.csrf_token,
66 'anonymous': anonymous,
66 'anonymous': anonymous,
67 'default_register': default_register,
67 'default_register': default_register,
68 'default_register_message': default_register_message,
68 'default_register_message': default_register_message,
69 'default_password_reset': default_password_reset,
69 'default_password_reset': default_password_reset,
70 'default_extern_activate': default_extern_activate,
70 'default_extern_activate': default_extern_activate,
71 }
71 }
72 response = self.app.post(url('admin_permissions_application'),
72 response = self.app.post(url('admin_permissions_application'),
73 params=params)
73 params=params)
74 if expect_form_error:
74 if expect_form_error:
75 assert response.status_int == 200
75 assert response.status_int == 200
76 response.mustcontain('Value must be one of')
76 response.mustcontain('Value must be one of')
77 else:
77 else:
78 if expect_error:
78 if expect_error:
79 msg = 'Error occurred during update of permissions'
79 msg = 'Error occurred during update of permissions'
80 else:
80 else:
81 msg = 'Application permissions updated successfully'
81 msg = 'Application permissions updated successfully'
82 assert_session_flash(response, msg)
82 assert_session_flash(response, msg)
83
83
84 def test_index_object(self):
84 def test_index_object(self):
85 self.log_user()
85 self.log_user()
86 self.app.get(url('admin_permissions_object'))
86 self.app.get(url('admin_permissions_object'))
87
87
88 @pytest.mark.parametrize(
88 @pytest.mark.parametrize(
89 'repo, repo_group, user_group, expect_error, expect_form_error', [
89 'repo, repo_group, user_group, expect_error, expect_form_error', [
90 ('repository.none', 'group.none', 'usergroup.none', False, False),
90 ('repository.none', 'group.none', 'usergroup.none', False, False),
91 ('repository.read', 'group.read', 'usergroup.read', False, False),
91 ('repository.read', 'group.read', 'usergroup.read', False, False),
92 ('repository.write', 'group.write', 'usergroup.write',
92 ('repository.write', 'group.write', 'usergroup.write',
93 False, False),
93 False, False),
94 ('repository.admin', 'group.admin', 'usergroup.admin',
94 ('repository.admin', 'group.admin', 'usergroup.admin',
95 False, False),
95 False, False),
96 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
96 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
97 ('', '', '', True, False),
97 ('', '', '', True, False),
98 ])
98 ])
99 def test_update_object_permissions(self, repo, repo_group, user_group,
99 def test_update_object_permissions(self, repo, repo_group, user_group,
100 expect_error, expect_form_error):
100 expect_error, expect_form_error):
101 self.log_user()
101 self.log_user()
102
102
103 params = {
103 params = {
104 'csrf_token': self.csrf_token,
104 'csrf_token': self.csrf_token,
105 'default_repo_perm': repo,
105 'default_repo_perm': repo,
106 'overwrite_default_repo': False,
106 'overwrite_default_repo': False,
107 'default_group_perm': repo_group,
107 'default_group_perm': repo_group,
108 'overwrite_default_group': False,
108 'overwrite_default_group': False,
109 'default_user_group_perm': user_group,
109 'default_user_group_perm': user_group,
110 'overwrite_default_user_group': False,
110 'overwrite_default_user_group': False,
111 }
111 }
112 response = self.app.post(url('admin_permissions_object'),
112 response = self.app.post(url('admin_permissions_object'),
113 params=params)
113 params=params)
114 if expect_form_error:
114 if expect_form_error:
115 assert response.status_int == 200
115 assert response.status_int == 200
116 response.mustcontain('Value must be one of')
116 response.mustcontain('Value must be one of')
117 else:
117 else:
118 if expect_error:
118 if expect_error:
119 msg = 'Error occurred during update of permissions'
119 msg = 'Error occurred during update of permissions'
120 else:
120 else:
121 msg = 'Object permissions updated successfully'
121 msg = 'Object permissions updated successfully'
122 assert_session_flash(response, msg)
122 assert_session_flash(response, msg)
123
123
124 def test_index_global(self):
124 def test_index_global(self):
125 self.log_user()
125 self.log_user()
126 self.app.get(url('admin_permissions_global'))
126 self.app.get(url('admin_permissions_global'))
127
127
128 @pytest.mark.parametrize(
128 @pytest.mark.parametrize(
129 'repo_create, repo_create_write, user_group_create, repo_group_create,'
129 'repo_create, repo_create_write, user_group_create, repo_group_create,'
130 'fork_create, inherit_default_permissions, expect_error,'
130 'fork_create, inherit_default_permissions, expect_error,'
131 'expect_form_error', [
131 'expect_form_error', [
132 ('hg.create.none', 'hg.create.write_on_repogroup.false',
132 ('hg.create.none', 'hg.create.write_on_repogroup.false',
133 'hg.usergroup.create.false', 'hg.repogroup.create.false',
133 'hg.usergroup.create.false', 'hg.repogroup.create.false',
134 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
134 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
135 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
135 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
136 'hg.usergroup.create.true', 'hg.repogroup.create.true',
136 'hg.usergroup.create.true', 'hg.repogroup.create.true',
137 'hg.fork.repository', 'hg.inherit_default_perms.false',
137 'hg.fork.repository', 'hg.inherit_default_perms.false',
138 False, False),
138 False, False),
139 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
139 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
140 'hg.usergroup.create.true', 'hg.repogroup.create.true',
140 'hg.usergroup.create.true', 'hg.repogroup.create.true',
141 'hg.fork.repository', 'hg.inherit_default_perms.false',
141 'hg.fork.repository', 'hg.inherit_default_perms.false',
142 False, True),
142 False, True),
143 ('', '', '', '', '', '', True, False),
143 ('', '', '', '', '', '', True, False),
144 ])
144 ])
145 def test_update_global_permissions(
145 def test_update_global_permissions(
146 self, repo_create, repo_create_write, user_group_create,
146 self, repo_create, repo_create_write, user_group_create,
147 repo_group_create, fork_create, inherit_default_permissions,
147 repo_group_create, fork_create, inherit_default_permissions,
148 expect_error, expect_form_error):
148 expect_error, expect_form_error):
149 self.log_user()
149 self.log_user()
150
150
151 params = {
151 params = {
152 'csrf_token': self.csrf_token,
152 'csrf_token': self.csrf_token,
153 'default_repo_create': repo_create,
153 'default_repo_create': repo_create,
154 'default_repo_create_on_write': repo_create_write,
154 'default_repo_create_on_write': repo_create_write,
155 'default_user_group_create': user_group_create,
155 'default_user_group_create': user_group_create,
156 'default_repo_group_create': repo_group_create,
156 'default_repo_group_create': repo_group_create,
157 'default_fork_create': fork_create,
157 'default_fork_create': fork_create,
158 'default_inherit_default_permissions': inherit_default_permissions
158 'default_inherit_default_permissions': inherit_default_permissions
159 }
159 }
160 response = self.app.post(url('admin_permissions_global'),
160 response = self.app.post(url('admin_permissions_global'),
161 params=params)
161 params=params)
162 if expect_form_error:
162 if expect_form_error:
163 assert response.status_int == 200
163 assert response.status_int == 200
164 response.mustcontain('Value must be one of')
164 response.mustcontain('Value must be one of')
165 else:
165 else:
166 if expect_error:
166 if expect_error:
167 msg = 'Error occurred during update of permissions'
167 msg = 'Error occurred during update of permissions'
168 else:
168 else:
169 msg = 'Global permissions updated successfully'
169 msg = 'Global permissions updated successfully'
170 assert_session_flash(response, msg)
170 assert_session_flash(response, msg)
171
171
172 def test_index_ips(self):
172 def test_index_ips(self):
173 self.log_user()
173 self.log_user()
174 response = self.app.get(url('admin_permissions_ips'))
174 response = self.app.get(url('admin_permissions_ips'))
175 # TODO: Test response...
175 # TODO: Test response...
176 response.mustcontain('All IP addresses are allowed')
176 response.mustcontain('All IP addresses are allowed')
177
177
178 def test_add_delete_ips(self):
178 def test_add_delete_ips(self):
179 self.log_user()
179 self.log_user()
180 clear_all_caches()
180 clear_all_caches()
181
181
182 # ADD
182 # ADD
183 default_user_id = User.get_default_user().user_id
183 default_user_id = User.get_default_user().user_id
184 response = self.app.post(
184 response = self.app.post(
185 url('edit_user_ips', user_id=default_user_id),
185 url('edit_user_ips', user_id=default_user_id),
186 params={'new_ip': '127.0.0.0/24', '_method': 'put',
186 params={'new_ip': '127.0.0.0/24', '_method': 'put',
187 'csrf_token': self.csrf_token})
187 'csrf_token': self.csrf_token})
188
188
189 response = self.app.get(url('admin_permissions_ips'))
189 response = self.app.get(url('admin_permissions_ips'))
190 response.mustcontain('127.0.0.0/24')
190 response.mustcontain('127.0.0.0/24')
191 response.mustcontain('127.0.0.0 - 127.0.0.255')
191 response.mustcontain('127.0.0.0 - 127.0.0.255')
192
192
193 # DELETE
193 # DELETE
194 default_user_id = User.get_default_user().user_id
194 default_user_id = User.get_default_user().user_id
195 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
195 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
196 default_user_id).first().ip_id
196 default_user_id).first().ip_id
197
197
198 response = self.app.post(
198 response = self.app.post(
199 url('edit_user_ips', user_id=default_user_id),
199 url('edit_user_ips', user_id=default_user_id),
200 params={'_method': 'delete', 'del_ip_id': del_ip_id,
200 params={'_method': 'delete', 'del_ip_id': del_ip_id,
201 'csrf_token': self.csrf_token})
201 'csrf_token': self.csrf_token})
202
203 assert_session_flash(response, 'Removed ip address from user whitelist')
204
202 clear_all_caches()
205 clear_all_caches()
203 response = self.app.get(url('admin_permissions_ips'))
206 response = self.app.get(url('admin_permissions_ips'))
204 response.mustcontain('All IP addresses are allowed')
207 response.mustcontain('All IP addresses are allowed')
205 response.mustcontain(no=['127.0.0.0/24'])
208 response.mustcontain(no=['127.0.0.0/24'])
206 response.mustcontain(no=['127.0.0.0 - 127.0.0.255'])
209 response.mustcontain(no=['127.0.0.0 - 127.0.0.255'])
207
210
208 def test_index_overview(self):
211 def test_index_overview(self):
209 self.log_user()
212 self.log_user()
210 self.app.get(url('admin_permissions_overview'))
213 self.app.get(url('admin_permissions_overview'))
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
General Comments 0
You need to be logged in to leave comments. Login now