##// 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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -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