##// END OF EJS Templates
pull-requests: simplified the UI for pr view....
marcink -
r4136:12e6938f default
parent child Browse files
Show More
@@ -1,1217 +1,1215 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.tests import (
33 from rhodecode.tests import (
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib
39
39
40 base_url = {
40 base_url = {
41 'repo_changelog': '/{repo_name}/changelog',
41 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_commits': '/{repo_name}/commits',
43 'repo_commits': '/{repo_name}/commits',
44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 }[name].format(**kwargs)
57 }[name].format(**kwargs)
58
58
59 if params:
59 if params:
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 return base_url
61 return base_url
62
62
63
63
64 @pytest.mark.usefixtures('app', 'autologin_user')
64 @pytest.mark.usefixtures('app', 'autologin_user')
65 @pytest.mark.backends("git", "hg")
65 @pytest.mark.backends("git", "hg")
66 class TestPullrequestsView(object):
66 class TestPullrequestsView(object):
67
67
68 def test_index(self, backend):
68 def test_index(self, backend):
69 self.app.get(route_path(
69 self.app.get(route_path(
70 'pullrequest_new',
70 'pullrequest_new',
71 repo_name=backend.repo_name))
71 repo_name=backend.repo_name))
72
72
73 def test_option_menu_create_pull_request_exists(self, backend):
73 def test_option_menu_create_pull_request_exists(self, backend):
74 repo_name = backend.repo_name
74 repo_name = backend.repo_name
75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
76
76
77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
78 'pullrequest_new', repo_name=repo_name)
78 'pullrequest_new', repo_name=repo_name)
79 response.mustcontain(create_pr_link)
79 response.mustcontain(create_pr_link)
80
80
81 def test_create_pr_form_with_raw_commit_id(self, backend):
81 def test_create_pr_form_with_raw_commit_id(self, backend):
82 repo = backend.repo
82 repo = backend.repo
83
83
84 self.app.get(
84 self.app.get(
85 route_path('pullrequest_new', repo_name=repo.repo_name,
85 route_path('pullrequest_new', repo_name=repo.repo_name,
86 commit=repo.get_commit().raw_id),
86 commit=repo.get_commit().raw_id),
87 status=200)
87 status=200)
88
88
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 @pytest.mark.parametrize('range_diff', ["0", "1"])
90 @pytest.mark.parametrize('range_diff', ["0", "1"])
91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
92 pull_request = pr_util.create_pull_request(
92 pull_request = pr_util.create_pull_request(
93 mergeable=pr_merge_enabled, enable_notifications=False)
93 mergeable=pr_merge_enabled, enable_notifications=False)
94
94
95 response = self.app.get(route_path(
95 response = self.app.get(route_path(
96 'pullrequest_show',
96 'pullrequest_show',
97 repo_name=pull_request.target_repo.scm_instance().name,
97 repo_name=pull_request.target_repo.scm_instance().name,
98 pull_request_id=pull_request.pull_request_id,
98 pull_request_id=pull_request.pull_request_id,
99 params={'range-diff': range_diff}))
99 params={'range-diff': range_diff}))
100
100
101 for commit_id in pull_request.revisions:
101 for commit_id in pull_request.revisions:
102 response.mustcontain(commit_id)
102 response.mustcontain(commit_id)
103
103
104 assert pull_request.target_ref_parts.type in response
104 response.mustcontain(pull_request.target_ref_parts.type)
105 assert pull_request.target_ref_parts.name in response
105 response.mustcontain(pull_request.target_ref_parts.name)
106 target_clone_url = pull_request.target_repo.clone_url()
107 assert target_clone_url in response
108
106
109 assert 'class="pull-request-merge"' in response
107 response.mustcontain('class="pull-request-merge"')
108
110 if pr_merge_enabled:
109 if pr_merge_enabled:
111 response.mustcontain('Pull request reviewer approval is pending')
110 response.mustcontain('Pull request reviewer approval is pending')
112 else:
111 else:
113 response.mustcontain('Server-side pull request merging is disabled.')
112 response.mustcontain('Server-side pull request merging is disabled.')
114
113
115 if range_diff == "1":
114 if range_diff == "1":
116 response.mustcontain('Turn off: Show the diff as commit range')
115 response.mustcontain('Turn off: Show the diff as commit range')
117
116
118 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
117 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 # Logout
118 # Logout
120 response = self.app.post(
119 response = self.app.post(
121 h.route_path('logout'),
120 h.route_path('logout'),
122 params={'csrf_token': csrf_token})
121 params={'csrf_token': csrf_token})
123 # Login as regular user
122 # Login as regular user
124 response = self.app.post(h.route_path('login'),
123 response = self.app.post(h.route_path('login'),
125 {'username': TEST_USER_REGULAR_LOGIN,
124 {'username': TEST_USER_REGULAR_LOGIN,
126 'password': 'test12'})
125 'password': 'test12'})
127
126
128 pull_request = pr_util.create_pull_request(
127 pull_request = pr_util.create_pull_request(
129 author=TEST_USER_REGULAR_LOGIN)
128 author=TEST_USER_REGULAR_LOGIN)
130
129
131 response = self.app.get(route_path(
130 response = self.app.get(route_path(
132 'pullrequest_show',
131 'pullrequest_show',
133 repo_name=pull_request.target_repo.scm_instance().name,
132 repo_name=pull_request.target_repo.scm_instance().name,
134 pull_request_id=pull_request.pull_request_id))
133 pull_request_id=pull_request.pull_request_id))
135
134
136 response.mustcontain('Server-side pull request merging is disabled.')
135 response.mustcontain('Server-side pull request merging is disabled.')
137
136
138 assert_response = response.assert_response()
137 assert_response = response.assert_response()
139 # for regular user without a merge permissions, we don't see it
138 # for regular user without a merge permissions, we don't see it
140 assert_response.no_element_exists('#close-pull-request-action')
139 assert_response.no_element_exists('#close-pull-request-action')
141
140
142 user_util.grant_user_permission_to_repo(
141 user_util.grant_user_permission_to_repo(
143 pull_request.target_repo,
142 pull_request.target_repo,
144 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
143 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 'repository.write')
144 'repository.write')
146 response = self.app.get(route_path(
145 response = self.app.get(route_path(
147 'pullrequest_show',
146 'pullrequest_show',
148 repo_name=pull_request.target_repo.scm_instance().name,
147 repo_name=pull_request.target_repo.scm_instance().name,
149 pull_request_id=pull_request.pull_request_id))
148 pull_request_id=pull_request.pull_request_id))
150
149
151 response.mustcontain('Server-side pull request merging is disabled.')
150 response.mustcontain('Server-side pull request merging is disabled.')
152
151
153 assert_response = response.assert_response()
152 assert_response = response.assert_response()
154 # now regular user has a merge permissions, we have CLOSE button
153 # now regular user has a merge permissions, we have CLOSE button
155 assert_response.one_element_exists('#close-pull-request-action')
154 assert_response.one_element_exists('#close-pull-request-action')
156
155
157 def test_show_invalid_commit_id(self, pr_util):
156 def test_show_invalid_commit_id(self, pr_util):
158 # Simulating invalid revisions which will cause a lookup error
157 # Simulating invalid revisions which will cause a lookup error
159 pull_request = pr_util.create_pull_request()
158 pull_request = pr_util.create_pull_request()
160 pull_request.revisions = ['invalid']
159 pull_request.revisions = ['invalid']
161 Session().add(pull_request)
160 Session().add(pull_request)
162 Session().commit()
161 Session().commit()
163
162
164 response = self.app.get(route_path(
163 response = self.app.get(route_path(
165 'pullrequest_show',
164 'pullrequest_show',
166 repo_name=pull_request.target_repo.scm_instance().name,
165 repo_name=pull_request.target_repo.scm_instance().name,
167 pull_request_id=pull_request.pull_request_id))
166 pull_request_id=pull_request.pull_request_id))
168
167
169 for commit_id in pull_request.revisions:
168 for commit_id in pull_request.revisions:
170 response.mustcontain(commit_id)
169 response.mustcontain(commit_id)
171
170
172 def test_show_invalid_source_reference(self, pr_util):
171 def test_show_invalid_source_reference(self, pr_util):
173 pull_request = pr_util.create_pull_request()
172 pull_request = pr_util.create_pull_request()
174 pull_request.source_ref = 'branch:b:invalid'
173 pull_request.source_ref = 'branch:b:invalid'
175 Session().add(pull_request)
174 Session().add(pull_request)
176 Session().commit()
175 Session().commit()
177
176
178 self.app.get(route_path(
177 self.app.get(route_path(
179 'pullrequest_show',
178 'pullrequest_show',
180 repo_name=pull_request.target_repo.scm_instance().name,
179 repo_name=pull_request.target_repo.scm_instance().name,
181 pull_request_id=pull_request.pull_request_id))
180 pull_request_id=pull_request.pull_request_id))
182
181
183 def test_edit_title_description(self, pr_util, csrf_token):
182 def test_edit_title_description(self, pr_util, csrf_token):
184 pull_request = pr_util.create_pull_request()
183 pull_request = pr_util.create_pull_request()
185 pull_request_id = pull_request.pull_request_id
184 pull_request_id = pull_request.pull_request_id
186
185
187 response = self.app.post(
186 response = self.app.post(
188 route_path('pullrequest_update',
187 route_path('pullrequest_update',
189 repo_name=pull_request.target_repo.repo_name,
188 repo_name=pull_request.target_repo.repo_name,
190 pull_request_id=pull_request_id),
189 pull_request_id=pull_request_id),
191 params={
190 params={
192 'edit_pull_request': 'true',
191 'edit_pull_request': 'true',
193 'title': 'New title',
192 'title': 'New title',
194 'description': 'New description',
193 'description': 'New description',
195 'csrf_token': csrf_token})
194 'csrf_token': csrf_token})
196
195
197 assert_session_flash(
196 assert_session_flash(
198 response, u'Pull request title & description updated.',
197 response, u'Pull request title & description updated.',
199 category='success')
198 category='success')
200
199
201 pull_request = PullRequest.get(pull_request_id)
200 pull_request = PullRequest.get(pull_request_id)
202 assert pull_request.title == 'New title'
201 assert pull_request.title == 'New title'
203 assert pull_request.description == 'New description'
202 assert pull_request.description == 'New description'
204
203
205 def test_edit_title_description_closed(self, pr_util, csrf_token):
204 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 pull_request = pr_util.create_pull_request()
205 pull_request = pr_util.create_pull_request()
207 pull_request_id = pull_request.pull_request_id
206 pull_request_id = pull_request.pull_request_id
208 repo_name = pull_request.target_repo.repo_name
207 repo_name = pull_request.target_repo.repo_name
209 pr_util.close()
208 pr_util.close()
210
209
211 response = self.app.post(
210 response = self.app.post(
212 route_path('pullrequest_update',
211 route_path('pullrequest_update',
213 repo_name=repo_name, pull_request_id=pull_request_id),
212 repo_name=repo_name, pull_request_id=pull_request_id),
214 params={
213 params={
215 'edit_pull_request': 'true',
214 'edit_pull_request': 'true',
216 'title': 'New title',
215 'title': 'New title',
217 'description': 'New description',
216 'description': 'New description',
218 'csrf_token': csrf_token}, status=200)
217 'csrf_token': csrf_token}, status=200)
219 assert_session_flash(
218 assert_session_flash(
220 response, u'Cannot update closed pull requests.',
219 response, u'Cannot update closed pull requests.',
221 category='error')
220 category='error')
222
221
223 def test_update_invalid_source_reference(self, pr_util, csrf_token):
222 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
223 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225
224
226 pull_request = pr_util.create_pull_request()
225 pull_request = pr_util.create_pull_request()
227 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
226 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 Session().add(pull_request)
227 Session().add(pull_request)
229 Session().commit()
228 Session().commit()
230
229
231 pull_request_id = pull_request.pull_request_id
230 pull_request_id = pull_request.pull_request_id
232
231
233 response = self.app.post(
232 response = self.app.post(
234 route_path('pullrequest_update',
233 route_path('pullrequest_update',
235 repo_name=pull_request.target_repo.repo_name,
234 repo_name=pull_request.target_repo.repo_name,
236 pull_request_id=pull_request_id),
235 pull_request_id=pull_request_id),
237 params={'update_commits': 'true', 'csrf_token': csrf_token})
236 params={'update_commits': 'true', 'csrf_token': csrf_token})
238
237
239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
238 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 UpdateFailureReason.MISSING_SOURCE_REF])
239 UpdateFailureReason.MISSING_SOURCE_REF])
241 assert_session_flash(response, expected_msg, category='error')
240 assert_session_flash(response, expected_msg, category='error')
242
241
243 def test_missing_target_reference(self, pr_util, csrf_token):
242 def test_missing_target_reference(self, pr_util, csrf_token):
244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
243 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 pull_request = pr_util.create_pull_request(
244 pull_request = pr_util.create_pull_request(
246 approved=True, mergeable=True)
245 approved=True, mergeable=True)
247 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
246 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 pull_request.target_ref = unicode_reference
247 pull_request.target_ref = unicode_reference
249 Session().add(pull_request)
248 Session().add(pull_request)
250 Session().commit()
249 Session().commit()
251
250
252 pull_request_id = pull_request.pull_request_id
251 pull_request_id = pull_request.pull_request_id
253 pull_request_url = route_path(
252 pull_request_url = route_path(
254 'pullrequest_show',
253 'pullrequest_show',
255 repo_name=pull_request.target_repo.repo_name,
254 repo_name=pull_request.target_repo.repo_name,
256 pull_request_id=pull_request_id)
255 pull_request_id=pull_request_id)
257
256
258 response = self.app.get(pull_request_url)
257 response = self.app.get(pull_request_url)
259 target_ref_id = 'invalid-branch'
258 target_ref_id = 'invalid-branch'
260 merge_resp = MergeResponse(
259 merge_resp = MergeResponse(
261 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
260 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
261 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 response.assert_response().element_contains(
262 response.assert_response().element_contains(
264 'div[data-role="merge-message"]', merge_resp.merge_status_message)
263 'div[data-role="merge-message"]', merge_resp.merge_status_message)
265
264
266 def test_comment_and_close_pull_request_custom_message_approved(
265 def test_comment_and_close_pull_request_custom_message_approved(
267 self, pr_util, csrf_token, xhr_header):
266 self, pr_util, csrf_token, xhr_header):
268
267
269 pull_request = pr_util.create_pull_request(approved=True)
268 pull_request = pr_util.create_pull_request(approved=True)
270 pull_request_id = pull_request.pull_request_id
269 pull_request_id = pull_request.pull_request_id
271 author = pull_request.user_id
270 author = pull_request.user_id
272 repo = pull_request.target_repo.repo_id
271 repo = pull_request.target_repo.repo_id
273
272
274 self.app.post(
273 self.app.post(
275 route_path('pullrequest_comment_create',
274 route_path('pullrequest_comment_create',
276 repo_name=pull_request.target_repo.scm_instance().name,
275 repo_name=pull_request.target_repo.scm_instance().name,
277 pull_request_id=pull_request_id),
276 pull_request_id=pull_request_id),
278 params={
277 params={
279 'close_pull_request': '1',
278 'close_pull_request': '1',
280 'text': 'Closing a PR',
279 'text': 'Closing a PR',
281 'csrf_token': csrf_token},
280 'csrf_token': csrf_token},
282 extra_environ=xhr_header,)
281 extra_environ=xhr_header,)
283
282
284 journal = UserLog.query()\
283 journal = UserLog.query()\
285 .filter(UserLog.user_id == author)\
284 .filter(UserLog.user_id == author)\
286 .filter(UserLog.repository_id == repo) \
285 .filter(UserLog.repository_id == repo) \
287 .order_by(UserLog.user_log_id.asc()) \
286 .order_by(UserLog.user_log_id.asc()) \
288 .all()
287 .all()
289 assert journal[-1].action == 'repo.pull_request.close'
288 assert journal[-1].action == 'repo.pull_request.close'
290
289
291 pull_request = PullRequest.get(pull_request_id)
290 pull_request = PullRequest.get(pull_request_id)
292 assert pull_request.is_closed()
291 assert pull_request.is_closed()
293
292
294 status = ChangesetStatusModel().get_status(
293 status = ChangesetStatusModel().get_status(
295 pull_request.source_repo, pull_request=pull_request)
294 pull_request.source_repo, pull_request=pull_request)
296 assert status == ChangesetStatus.STATUS_APPROVED
295 assert status == ChangesetStatus.STATUS_APPROVED
297 comments = ChangesetComment().query() \
296 comments = ChangesetComment().query() \
298 .filter(ChangesetComment.pull_request == pull_request) \
297 .filter(ChangesetComment.pull_request == pull_request) \
299 .order_by(ChangesetComment.comment_id.asc())\
298 .order_by(ChangesetComment.comment_id.asc())\
300 .all()
299 .all()
301 assert comments[-1].text == 'Closing a PR'
300 assert comments[-1].text == 'Closing a PR'
302
301
303 def test_comment_force_close_pull_request_rejected(
302 def test_comment_force_close_pull_request_rejected(
304 self, pr_util, csrf_token, xhr_header):
303 self, pr_util, csrf_token, xhr_header):
305 pull_request = pr_util.create_pull_request()
304 pull_request = pr_util.create_pull_request()
306 pull_request_id = pull_request.pull_request_id
305 pull_request_id = pull_request.pull_request_id
307 PullRequestModel().update_reviewers(
306 PullRequestModel().update_reviewers(
308 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
307 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 pull_request.author)
308 pull_request.author)
310 author = pull_request.user_id
309 author = pull_request.user_id
311 repo = pull_request.target_repo.repo_id
310 repo = pull_request.target_repo.repo_id
312
311
313 self.app.post(
312 self.app.post(
314 route_path('pullrequest_comment_create',
313 route_path('pullrequest_comment_create',
315 repo_name=pull_request.target_repo.scm_instance().name,
314 repo_name=pull_request.target_repo.scm_instance().name,
316 pull_request_id=pull_request_id),
315 pull_request_id=pull_request_id),
317 params={
316 params={
318 'close_pull_request': '1',
317 'close_pull_request': '1',
319 'csrf_token': csrf_token},
318 'csrf_token': csrf_token},
320 extra_environ=xhr_header)
319 extra_environ=xhr_header)
321
320
322 pull_request = PullRequest.get(pull_request_id)
321 pull_request = PullRequest.get(pull_request_id)
323
322
324 journal = UserLog.query()\
323 journal = UserLog.query()\
325 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
324 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 .order_by(UserLog.user_log_id.asc()) \
325 .order_by(UserLog.user_log_id.asc()) \
327 .all()
326 .all()
328 assert journal[-1].action == 'repo.pull_request.close'
327 assert journal[-1].action == 'repo.pull_request.close'
329
328
330 # check only the latest status, not the review status
329 # check only the latest status, not the review status
331 status = ChangesetStatusModel().get_status(
330 status = ChangesetStatusModel().get_status(
332 pull_request.source_repo, pull_request=pull_request)
331 pull_request.source_repo, pull_request=pull_request)
333 assert status == ChangesetStatus.STATUS_REJECTED
332 assert status == ChangesetStatus.STATUS_REJECTED
334
333
335 def test_comment_and_close_pull_request(
334 def test_comment_and_close_pull_request(
336 self, pr_util, csrf_token, xhr_header):
335 self, pr_util, csrf_token, xhr_header):
337 pull_request = pr_util.create_pull_request()
336 pull_request = pr_util.create_pull_request()
338 pull_request_id = pull_request.pull_request_id
337 pull_request_id = pull_request.pull_request_id
339
338
340 response = self.app.post(
339 response = self.app.post(
341 route_path('pullrequest_comment_create',
340 route_path('pullrequest_comment_create',
342 repo_name=pull_request.target_repo.scm_instance().name,
341 repo_name=pull_request.target_repo.scm_instance().name,
343 pull_request_id=pull_request.pull_request_id),
342 pull_request_id=pull_request.pull_request_id),
344 params={
343 params={
345 'close_pull_request': 'true',
344 'close_pull_request': 'true',
346 'csrf_token': csrf_token},
345 'csrf_token': csrf_token},
347 extra_environ=xhr_header)
346 extra_environ=xhr_header)
348
347
349 assert response.json
348 assert response.json
350
349
351 pull_request = PullRequest.get(pull_request_id)
350 pull_request = PullRequest.get(pull_request_id)
352 assert pull_request.is_closed()
351 assert pull_request.is_closed()
353
352
354 # check only the latest status, not the review status
353 # check only the latest status, not the review status
355 status = ChangesetStatusModel().get_status(
354 status = ChangesetStatusModel().get_status(
356 pull_request.source_repo, pull_request=pull_request)
355 pull_request.source_repo, pull_request=pull_request)
357 assert status == ChangesetStatus.STATUS_REJECTED
356 assert status == ChangesetStatus.STATUS_REJECTED
358
357
359 def test_create_pull_request(self, backend, csrf_token):
358 def test_create_pull_request(self, backend, csrf_token):
360 commits = [
359 commits = [
361 {'message': 'ancestor'},
360 {'message': 'ancestor'},
362 {'message': 'change'},
361 {'message': 'change'},
363 {'message': 'change2'},
362 {'message': 'change2'},
364 ]
363 ]
365 commit_ids = backend.create_master_repo(commits)
364 commit_ids = backend.create_master_repo(commits)
366 target = backend.create_repo(heads=['ancestor'])
365 target = backend.create_repo(heads=['ancestor'])
367 source = backend.create_repo(heads=['change2'])
366 source = backend.create_repo(heads=['change2'])
368
367
369 response = self.app.post(
368 response = self.app.post(
370 route_path('pullrequest_create', repo_name=source.repo_name),
369 route_path('pullrequest_create', repo_name=source.repo_name),
371 [
370 [
372 ('source_repo', source.repo_name),
371 ('source_repo', source.repo_name),
373 ('source_ref', 'branch:default:' + commit_ids['change2']),
372 ('source_ref', 'branch:default:' + commit_ids['change2']),
374 ('target_repo', target.repo_name),
373 ('target_repo', target.repo_name),
375 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
374 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
376 ('common_ancestor', commit_ids['ancestor']),
375 ('common_ancestor', commit_ids['ancestor']),
377 ('pullrequest_title', 'Title'),
376 ('pullrequest_title', 'Title'),
378 ('pullrequest_desc', 'Description'),
377 ('pullrequest_desc', 'Description'),
379 ('description_renderer', 'markdown'),
378 ('description_renderer', 'markdown'),
380 ('__start__', 'review_members:sequence'),
379 ('__start__', 'review_members:sequence'),
381 ('__start__', 'reviewer:mapping'),
380 ('__start__', 'reviewer:mapping'),
382 ('user_id', '1'),
381 ('user_id', '1'),
383 ('__start__', 'reasons:sequence'),
382 ('__start__', 'reasons:sequence'),
384 ('reason', 'Some reason'),
383 ('reason', 'Some reason'),
385 ('__end__', 'reasons:sequence'),
384 ('__end__', 'reasons:sequence'),
386 ('__start__', 'rules:sequence'),
385 ('__start__', 'rules:sequence'),
387 ('__end__', 'rules:sequence'),
386 ('__end__', 'rules:sequence'),
388 ('mandatory', 'False'),
387 ('mandatory', 'False'),
389 ('__end__', 'reviewer:mapping'),
388 ('__end__', 'reviewer:mapping'),
390 ('__end__', 'review_members:sequence'),
389 ('__end__', 'review_members:sequence'),
391 ('__start__', 'revisions:sequence'),
390 ('__start__', 'revisions:sequence'),
392 ('revisions', commit_ids['change']),
391 ('revisions', commit_ids['change']),
393 ('revisions', commit_ids['change2']),
392 ('revisions', commit_ids['change2']),
394 ('__end__', 'revisions:sequence'),
393 ('__end__', 'revisions:sequence'),
395 ('user', ''),
394 ('user', ''),
396 ('csrf_token', csrf_token),
395 ('csrf_token', csrf_token),
397 ],
396 ],
398 status=302)
397 status=302)
399
398
400 location = response.headers['Location']
399 location = response.headers['Location']
401 pull_request_id = location.rsplit('/', 1)[1]
400 pull_request_id = location.rsplit('/', 1)[1]
402 assert pull_request_id != 'new'
401 assert pull_request_id != 'new'
403 pull_request = PullRequest.get(int(pull_request_id))
402 pull_request = PullRequest.get(int(pull_request_id))
404
403
405 # check that we have now both revisions
404 # check that we have now both revisions
406 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
405 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
407 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
406 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
408 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
407 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
409 assert pull_request.target_ref == expected_target_ref
408 assert pull_request.target_ref == expected_target_ref
410
409
411 def test_reviewer_notifications(self, backend, csrf_token):
410 def test_reviewer_notifications(self, backend, csrf_token):
412 # We have to use the app.post for this test so it will create the
411 # We have to use the app.post for this test so it will create the
413 # notifications properly with the new PR
412 # notifications properly with the new PR
414 commits = [
413 commits = [
415 {'message': 'ancestor',
414 {'message': 'ancestor',
416 'added': [FileNode('file_A', content='content_of_ancestor')]},
415 'added': [FileNode('file_A', content='content_of_ancestor')]},
417 {'message': 'change',
416 {'message': 'change',
418 'added': [FileNode('file_a', content='content_of_change')]},
417 'added': [FileNode('file_a', content='content_of_change')]},
419 {'message': 'change-child'},
418 {'message': 'change-child'},
420 {'message': 'ancestor-child', 'parents': ['ancestor'],
419 {'message': 'ancestor-child', 'parents': ['ancestor'],
421 'added': [
420 'added': [
422 FileNode('file_B', content='content_of_ancestor_child')]},
421 FileNode('file_B', content='content_of_ancestor_child')]},
423 {'message': 'ancestor-child-2'},
422 {'message': 'ancestor-child-2'},
424 ]
423 ]
425 commit_ids = backend.create_master_repo(commits)
424 commit_ids = backend.create_master_repo(commits)
426 target = backend.create_repo(heads=['ancestor-child'])
425 target = backend.create_repo(heads=['ancestor-child'])
427 source = backend.create_repo(heads=['change'])
426 source = backend.create_repo(heads=['change'])
428
427
429 response = self.app.post(
428 response = self.app.post(
430 route_path('pullrequest_create', repo_name=source.repo_name),
429 route_path('pullrequest_create', repo_name=source.repo_name),
431 [
430 [
432 ('source_repo', source.repo_name),
431 ('source_repo', source.repo_name),
433 ('source_ref', 'branch:default:' + commit_ids['change']),
432 ('source_ref', 'branch:default:' + commit_ids['change']),
434 ('target_repo', target.repo_name),
433 ('target_repo', target.repo_name),
435 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
434 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
436 ('common_ancestor', commit_ids['ancestor']),
435 ('common_ancestor', commit_ids['ancestor']),
437 ('pullrequest_title', 'Title'),
436 ('pullrequest_title', 'Title'),
438 ('pullrequest_desc', 'Description'),
437 ('pullrequest_desc', 'Description'),
439 ('description_renderer', 'markdown'),
438 ('description_renderer', 'markdown'),
440 ('__start__', 'review_members:sequence'),
439 ('__start__', 'review_members:sequence'),
441 ('__start__', 'reviewer:mapping'),
440 ('__start__', 'reviewer:mapping'),
442 ('user_id', '2'),
441 ('user_id', '2'),
443 ('__start__', 'reasons:sequence'),
442 ('__start__', 'reasons:sequence'),
444 ('reason', 'Some reason'),
443 ('reason', 'Some reason'),
445 ('__end__', 'reasons:sequence'),
444 ('__end__', 'reasons:sequence'),
446 ('__start__', 'rules:sequence'),
445 ('__start__', 'rules:sequence'),
447 ('__end__', 'rules:sequence'),
446 ('__end__', 'rules:sequence'),
448 ('mandatory', 'False'),
447 ('mandatory', 'False'),
449 ('__end__', 'reviewer:mapping'),
448 ('__end__', 'reviewer:mapping'),
450 ('__end__', 'review_members:sequence'),
449 ('__end__', 'review_members:sequence'),
451 ('__start__', 'revisions:sequence'),
450 ('__start__', 'revisions:sequence'),
452 ('revisions', commit_ids['change']),
451 ('revisions', commit_ids['change']),
453 ('__end__', 'revisions:sequence'),
452 ('__end__', 'revisions:sequence'),
454 ('user', ''),
453 ('user', ''),
455 ('csrf_token', csrf_token),
454 ('csrf_token', csrf_token),
456 ],
455 ],
457 status=302)
456 status=302)
458
457
459 location = response.headers['Location']
458 location = response.headers['Location']
460
459
461 pull_request_id = location.rsplit('/', 1)[1]
460 pull_request_id = location.rsplit('/', 1)[1]
462 assert pull_request_id != 'new'
461 assert pull_request_id != 'new'
463 pull_request = PullRequest.get(int(pull_request_id))
462 pull_request = PullRequest.get(int(pull_request_id))
464
463
465 # Check that a notification was made
464 # Check that a notification was made
466 notifications = Notification.query()\
465 notifications = Notification.query()\
467 .filter(Notification.created_by == pull_request.author.user_id,
466 .filter(Notification.created_by == pull_request.author.user_id,
468 Notification.type_ == Notification.TYPE_PULL_REQUEST,
467 Notification.type_ == Notification.TYPE_PULL_REQUEST,
469 Notification.subject.contains(
468 Notification.subject.contains(
470 "requested a pull request review. !%s" % pull_request_id))
469 "requested a pull request review. !%s" % pull_request_id))
471 assert len(notifications.all()) == 1
470 assert len(notifications.all()) == 1
472
471
473 # Change reviewers and check that a notification was made
472 # Change reviewers and check that a notification was made
474 PullRequestModel().update_reviewers(
473 PullRequestModel().update_reviewers(
475 pull_request.pull_request_id, [(1, [], False, [])],
474 pull_request.pull_request_id, [(1, [], False, [])],
476 pull_request.author)
475 pull_request.author)
477 assert len(notifications.all()) == 2
476 assert len(notifications.all()) == 2
478
477
479 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
478 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
480 csrf_token):
479 csrf_token):
481 commits = [
480 commits = [
482 {'message': 'ancestor',
481 {'message': 'ancestor',
483 'added': [FileNode('file_A', content='content_of_ancestor')]},
482 'added': [FileNode('file_A', content='content_of_ancestor')]},
484 {'message': 'change',
483 {'message': 'change',
485 'added': [FileNode('file_a', content='content_of_change')]},
484 'added': [FileNode('file_a', content='content_of_change')]},
486 {'message': 'change-child'},
485 {'message': 'change-child'},
487 {'message': 'ancestor-child', 'parents': ['ancestor'],
486 {'message': 'ancestor-child', 'parents': ['ancestor'],
488 'added': [
487 'added': [
489 FileNode('file_B', content='content_of_ancestor_child')]},
488 FileNode('file_B', content='content_of_ancestor_child')]},
490 {'message': 'ancestor-child-2'},
489 {'message': 'ancestor-child-2'},
491 ]
490 ]
492 commit_ids = backend.create_master_repo(commits)
491 commit_ids = backend.create_master_repo(commits)
493 target = backend.create_repo(heads=['ancestor-child'])
492 target = backend.create_repo(heads=['ancestor-child'])
494 source = backend.create_repo(heads=['change'])
493 source = backend.create_repo(heads=['change'])
495
494
496 response = self.app.post(
495 response = self.app.post(
497 route_path('pullrequest_create', repo_name=source.repo_name),
496 route_path('pullrequest_create', repo_name=source.repo_name),
498 [
497 [
499 ('source_repo', source.repo_name),
498 ('source_repo', source.repo_name),
500 ('source_ref', 'branch:default:' + commit_ids['change']),
499 ('source_ref', 'branch:default:' + commit_ids['change']),
501 ('target_repo', target.repo_name),
500 ('target_repo', target.repo_name),
502 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
501 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
503 ('common_ancestor', commit_ids['ancestor']),
502 ('common_ancestor', commit_ids['ancestor']),
504 ('pullrequest_title', 'Title'),
503 ('pullrequest_title', 'Title'),
505 ('pullrequest_desc', 'Description'),
504 ('pullrequest_desc', 'Description'),
506 ('description_renderer', 'markdown'),
505 ('description_renderer', 'markdown'),
507 ('__start__', 'review_members:sequence'),
506 ('__start__', 'review_members:sequence'),
508 ('__start__', 'reviewer:mapping'),
507 ('__start__', 'reviewer:mapping'),
509 ('user_id', '1'),
508 ('user_id', '1'),
510 ('__start__', 'reasons:sequence'),
509 ('__start__', 'reasons:sequence'),
511 ('reason', 'Some reason'),
510 ('reason', 'Some reason'),
512 ('__end__', 'reasons:sequence'),
511 ('__end__', 'reasons:sequence'),
513 ('__start__', 'rules:sequence'),
512 ('__start__', 'rules:sequence'),
514 ('__end__', 'rules:sequence'),
513 ('__end__', 'rules:sequence'),
515 ('mandatory', 'False'),
514 ('mandatory', 'False'),
516 ('__end__', 'reviewer:mapping'),
515 ('__end__', 'reviewer:mapping'),
517 ('__end__', 'review_members:sequence'),
516 ('__end__', 'review_members:sequence'),
518 ('__start__', 'revisions:sequence'),
517 ('__start__', 'revisions:sequence'),
519 ('revisions', commit_ids['change']),
518 ('revisions', commit_ids['change']),
520 ('__end__', 'revisions:sequence'),
519 ('__end__', 'revisions:sequence'),
521 ('user', ''),
520 ('user', ''),
522 ('csrf_token', csrf_token),
521 ('csrf_token', csrf_token),
523 ],
522 ],
524 status=302)
523 status=302)
525
524
526 location = response.headers['Location']
525 location = response.headers['Location']
527
526
528 pull_request_id = location.rsplit('/', 1)[1]
527 pull_request_id = location.rsplit('/', 1)[1]
529 assert pull_request_id != 'new'
528 assert pull_request_id != 'new'
530 pull_request = PullRequest.get(int(pull_request_id))
529 pull_request = PullRequest.get(int(pull_request_id))
531
530
532 # target_ref has to point to the ancestor's commit_id in order to
531 # target_ref has to point to the ancestor's commit_id in order to
533 # show the correct diff
532 # show the correct diff
534 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
533 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
535 assert pull_request.target_ref == expected_target_ref
534 assert pull_request.target_ref == expected_target_ref
536
535
537 # Check generated diff contents
536 # Check generated diff contents
538 response = response.follow()
537 response = response.follow()
539 assert 'content_of_ancestor' not in response.body
538 response.mustcontain(no=['content_of_ancestor'])
540 assert 'content_of_ancestor-child' not in response.body
539 response.mustcontain(no=['content_of_ancestor-child'])
541 assert 'content_of_change' in response.body
540 response.mustcontain('content_of_change')
542
541
543 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
542 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
544 # Clear any previous calls to rcextensions
543 # Clear any previous calls to rcextensions
545 rhodecode.EXTENSIONS.calls.clear()
544 rhodecode.EXTENSIONS.calls.clear()
546
545
547 pull_request = pr_util.create_pull_request(
546 pull_request = pr_util.create_pull_request(
548 approved=True, mergeable=True)
547 approved=True, mergeable=True)
549 pull_request_id = pull_request.pull_request_id
548 pull_request_id = pull_request.pull_request_id
550 repo_name = pull_request.target_repo.scm_instance().name,
549 repo_name = pull_request.target_repo.scm_instance().name,
551
550
552 url = route_path('pullrequest_merge',
551 url = route_path('pullrequest_merge',
553 repo_name=str(repo_name[0]),
552 repo_name=str(repo_name[0]),
554 pull_request_id=pull_request_id)
553 pull_request_id=pull_request_id)
555 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
554 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
556
555
557 pull_request = PullRequest.get(pull_request_id)
556 pull_request = PullRequest.get(pull_request_id)
558
557
559 assert response.status_int == 200
558 assert response.status_int == 200
560 assert pull_request.is_closed()
559 assert pull_request.is_closed()
561 assert_pull_request_status(
560 assert_pull_request_status(
562 pull_request, ChangesetStatus.STATUS_APPROVED)
561 pull_request, ChangesetStatus.STATUS_APPROVED)
563
562
564 # Check the relevant log entries were added
563 # Check the relevant log entries were added
565 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
564 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
566 actions = [log.action for log in user_logs]
565 actions = [log.action for log in user_logs]
567 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
566 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
568 expected_actions = [
567 expected_actions = [
569 u'repo.pull_request.close',
568 u'repo.pull_request.close',
570 u'repo.pull_request.merge',
569 u'repo.pull_request.merge',
571 u'repo.pull_request.comment.create'
570 u'repo.pull_request.comment.create'
572 ]
571 ]
573 assert actions == expected_actions
572 assert actions == expected_actions
574
573
575 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
574 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
576 actions = [log for log in user_logs]
575 actions = [log for log in user_logs]
577 assert actions[-1].action == 'user.push'
576 assert actions[-1].action == 'user.push'
578 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
577 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
579
578
580 # Check post_push rcextension was really executed
579 # Check post_push rcextension was really executed
581 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
580 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
582 assert len(push_calls) == 1
581 assert len(push_calls) == 1
583 unused_last_call_args, last_call_kwargs = push_calls[0]
582 unused_last_call_args, last_call_kwargs = push_calls[0]
584 assert last_call_kwargs['action'] == 'push'
583 assert last_call_kwargs['action'] == 'push'
585 assert last_call_kwargs['commit_ids'] == pr_commit_ids
584 assert last_call_kwargs['commit_ids'] == pr_commit_ids
586
585
587 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
586 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
588 pull_request = pr_util.create_pull_request(mergeable=False)
587 pull_request = pr_util.create_pull_request(mergeable=False)
589 pull_request_id = pull_request.pull_request_id
588 pull_request_id = pull_request.pull_request_id
590 pull_request = PullRequest.get(pull_request_id)
589 pull_request = PullRequest.get(pull_request_id)
591
590
592 response = self.app.post(
591 response = self.app.post(
593 route_path('pullrequest_merge',
592 route_path('pullrequest_merge',
594 repo_name=pull_request.target_repo.scm_instance().name,
593 repo_name=pull_request.target_repo.scm_instance().name,
595 pull_request_id=pull_request.pull_request_id),
594 pull_request_id=pull_request.pull_request_id),
596 params={'csrf_token': csrf_token}).follow()
595 params={'csrf_token': csrf_token}).follow()
597
596
598 assert response.status_int == 200
597 assert response.status_int == 200
599 response.mustcontain(
598 response.mustcontain(
600 'Merge is not currently possible because of below failed checks.')
599 'Merge is not currently possible because of below failed checks.')
601 response.mustcontain('Server-side pull request merging is disabled.')
600 response.mustcontain('Server-side pull request merging is disabled.')
602
601
603 @pytest.mark.skip_backends('svn')
602 @pytest.mark.skip_backends('svn')
604 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
603 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
605 pull_request = pr_util.create_pull_request(mergeable=True)
604 pull_request = pr_util.create_pull_request(mergeable=True)
606 pull_request_id = pull_request.pull_request_id
605 pull_request_id = pull_request.pull_request_id
607 repo_name = pull_request.target_repo.scm_instance().name
606 repo_name = pull_request.target_repo.scm_instance().name
608
607
609 response = self.app.post(
608 response = self.app.post(
610 route_path('pullrequest_merge',
609 route_path('pullrequest_merge',
611 repo_name=repo_name, pull_request_id=pull_request_id),
610 repo_name=repo_name, pull_request_id=pull_request_id),
612 params={'csrf_token': csrf_token}).follow()
611 params={'csrf_token': csrf_token}).follow()
613
612
614 assert response.status_int == 200
613 assert response.status_int == 200
615
614
616 response.mustcontain(
615 response.mustcontain(
617 'Merge is not currently possible because of below failed checks.')
616 'Merge is not currently possible because of below failed checks.')
618 response.mustcontain('Pull request reviewer approval is pending.')
617 response.mustcontain('Pull request reviewer approval is pending.')
619
618
620 def test_merge_pull_request_renders_failure_reason(
619 def test_merge_pull_request_renders_failure_reason(
621 self, user_regular, csrf_token, pr_util):
620 self, user_regular, csrf_token, pr_util):
622 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
621 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
623 pull_request_id = pull_request.pull_request_id
622 pull_request_id = pull_request.pull_request_id
624 repo_name = pull_request.target_repo.scm_instance().name
623 repo_name = pull_request.target_repo.scm_instance().name
625
624
626 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
625 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
627 MergeFailureReason.PUSH_FAILED,
626 MergeFailureReason.PUSH_FAILED,
628 metadata={'target': 'shadow repo',
627 metadata={'target': 'shadow repo',
629 'merge_commit': 'xxx'})
628 'merge_commit': 'xxx'})
630 model_patcher = mock.patch.multiple(
629 model_patcher = mock.patch.multiple(
631 PullRequestModel,
630 PullRequestModel,
632 merge_repo=mock.Mock(return_value=merge_resp),
631 merge_repo=mock.Mock(return_value=merge_resp),
633 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
634
633
635 with model_patcher:
634 with model_patcher:
636 response = self.app.post(
635 response = self.app.post(
637 route_path('pullrequest_merge',
636 route_path('pullrequest_merge',
638 repo_name=repo_name,
637 repo_name=repo_name,
639 pull_request_id=pull_request_id),
638 pull_request_id=pull_request_id),
640 params={'csrf_token': csrf_token}, status=302)
639 params={'csrf_token': csrf_token}, status=302)
641
640
642 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
641 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
643 metadata={'target': 'shadow repo',
642 metadata={'target': 'shadow repo',
644 'merge_commit': 'xxx'})
643 'merge_commit': 'xxx'})
645 assert_session_flash(response, merge_resp.merge_status_message)
644 assert_session_flash(response, merge_resp.merge_status_message)
646
645
647 def test_update_source_revision(self, backend, csrf_token):
646 def test_update_source_revision(self, backend, csrf_token):
648 commits = [
647 commits = [
649 {'message': 'ancestor'},
648 {'message': 'ancestor'},
650 {'message': 'change'},
649 {'message': 'change'},
651 {'message': 'change-2'},
650 {'message': 'change-2'},
652 ]
651 ]
653 commit_ids = backend.create_master_repo(commits)
652 commit_ids = backend.create_master_repo(commits)
654 target = backend.create_repo(heads=['ancestor'])
653 target = backend.create_repo(heads=['ancestor'])
655 source = backend.create_repo(heads=['change'])
654 source = backend.create_repo(heads=['change'])
656
655
657 # create pr from a in source to A in target
656 # create pr from a in source to A in target
658 pull_request = PullRequest()
657 pull_request = PullRequest()
659
658
660 pull_request.source_repo = source
659 pull_request.source_repo = source
661 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
660 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
662 branch=backend.default_branch_name, commit_id=commit_ids['change'])
661 branch=backend.default_branch_name, commit_id=commit_ids['change'])
663
662
664 pull_request.target_repo = target
663 pull_request.target_repo = target
665 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
664 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
666 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
665 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
667
666
668 pull_request.revisions = [commit_ids['change']]
667 pull_request.revisions = [commit_ids['change']]
669 pull_request.title = u"Test"
668 pull_request.title = u"Test"
670 pull_request.description = u"Description"
669 pull_request.description = u"Description"
671 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
670 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
672 pull_request.pull_request_state = PullRequest.STATE_CREATED
671 pull_request.pull_request_state = PullRequest.STATE_CREATED
673 Session().add(pull_request)
672 Session().add(pull_request)
674 Session().commit()
673 Session().commit()
675 pull_request_id = pull_request.pull_request_id
674 pull_request_id = pull_request.pull_request_id
676
675
677 # source has ancestor - change - change-2
676 # source has ancestor - change - change-2
678 backend.pull_heads(source, heads=['change-2'])
677 backend.pull_heads(source, heads=['change-2'])
679
678
680 # update PR
679 # update PR
681 self.app.post(
680 self.app.post(
682 route_path('pullrequest_update',
681 route_path('pullrequest_update',
683 repo_name=target.repo_name, pull_request_id=pull_request_id),
682 repo_name=target.repo_name, pull_request_id=pull_request_id),
684 params={'update_commits': 'true', 'csrf_token': csrf_token})
683 params={'update_commits': 'true', 'csrf_token': csrf_token})
685
684
686 response = self.app.get(
685 response = self.app.get(
687 route_path('pullrequest_show',
686 route_path('pullrequest_show',
688 repo_name=target.repo_name,
687 repo_name=target.repo_name,
689 pull_request_id=pull_request.pull_request_id))
688 pull_request_id=pull_request.pull_request_id))
690
689
691 assert response.status_int == 200
690 assert response.status_int == 200
692 assert 'Pull request updated to' in response.body
691 response.mustcontain('Pull request updated to')
693 assert 'with 1 added, 0 removed commits.' in response.body
692 response.mustcontain('with 1 added, 0 removed commits.')
694
693
695 # check that we have now both revisions
694 # check that we have now both revisions
696 pull_request = PullRequest.get(pull_request_id)
695 pull_request = PullRequest.get(pull_request_id)
697 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
696 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
698
697
699 def test_update_target_revision(self, backend, csrf_token):
698 def test_update_target_revision(self, backend, csrf_token):
700 commits = [
699 commits = [
701 {'message': 'ancestor'},
700 {'message': 'ancestor'},
702 {'message': 'change'},
701 {'message': 'change'},
703 {'message': 'ancestor-new', 'parents': ['ancestor']},
702 {'message': 'ancestor-new', 'parents': ['ancestor']},
704 {'message': 'change-rebased'},
703 {'message': 'change-rebased'},
705 ]
704 ]
706 commit_ids = backend.create_master_repo(commits)
705 commit_ids = backend.create_master_repo(commits)
707 target = backend.create_repo(heads=['ancestor'])
706 target = backend.create_repo(heads=['ancestor'])
708 source = backend.create_repo(heads=['change'])
707 source = backend.create_repo(heads=['change'])
709
708
710 # create pr from a in source to A in target
709 # create pr from a in source to A in target
711 pull_request = PullRequest()
710 pull_request = PullRequest()
712
711
713 pull_request.source_repo = source
712 pull_request.source_repo = source
714 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
713 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
715 branch=backend.default_branch_name, commit_id=commit_ids['change'])
714 branch=backend.default_branch_name, commit_id=commit_ids['change'])
716
715
717 pull_request.target_repo = target
716 pull_request.target_repo = target
718 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
717 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
719 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
718 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
720
719
721 pull_request.revisions = [commit_ids['change']]
720 pull_request.revisions = [commit_ids['change']]
722 pull_request.title = u"Test"
721 pull_request.title = u"Test"
723 pull_request.description = u"Description"
722 pull_request.description = u"Description"
724 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
723 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
725 pull_request.pull_request_state = PullRequest.STATE_CREATED
724 pull_request.pull_request_state = PullRequest.STATE_CREATED
726
725
727 Session().add(pull_request)
726 Session().add(pull_request)
728 Session().commit()
727 Session().commit()
729 pull_request_id = pull_request.pull_request_id
728 pull_request_id = pull_request.pull_request_id
730
729
731 # target has ancestor - ancestor-new
730 # target has ancestor - ancestor-new
732 # source has ancestor - ancestor-new - change-rebased
731 # source has ancestor - ancestor-new - change-rebased
733 backend.pull_heads(target, heads=['ancestor-new'])
732 backend.pull_heads(target, heads=['ancestor-new'])
734 backend.pull_heads(source, heads=['change-rebased'])
733 backend.pull_heads(source, heads=['change-rebased'])
735
734
736 # update PR
735 # update PR
737 url = route_path('pullrequest_update',
736 url = route_path('pullrequest_update',
738 repo_name=target.repo_name,
737 repo_name=target.repo_name,
739 pull_request_id=pull_request_id)
738 pull_request_id=pull_request_id)
740 self.app.post(url,
739 self.app.post(url,
741 params={'update_commits': 'true', 'csrf_token': csrf_token},
740 params={'update_commits': 'true', 'csrf_token': csrf_token},
742 status=200)
741 status=200)
743
742
744 # check that we have now both revisions
743 # check that we have now both revisions
745 pull_request = PullRequest.get(pull_request_id)
744 pull_request = PullRequest.get(pull_request_id)
746 assert pull_request.revisions == [commit_ids['change-rebased']]
745 assert pull_request.revisions == [commit_ids['change-rebased']]
747 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
746 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
748 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
747 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
749
748
750 response = self.app.get(
749 response = self.app.get(
751 route_path('pullrequest_show',
750 route_path('pullrequest_show',
752 repo_name=target.repo_name,
751 repo_name=target.repo_name,
753 pull_request_id=pull_request.pull_request_id))
752 pull_request_id=pull_request.pull_request_id))
754 assert response.status_int == 200
753 assert response.status_int == 200
755 assert 'Pull request updated to' in response.body
754 response.mustcontain('Pull request updated to')
756 assert 'with 1 added, 1 removed commits.' in response.body
755 response.mustcontain('with 1 added, 1 removed commits.')
757
756
758 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
757 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
759 backend = backend_git
758 backend = backend_git
760 commits = [
759 commits = [
761 {'message': 'master-commit-1'},
760 {'message': 'master-commit-1'},
762 {'message': 'master-commit-2-change-1'},
761 {'message': 'master-commit-2-change-1'},
763 {'message': 'master-commit-3-change-2'},
762 {'message': 'master-commit-3-change-2'},
764
763
765 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
764 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
766 {'message': 'feat-commit-2'},
765 {'message': 'feat-commit-2'},
767 ]
766 ]
768 commit_ids = backend.create_master_repo(commits)
767 commit_ids = backend.create_master_repo(commits)
769 target = backend.create_repo(heads=['master-commit-3-change-2'])
768 target = backend.create_repo(heads=['master-commit-3-change-2'])
770 source = backend.create_repo(heads=['feat-commit-2'])
769 source = backend.create_repo(heads=['feat-commit-2'])
771
770
772 # create pr from a in source to A in target
771 # create pr from a in source to A in target
773 pull_request = PullRequest()
772 pull_request = PullRequest()
774 pull_request.source_repo = source
773 pull_request.source_repo = source
775
774
776 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
775 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
777 branch=backend.default_branch_name,
776 branch=backend.default_branch_name,
778 commit_id=commit_ids['master-commit-3-change-2'])
777 commit_id=commit_ids['master-commit-3-change-2'])
779
778
780 pull_request.target_repo = target
779 pull_request.target_repo = target
781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
780 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
782 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
781 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
783
782
784 pull_request.revisions = [
783 pull_request.revisions = [
785 commit_ids['feat-commit-1'],
784 commit_ids['feat-commit-1'],
786 commit_ids['feat-commit-2']
785 commit_ids['feat-commit-2']
787 ]
786 ]
788 pull_request.title = u"Test"
787 pull_request.title = u"Test"
789 pull_request.description = u"Description"
788 pull_request.description = u"Description"
790 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
789 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
791 pull_request.pull_request_state = PullRequest.STATE_CREATED
790 pull_request.pull_request_state = PullRequest.STATE_CREATED
792 Session().add(pull_request)
791 Session().add(pull_request)
793 Session().commit()
792 Session().commit()
794 pull_request_id = pull_request.pull_request_id
793 pull_request_id = pull_request.pull_request_id
795
794
796 # PR is created, now we simulate a force-push into target,
795 # PR is created, now we simulate a force-push into target,
797 # that drops a 2 last commits
796 # that drops a 2 last commits
798 vcsrepo = target.scm_instance()
797 vcsrepo = target.scm_instance()
799 vcsrepo.config.clear_section('hooks')
798 vcsrepo.config.clear_section('hooks')
800 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
799 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
801
800
802 # update PR
801 # update PR
803 url = route_path('pullrequest_update',
802 url = route_path('pullrequest_update',
804 repo_name=target.repo_name,
803 repo_name=target.repo_name,
805 pull_request_id=pull_request_id)
804 pull_request_id=pull_request_id)
806 self.app.post(url,
805 self.app.post(url,
807 params={'update_commits': 'true', 'csrf_token': csrf_token},
806 params={'update_commits': 'true', 'csrf_token': csrf_token},
808 status=200)
807 status=200)
809
808
810 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
809 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
811 assert response.status_int == 200
810 assert response.status_int == 200
812 response.mustcontain('Pull request updated to')
811 response.mustcontain('Pull request updated to')
813 response.mustcontain('with 0 added, 0 removed commits.')
812 response.mustcontain('with 0 added, 0 removed commits.')
814
813
815 def test_update_of_ancestor_reference(self, backend, csrf_token):
814 def test_update_of_ancestor_reference(self, backend, csrf_token):
816 commits = [
815 commits = [
817 {'message': 'ancestor'},
816 {'message': 'ancestor'},
818 {'message': 'change'},
817 {'message': 'change'},
819 {'message': 'change-2'},
818 {'message': 'change-2'},
820 {'message': 'ancestor-new', 'parents': ['ancestor']},
819 {'message': 'ancestor-new', 'parents': ['ancestor']},
821 {'message': 'change-rebased'},
820 {'message': 'change-rebased'},
822 ]
821 ]
823 commit_ids = backend.create_master_repo(commits)
822 commit_ids = backend.create_master_repo(commits)
824 target = backend.create_repo(heads=['ancestor'])
823 target = backend.create_repo(heads=['ancestor'])
825 source = backend.create_repo(heads=['change'])
824 source = backend.create_repo(heads=['change'])
826
825
827 # create pr from a in source to A in target
826 # create pr from a in source to A in target
828 pull_request = PullRequest()
827 pull_request = PullRequest()
829 pull_request.source_repo = source
828 pull_request.source_repo = source
830
829
831 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
830 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
832 branch=backend.default_branch_name, commit_id=commit_ids['change'])
831 branch=backend.default_branch_name, commit_id=commit_ids['change'])
833 pull_request.target_repo = target
832 pull_request.target_repo = target
834 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
833 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
835 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
834 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
836 pull_request.revisions = [commit_ids['change']]
835 pull_request.revisions = [commit_ids['change']]
837 pull_request.title = u"Test"
836 pull_request.title = u"Test"
838 pull_request.description = u"Description"
837 pull_request.description = u"Description"
839 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
838 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
840 pull_request.pull_request_state = PullRequest.STATE_CREATED
839 pull_request.pull_request_state = PullRequest.STATE_CREATED
841 Session().add(pull_request)
840 Session().add(pull_request)
842 Session().commit()
841 Session().commit()
843 pull_request_id = pull_request.pull_request_id
842 pull_request_id = pull_request.pull_request_id
844
843
845 # target has ancestor - ancestor-new
844 # target has ancestor - ancestor-new
846 # source has ancestor - ancestor-new - change-rebased
845 # source has ancestor - ancestor-new - change-rebased
847 backend.pull_heads(target, heads=['ancestor-new'])
846 backend.pull_heads(target, heads=['ancestor-new'])
848 backend.pull_heads(source, heads=['change-rebased'])
847 backend.pull_heads(source, heads=['change-rebased'])
849
848
850 # update PR
849 # update PR
851 self.app.post(
850 self.app.post(
852 route_path('pullrequest_update',
851 route_path('pullrequest_update',
853 repo_name=target.repo_name, pull_request_id=pull_request_id),
852 repo_name=target.repo_name, pull_request_id=pull_request_id),
854 params={'update_commits': 'true', 'csrf_token': csrf_token},
853 params={'update_commits': 'true', 'csrf_token': csrf_token},
855 status=200)
854 status=200)
856
855
857 # Expect the target reference to be updated correctly
856 # Expect the target reference to be updated correctly
858 pull_request = PullRequest.get(pull_request_id)
857 pull_request = PullRequest.get(pull_request_id)
859 assert pull_request.revisions == [commit_ids['change-rebased']]
858 assert pull_request.revisions == [commit_ids['change-rebased']]
860 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
859 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
861 branch=backend.default_branch_name,
860 branch=backend.default_branch_name,
862 commit_id=commit_ids['ancestor-new'])
861 commit_id=commit_ids['ancestor-new'])
863 assert pull_request.target_ref == expected_target_ref
862 assert pull_request.target_ref == expected_target_ref
864
863
865 def test_remove_pull_request_branch(self, backend_git, csrf_token):
864 def test_remove_pull_request_branch(self, backend_git, csrf_token):
866 branch_name = 'development'
865 branch_name = 'development'
867 commits = [
866 commits = [
868 {'message': 'initial-commit'},
867 {'message': 'initial-commit'},
869 {'message': 'old-feature'},
868 {'message': 'old-feature'},
870 {'message': 'new-feature', 'branch': branch_name},
869 {'message': 'new-feature', 'branch': branch_name},
871 ]
870 ]
872 repo = backend_git.create_repo(commits)
871 repo = backend_git.create_repo(commits)
873 repo_name = repo.repo_name
872 repo_name = repo.repo_name
874 commit_ids = backend_git.commit_ids
873 commit_ids = backend_git.commit_ids
875
874
876 pull_request = PullRequest()
875 pull_request = PullRequest()
877 pull_request.source_repo = repo
876 pull_request.source_repo = repo
878 pull_request.target_repo = repo
877 pull_request.target_repo = repo
879 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
878 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
880 branch=branch_name, commit_id=commit_ids['new-feature'])
879 branch=branch_name, commit_id=commit_ids['new-feature'])
881 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
880 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
882 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
881 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
883 pull_request.revisions = [commit_ids['new-feature']]
882 pull_request.revisions = [commit_ids['new-feature']]
884 pull_request.title = u"Test"
883 pull_request.title = u"Test"
885 pull_request.description = u"Description"
884 pull_request.description = u"Description"
886 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
885 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
887 pull_request.pull_request_state = PullRequest.STATE_CREATED
886 pull_request.pull_request_state = PullRequest.STATE_CREATED
888 Session().add(pull_request)
887 Session().add(pull_request)
889 Session().commit()
888 Session().commit()
890
889
891 pull_request_id = pull_request.pull_request_id
890 pull_request_id = pull_request.pull_request_id
892
891
893 vcs = repo.scm_instance()
892 vcs = repo.scm_instance()
894 vcs.remove_ref('refs/heads/{}'.format(branch_name))
893 vcs.remove_ref('refs/heads/{}'.format(branch_name))
895
894
896 response = self.app.get(route_path(
895 response = self.app.get(route_path(
897 'pullrequest_show',
896 'pullrequest_show',
898 repo_name=repo_name,
897 repo_name=repo_name,
899 pull_request_id=pull_request_id))
898 pull_request_id=pull_request_id))
900
899
901 assert response.status_int == 200
900 assert response.status_int == 200
902
901
903 response.assert_response().element_contains(
902 response.assert_response().element_contains(
904 '#changeset_compare_view_content .alert strong',
903 '#changeset_compare_view_content .alert strong',
905 'Missing commits')
904 'Missing commits')
906 response.assert_response().element_contains(
905 response.assert_response().element_contains(
907 '#changeset_compare_view_content .alert',
906 '#changeset_compare_view_content .alert',
908 'This pull request cannot be displayed, because one or more'
907 'This pull request cannot be displayed, because one or more'
909 ' commits no longer exist in the source repository.')
908 ' commits no longer exist in the source repository.')
910
909
911 def test_strip_commits_from_pull_request(
910 def test_strip_commits_from_pull_request(
912 self, backend, pr_util, csrf_token):
911 self, backend, pr_util, csrf_token):
913 commits = [
912 commits = [
914 {'message': 'initial-commit'},
913 {'message': 'initial-commit'},
915 {'message': 'old-feature'},
914 {'message': 'old-feature'},
916 {'message': 'new-feature', 'parents': ['initial-commit']},
915 {'message': 'new-feature', 'parents': ['initial-commit']},
917 ]
916 ]
918 pull_request = pr_util.create_pull_request(
917 pull_request = pr_util.create_pull_request(
919 commits, target_head='initial-commit', source_head='new-feature',
918 commits, target_head='initial-commit', source_head='new-feature',
920 revisions=['new-feature'])
919 revisions=['new-feature'])
921
920
922 vcs = pr_util.source_repository.scm_instance()
921 vcs = pr_util.source_repository.scm_instance()
923 if backend.alias == 'git':
922 if backend.alias == 'git':
924 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
923 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
925 else:
924 else:
926 vcs.strip(pr_util.commit_ids['new-feature'])
925 vcs.strip(pr_util.commit_ids['new-feature'])
927
926
928 response = self.app.get(route_path(
927 response = self.app.get(route_path(
929 'pullrequest_show',
928 'pullrequest_show',
930 repo_name=pr_util.target_repository.repo_name,
929 repo_name=pr_util.target_repository.repo_name,
931 pull_request_id=pull_request.pull_request_id))
930 pull_request_id=pull_request.pull_request_id))
932
931
933 assert response.status_int == 200
932 assert response.status_int == 200
934
933
935 response.assert_response().element_contains(
934 response.assert_response().element_contains(
936 '#changeset_compare_view_content .alert strong',
935 '#changeset_compare_view_content .alert strong',
937 'Missing commits')
936 'Missing commits')
938 response.assert_response().element_contains(
937 response.assert_response().element_contains(
939 '#changeset_compare_view_content .alert',
938 '#changeset_compare_view_content .alert',
940 'This pull request cannot be displayed, because one or more'
939 'This pull request cannot be displayed, because one or more'
941 ' commits no longer exist in the source repository.')
940 ' commits no longer exist in the source repository.')
942 response.assert_response().element_contains(
941 response.assert_response().element_contains(
943 '#update_commits',
942 '#update_commits',
944 'Update commits')
943 'Update commits')
945
944
946 def test_strip_commits_and_update(
945 def test_strip_commits_and_update(
947 self, backend, pr_util, csrf_token):
946 self, backend, pr_util, csrf_token):
948 commits = [
947 commits = [
949 {'message': 'initial-commit'},
948 {'message': 'initial-commit'},
950 {'message': 'old-feature'},
949 {'message': 'old-feature'},
951 {'message': 'new-feature', 'parents': ['old-feature']},
950 {'message': 'new-feature', 'parents': ['old-feature']},
952 ]
951 ]
953 pull_request = pr_util.create_pull_request(
952 pull_request = pr_util.create_pull_request(
954 commits, target_head='old-feature', source_head='new-feature',
953 commits, target_head='old-feature', source_head='new-feature',
955 revisions=['new-feature'], mergeable=True)
954 revisions=['new-feature'], mergeable=True)
956
955
957 vcs = pr_util.source_repository.scm_instance()
956 vcs = pr_util.source_repository.scm_instance()
958 if backend.alias == 'git':
957 if backend.alias == 'git':
959 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
958 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
960 else:
959 else:
961 vcs.strip(pr_util.commit_ids['new-feature'])
960 vcs.strip(pr_util.commit_ids['new-feature'])
962
961
963 url = route_path('pullrequest_update',
962 url = route_path('pullrequest_update',
964 repo_name=pull_request.target_repo.repo_name,
963 repo_name=pull_request.target_repo.repo_name,
965 pull_request_id=pull_request.pull_request_id)
964 pull_request_id=pull_request.pull_request_id)
966 response = self.app.post(url,
965 response = self.app.post(url,
967 params={'update_commits': 'true',
966 params={'update_commits': 'true',
968 'csrf_token': csrf_token})
967 'csrf_token': csrf_token})
969
968
970 assert response.status_int == 200
969 assert response.status_int == 200
971 assert response.body == '{"response": true, "redirect_url": null}'
970 assert response.body == '{"response": true, "redirect_url": null}'
972
971
973 # Make sure that after update, it won't raise 500 errors
972 # Make sure that after update, it won't raise 500 errors
974 response = self.app.get(route_path(
973 response = self.app.get(route_path(
975 'pullrequest_show',
974 'pullrequest_show',
976 repo_name=pr_util.target_repository.repo_name,
975 repo_name=pr_util.target_repository.repo_name,
977 pull_request_id=pull_request.pull_request_id))
976 pull_request_id=pull_request.pull_request_id))
978
977
979 assert response.status_int == 200
978 assert response.status_int == 200
980 response.assert_response().element_contains(
979 response.assert_response().element_contains(
981 '#changeset_compare_view_content .alert strong',
980 '#changeset_compare_view_content .alert strong',
982 'Missing commits')
981 'Missing commits')
983
982
984 def test_branch_is_a_link(self, pr_util):
983 def test_branch_is_a_link(self, pr_util):
985 pull_request = pr_util.create_pull_request()
984 pull_request = pr_util.create_pull_request()
986 pull_request.source_ref = 'branch:origin:1234567890abcdef'
985 pull_request.source_ref = 'branch:origin:1234567890abcdef'
987 pull_request.target_ref = 'branch:target:abcdef1234567890'
986 pull_request.target_ref = 'branch:target:abcdef1234567890'
988 Session().add(pull_request)
987 Session().add(pull_request)
989 Session().commit()
988 Session().commit()
990
989
991 response = self.app.get(route_path(
990 response = self.app.get(route_path(
992 'pullrequest_show',
991 'pullrequest_show',
993 repo_name=pull_request.target_repo.scm_instance().name,
992 repo_name=pull_request.target_repo.scm_instance().name,
994 pull_request_id=pull_request.pull_request_id))
993 pull_request_id=pull_request.pull_request_id))
995 assert response.status_int == 200
994 assert response.status_int == 200
996
995
997 origin = response.assert_response().get_element('.pr-origininfo .tag')
996 source = response.assert_response().get_element('.pr-source-info')
998 origin_children = origin.getchildren()
997 source_parent = source.getparent()
999 assert len(origin_children) == 1
998 assert len(source_parent) == 1
1000 target = response.assert_response().get_element('.pr-targetinfo .tag')
999
1001 target_children = target.getchildren()
1000 target = response.assert_response().get_element('.pr-target-info')
1002 assert len(target_children) == 1
1001 target_parent = target.getparent()
1002 assert len(target_parent) == 1
1003
1003
1004 expected_origin_link = route_path(
1004 expected_origin_link = route_path(
1005 'repo_commits',
1005 'repo_commits',
1006 repo_name=pull_request.source_repo.scm_instance().name,
1006 repo_name=pull_request.source_repo.scm_instance().name,
1007 params=dict(branch='origin'))
1007 params=dict(branch='origin'))
1008 expected_target_link = route_path(
1008 expected_target_link = route_path(
1009 'repo_commits',
1009 'repo_commits',
1010 repo_name=pull_request.target_repo.scm_instance().name,
1010 repo_name=pull_request.target_repo.scm_instance().name,
1011 params=dict(branch='target'))
1011 params=dict(branch='target'))
1012 assert origin_children[0].attrib['href'] == expected_origin_link
1012 assert source_parent.attrib['href'] == expected_origin_link
1013 assert origin_children[0].text == 'branch: origin'
1013 assert target_parent.attrib['href'] == expected_target_link
1014 assert target_children[0].attrib['href'] == expected_target_link
1015 assert target_children[0].text == 'branch: target'
1016
1014
1017 def test_bookmark_is_not_a_link(self, pr_util):
1015 def test_bookmark_is_not_a_link(self, pr_util):
1018 pull_request = pr_util.create_pull_request()
1016 pull_request = pr_util.create_pull_request()
1019 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1017 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1020 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1018 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1021 Session().add(pull_request)
1019 Session().add(pull_request)
1022 Session().commit()
1020 Session().commit()
1023
1021
1024 response = self.app.get(route_path(
1022 response = self.app.get(route_path(
1025 'pullrequest_show',
1023 'pullrequest_show',
1026 repo_name=pull_request.target_repo.scm_instance().name,
1024 repo_name=pull_request.target_repo.scm_instance().name,
1027 pull_request_id=pull_request.pull_request_id))
1025 pull_request_id=pull_request.pull_request_id))
1028 assert response.status_int == 200
1026 assert response.status_int == 200
1029
1027
1030 origin = response.assert_response().get_element('.pr-origininfo .tag')
1028 source = response.assert_response().get_element('.pr-source-info')
1031 assert origin.text.strip() == 'bookmark: origin'
1029 assert source.text.strip() == 'bookmark:origin'
1032 assert origin.getchildren() == []
1030 assert source.getparent().attrib.get('href') is None
1033
1031
1034 target = response.assert_response().get_element('.pr-targetinfo .tag')
1032 target = response.assert_response().get_element('.pr-target-info')
1035 assert target.text.strip() == 'bookmark: target'
1033 assert target.text.strip() == 'bookmark:target'
1036 assert target.getchildren() == []
1034 assert target.getparent().attrib.get('href') is None
1037
1035
1038 def test_tag_is_not_a_link(self, pr_util):
1036 def test_tag_is_not_a_link(self, pr_util):
1039 pull_request = pr_util.create_pull_request()
1037 pull_request = pr_util.create_pull_request()
1040 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1038 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1041 pull_request.target_ref = 'tag:target:abcdef1234567890'
1039 pull_request.target_ref = 'tag:target:abcdef1234567890'
1042 Session().add(pull_request)
1040 Session().add(pull_request)
1043 Session().commit()
1041 Session().commit()
1044
1042
1045 response = self.app.get(route_path(
1043 response = self.app.get(route_path(
1046 'pullrequest_show',
1044 'pullrequest_show',
1047 repo_name=pull_request.target_repo.scm_instance().name,
1045 repo_name=pull_request.target_repo.scm_instance().name,
1048 pull_request_id=pull_request.pull_request_id))
1046 pull_request_id=pull_request.pull_request_id))
1049 assert response.status_int == 200
1047 assert response.status_int == 200
1050
1048
1051 origin = response.assert_response().get_element('.pr-origininfo .tag')
1049 source = response.assert_response().get_element('.pr-source-info')
1052 assert origin.text.strip() == 'tag: origin'
1050 assert source.text.strip() == 'tag:origin'
1053 assert origin.getchildren() == []
1051 assert source.getparent().attrib.get('href') is None
1054
1052
1055 target = response.assert_response().get_element('.pr-targetinfo .tag')
1053 target = response.assert_response().get_element('.pr-target-info')
1056 assert target.text.strip() == 'tag: target'
1054 assert target.text.strip() == 'tag:target'
1057 assert target.getchildren() == []
1055 assert target.getparent().attrib.get('href') is None
1058
1056
1059 @pytest.mark.parametrize('mergeable', [True, False])
1057 @pytest.mark.parametrize('mergeable', [True, False])
1060 def test_shadow_repository_link(
1058 def test_shadow_repository_link(
1061 self, mergeable, pr_util, http_host_only_stub):
1059 self, mergeable, pr_util, http_host_only_stub):
1062 """
1060 """
1063 Check that the pull request summary page displays a link to the shadow
1061 Check that the pull request summary page displays a link to the shadow
1064 repository if the pull request is mergeable. If it is not mergeable
1062 repository if the pull request is mergeable. If it is not mergeable
1065 the link should not be displayed.
1063 the link should not be displayed.
1066 """
1064 """
1067 pull_request = pr_util.create_pull_request(
1065 pull_request = pr_util.create_pull_request(
1068 mergeable=mergeable, enable_notifications=False)
1066 mergeable=mergeable, enable_notifications=False)
1069 target_repo = pull_request.target_repo.scm_instance()
1067 target_repo = pull_request.target_repo.scm_instance()
1070 pr_id = pull_request.pull_request_id
1068 pr_id = pull_request.pull_request_id
1071 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1069 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1072 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1070 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1073
1071
1074 response = self.app.get(route_path(
1072 response = self.app.get(route_path(
1075 'pullrequest_show',
1073 'pullrequest_show',
1076 repo_name=target_repo.name,
1074 repo_name=target_repo.name,
1077 pull_request_id=pr_id))
1075 pull_request_id=pr_id))
1078
1076
1079 if mergeable:
1077 if mergeable:
1080 response.assert_response().element_value_contains(
1078 response.assert_response().element_value_contains(
1081 'input.pr-mergeinfo', shadow_url)
1079 'input.pr-mergeinfo', shadow_url)
1082 response.assert_response().element_value_contains(
1080 response.assert_response().element_value_contains(
1083 'input.pr-mergeinfo ', 'pr-merge')
1081 'input.pr-mergeinfo ', 'pr-merge')
1084 else:
1082 else:
1085 response.assert_response().no_element_exists('.pr-mergeinfo')
1083 response.assert_response().no_element_exists('.pr-mergeinfo')
1086
1084
1087
1085
1088 @pytest.mark.usefixtures('app')
1086 @pytest.mark.usefixtures('app')
1089 @pytest.mark.backends("git", "hg")
1087 @pytest.mark.backends("git", "hg")
1090 class TestPullrequestsControllerDelete(object):
1088 class TestPullrequestsControllerDelete(object):
1091 def test_pull_request_delete_button_permissions_admin(
1089 def test_pull_request_delete_button_permissions_admin(
1092 self, autologin_user, user_admin, pr_util):
1090 self, autologin_user, user_admin, pr_util):
1093 pull_request = pr_util.create_pull_request(
1091 pull_request = pr_util.create_pull_request(
1094 author=user_admin.username, enable_notifications=False)
1092 author=user_admin.username, enable_notifications=False)
1095
1093
1096 response = self.app.get(route_path(
1094 response = self.app.get(route_path(
1097 'pullrequest_show',
1095 'pullrequest_show',
1098 repo_name=pull_request.target_repo.scm_instance().name,
1096 repo_name=pull_request.target_repo.scm_instance().name,
1099 pull_request_id=pull_request.pull_request_id))
1097 pull_request_id=pull_request.pull_request_id))
1100
1098
1101 response.mustcontain('id="delete_pullrequest"')
1099 response.mustcontain('id="delete_pullrequest"')
1102 response.mustcontain('Confirm to delete this pull request')
1100 response.mustcontain('Confirm to delete this pull request')
1103
1101
1104 def test_pull_request_delete_button_permissions_owner(
1102 def test_pull_request_delete_button_permissions_owner(
1105 self, autologin_regular_user, user_regular, pr_util):
1103 self, autologin_regular_user, user_regular, pr_util):
1106 pull_request = pr_util.create_pull_request(
1104 pull_request = pr_util.create_pull_request(
1107 author=user_regular.username, enable_notifications=False)
1105 author=user_regular.username, enable_notifications=False)
1108
1106
1109 response = self.app.get(route_path(
1107 response = self.app.get(route_path(
1110 'pullrequest_show',
1108 'pullrequest_show',
1111 repo_name=pull_request.target_repo.scm_instance().name,
1109 repo_name=pull_request.target_repo.scm_instance().name,
1112 pull_request_id=pull_request.pull_request_id))
1110 pull_request_id=pull_request.pull_request_id))
1113
1111
1114 response.mustcontain('id="delete_pullrequest"')
1112 response.mustcontain('id="delete_pullrequest"')
1115 response.mustcontain('Confirm to delete this pull request')
1113 response.mustcontain('Confirm to delete this pull request')
1116
1114
1117 def test_pull_request_delete_button_permissions_forbidden(
1115 def test_pull_request_delete_button_permissions_forbidden(
1118 self, autologin_regular_user, user_regular, user_admin, pr_util):
1116 self, autologin_regular_user, user_regular, user_admin, pr_util):
1119 pull_request = pr_util.create_pull_request(
1117 pull_request = pr_util.create_pull_request(
1120 author=user_admin.username, enable_notifications=False)
1118 author=user_admin.username, enable_notifications=False)
1121
1119
1122 response = self.app.get(route_path(
1120 response = self.app.get(route_path(
1123 'pullrequest_show',
1121 'pullrequest_show',
1124 repo_name=pull_request.target_repo.scm_instance().name,
1122 repo_name=pull_request.target_repo.scm_instance().name,
1125 pull_request_id=pull_request.pull_request_id))
1123 pull_request_id=pull_request.pull_request_id))
1126 response.mustcontain(no=['id="delete_pullrequest"'])
1124 response.mustcontain(no=['id="delete_pullrequest"'])
1127 response.mustcontain(no=['Confirm to delete this pull request'])
1125 response.mustcontain(no=['Confirm to delete this pull request'])
1128
1126
1129 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1127 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1130 self, autologin_regular_user, user_regular, user_admin, pr_util,
1128 self, autologin_regular_user, user_regular, user_admin, pr_util,
1131 user_util):
1129 user_util):
1132
1130
1133 pull_request = pr_util.create_pull_request(
1131 pull_request = pr_util.create_pull_request(
1134 author=user_admin.username, enable_notifications=False)
1132 author=user_admin.username, enable_notifications=False)
1135
1133
1136 user_util.grant_user_permission_to_repo(
1134 user_util.grant_user_permission_to_repo(
1137 pull_request.target_repo, user_regular,
1135 pull_request.target_repo, user_regular,
1138 'repository.write')
1136 'repository.write')
1139
1137
1140 response = self.app.get(route_path(
1138 response = self.app.get(route_path(
1141 'pullrequest_show',
1139 'pullrequest_show',
1142 repo_name=pull_request.target_repo.scm_instance().name,
1140 repo_name=pull_request.target_repo.scm_instance().name,
1143 pull_request_id=pull_request.pull_request_id))
1141 pull_request_id=pull_request.pull_request_id))
1144
1142
1145 response.mustcontain('id="open_edit_pullrequest"')
1143 response.mustcontain('id="open_edit_pullrequest"')
1146 response.mustcontain('id="delete_pullrequest"')
1144 response.mustcontain('id="delete_pullrequest"')
1147 response.mustcontain(no=['Confirm to delete this pull request'])
1145 response.mustcontain(no=['Confirm to delete this pull request'])
1148
1146
1149 def test_delete_comment_returns_404_if_comment_does_not_exist(
1147 def test_delete_comment_returns_404_if_comment_does_not_exist(
1150 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1148 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1151
1149
1152 pull_request = pr_util.create_pull_request(
1150 pull_request = pr_util.create_pull_request(
1153 author=user_admin.username, enable_notifications=False)
1151 author=user_admin.username, enable_notifications=False)
1154
1152
1155 self.app.post(
1153 self.app.post(
1156 route_path(
1154 route_path(
1157 'pullrequest_comment_delete',
1155 'pullrequest_comment_delete',
1158 repo_name=pull_request.target_repo.scm_instance().name,
1156 repo_name=pull_request.target_repo.scm_instance().name,
1159 pull_request_id=pull_request.pull_request_id,
1157 pull_request_id=pull_request.pull_request_id,
1160 comment_id=1024404),
1158 comment_id=1024404),
1161 extra_environ=xhr_header,
1159 extra_environ=xhr_header,
1162 params={'csrf_token': csrf_token},
1160 params={'csrf_token': csrf_token},
1163 status=404
1161 status=404
1164 )
1162 )
1165
1163
1166 def test_delete_comment(
1164 def test_delete_comment(
1167 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1165 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1168
1166
1169 pull_request = pr_util.create_pull_request(
1167 pull_request = pr_util.create_pull_request(
1170 author=user_admin.username, enable_notifications=False)
1168 author=user_admin.username, enable_notifications=False)
1171 comment = pr_util.create_comment()
1169 comment = pr_util.create_comment()
1172 comment_id = comment.comment_id
1170 comment_id = comment.comment_id
1173
1171
1174 response = self.app.post(
1172 response = self.app.post(
1175 route_path(
1173 route_path(
1176 'pullrequest_comment_delete',
1174 'pullrequest_comment_delete',
1177 repo_name=pull_request.target_repo.scm_instance().name,
1175 repo_name=pull_request.target_repo.scm_instance().name,
1178 pull_request_id=pull_request.pull_request_id,
1176 pull_request_id=pull_request.pull_request_id,
1179 comment_id=comment_id),
1177 comment_id=comment_id),
1180 extra_environ=xhr_header,
1178 extra_environ=xhr_header,
1181 params={'csrf_token': csrf_token},
1179 params={'csrf_token': csrf_token},
1182 status=200
1180 status=200
1183 )
1181 )
1184 assert response.body == 'true'
1182 assert response.body == 'true'
1185
1183
1186 @pytest.mark.parametrize('url_type', [
1184 @pytest.mark.parametrize('url_type', [
1187 'pullrequest_new',
1185 'pullrequest_new',
1188 'pullrequest_create',
1186 'pullrequest_create',
1189 'pullrequest_update',
1187 'pullrequest_update',
1190 'pullrequest_merge',
1188 'pullrequest_merge',
1191 ])
1189 ])
1192 def test_pull_request_is_forbidden_on_archived_repo(
1190 def test_pull_request_is_forbidden_on_archived_repo(
1193 self, autologin_user, backend, xhr_header, user_util, url_type):
1191 self, autologin_user, backend, xhr_header, user_util, url_type):
1194
1192
1195 # create a temporary repo
1193 # create a temporary repo
1196 source = user_util.create_repo(repo_type=backend.alias)
1194 source = user_util.create_repo(repo_type=backend.alias)
1197 repo_name = source.repo_name
1195 repo_name = source.repo_name
1198 repo = Repository.get_by_repo_name(repo_name)
1196 repo = Repository.get_by_repo_name(repo_name)
1199 repo.archived = True
1197 repo.archived = True
1200 Session().commit()
1198 Session().commit()
1201
1199
1202 response = self.app.get(
1200 response = self.app.get(
1203 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1201 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1204
1202
1205 msg = 'Action not supported for archived repository.'
1203 msg = 'Action not supported for archived repository.'
1206 assert_session_flash(response, msg)
1204 assert_session_flash(response, msg)
1207
1205
1208
1206
1209 def assert_pull_request_status(pull_request, expected_status):
1207 def assert_pull_request_status(pull_request, expected_status):
1210 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1208 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1211 assert status == expected_status
1209 assert status == expected_status
1212
1210
1213
1211
1214 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1212 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1215 @pytest.mark.usefixtures("autologin_user")
1213 @pytest.mark.usefixtures("autologin_user")
1216 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1214 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1217 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1215 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,1943 +1,1945 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import os
28 import os
29 import random
29 import random
30 import hashlib
30 import hashlib
31 import StringIO
31 import StringIO
32 import textwrap
32 import textwrap
33 import urllib
33 import urllib
34 import math
34 import math
35 import logging
35 import logging
36 import re
36 import re
37 import time
37 import time
38 import string
38 import string
39 import hashlib
39 import hashlib
40 from collections import OrderedDict
40 from collections import OrderedDict
41
41
42 import pygments
42 import pygments
43 import itertools
43 import itertools
44 import fnmatch
44 import fnmatch
45 import bleach
45 import bleach
46
46
47 from pyramid import compat
47 from pyramid import compat
48 from datetime import datetime
48 from datetime import datetime
49 from functools import partial
49 from functools import partial
50 from pygments.formatters.html import HtmlFormatter
50 from pygments.formatters.html import HtmlFormatter
51 from pygments.lexers import (
51 from pygments.lexers import (
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53
53
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55
55
56 from webhelpers2.html import literal, HTML, escape
56 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html._autolink import _auto_link_urls
57 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html.tools import (
58 from webhelpers2.html.tools import (
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60
60
61 from webhelpers2.text import (
61 from webhelpers2.text import (
62 chop_at, collapse, convert_accented_entities,
62 chop_at, collapse, convert_accented_entities,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 from webhelpers2.date import time_ago_in_words
65 from webhelpers2.date import time_ago_in_words
66
66
67 from webhelpers2.html.tags import (
67 from webhelpers2.html.tags import (
68 _input, NotGiven, _make_safe_id_component as safeid,
68 _input, NotGiven, _make_safe_id_component as safeid,
69 form as insecure_form,
69 form as insecure_form,
70 auto_discovery_link, checkbox, end_form, file,
70 auto_discovery_link, checkbox, end_form, file,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 ul, radio, Options)
73 ul, radio, Options)
74
74
75 from webhelpers2.number import format_byte_size
75 from webhelpers2.number import format_byte_size
76
76
77 from rhodecode.lib.action_parser import action_parser
77 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.ext_json import json
79 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils2 import (
81 from rhodecode.lib.utils2 import (
82 str2bool, safe_unicode, safe_str,
82 str2bool, safe_unicode, safe_str,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 from rhodecode.model.changeset_status import ChangesetStatusModel
90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
91 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.repo_group import RepoGroupModel
92 from rhodecode.model.repo_group import RepoGroupModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
94
94
95
95
96 log = logging.getLogger(__name__)
96 log = logging.getLogger(__name__)
97
97
98
98
99 DEFAULT_USER = User.DEFAULT_USER
99 DEFAULT_USER = User.DEFAULT_USER
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101
101
102
102
103 def asset(path, ver=None, **kwargs):
103 def asset(path, ver=None, **kwargs):
104 """
104 """
105 Helper to generate a static asset file path for rhodecode assets
105 Helper to generate a static asset file path for rhodecode assets
106
106
107 eg. h.asset('images/image.png', ver='3923')
107 eg. h.asset('images/image.png', ver='3923')
108
108
109 :param path: path of asset
109 :param path: path of asset
110 :param ver: optional version query param to append as ?ver=
110 :param ver: optional version query param to append as ?ver=
111 """
111 """
112 request = get_current_request()
112 request = get_current_request()
113 query = {}
113 query = {}
114 query.update(kwargs)
114 query.update(kwargs)
115 if ver:
115 if ver:
116 query = {'ver': ver}
116 query = {'ver': ver}
117 return request.static_path(
117 return request.static_path(
118 'rhodecode:public/{}'.format(path), _query=query)
118 'rhodecode:public/{}'.format(path), _query=query)
119
119
120
120
121 default_html_escape_table = {
121 default_html_escape_table = {
122 ord('&'): u'&amp;',
122 ord('&'): u'&amp;',
123 ord('<'): u'&lt;',
123 ord('<'): u'&lt;',
124 ord('>'): u'&gt;',
124 ord('>'): u'&gt;',
125 ord('"'): u'&quot;',
125 ord('"'): u'&quot;',
126 ord("'"): u'&#39;',
126 ord("'"): u'&#39;',
127 }
127 }
128
128
129
129
130 def html_escape(text, html_escape_table=default_html_escape_table):
130 def html_escape(text, html_escape_table=default_html_escape_table):
131 """Produce entities within text."""
131 """Produce entities within text."""
132 return text.translate(html_escape_table)
132 return text.translate(html_escape_table)
133
133
134
134
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 """
136 """
137 Truncate string ``s`` at the first occurrence of ``sub``.
137 Truncate string ``s`` at the first occurrence of ``sub``.
138
138
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 """
140 """
141 suffix_if_chopped = suffix_if_chopped or ''
141 suffix_if_chopped = suffix_if_chopped or ''
142 pos = s.find(sub)
142 pos = s.find(sub)
143 if pos == -1:
143 if pos == -1:
144 return s
144 return s
145
145
146 if inclusive:
146 if inclusive:
147 pos += len(sub)
147 pos += len(sub)
148
148
149 chopped = s[:pos]
149 chopped = s[:pos]
150 left = s[pos:].strip()
150 left = s[pos:].strip()
151
151
152 if left and suffix_if_chopped:
152 if left and suffix_if_chopped:
153 chopped += suffix_if_chopped
153 chopped += suffix_if_chopped
154
154
155 return chopped
155 return chopped
156
156
157
157
158 def shorter(text, size=20, prefix=False):
158 def shorter(text, size=20, prefix=False):
159 postfix = '...'
159 postfix = '...'
160 if len(text) > size:
160 if len(text) > size:
161 if prefix:
161 if prefix:
162 # shorten in front
162 # shorten in front
163 return postfix + text[-(size - len(postfix)):]
163 return postfix + text[-(size - len(postfix)):]
164 else:
164 else:
165 return text[:size - len(postfix)] + postfix
165 return text[:size - len(postfix)] + postfix
166 return text
166 return text
167
167
168
168
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 """
170 """
171 Reset button
171 Reset button
172 """
172 """
173 return _input(type, name, value, id, attrs)
173 return _input(type, name, value, id, attrs)
174
174
175
175
176 def select(name, selected_values, options, id=NotGiven, **attrs):
176 def select(name, selected_values, options, id=NotGiven, **attrs):
177
177
178 if isinstance(options, (list, tuple)):
178 if isinstance(options, (list, tuple)):
179 options_iter = options
179 options_iter = options
180 # Handle old value,label lists ... where value also can be value,label lists
180 # Handle old value,label lists ... where value also can be value,label lists
181 options = Options()
181 options = Options()
182 for opt in options_iter:
182 for opt in options_iter:
183 if isinstance(opt, tuple) and len(opt) == 2:
183 if isinstance(opt, tuple) and len(opt) == 2:
184 value, label = opt
184 value, label = opt
185 elif isinstance(opt, basestring):
185 elif isinstance(opt, basestring):
186 value = label = opt
186 value = label = opt
187 else:
187 else:
188 raise ValueError('invalid select option type %r' % type(opt))
188 raise ValueError('invalid select option type %r' % type(opt))
189
189
190 if isinstance(value, (list, tuple)):
190 if isinstance(value, (list, tuple)):
191 option_group = options.add_optgroup(label)
191 option_group = options.add_optgroup(label)
192 for opt2 in value:
192 for opt2 in value:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 group_value, group_label = opt2
194 group_value, group_label = opt2
195 elif isinstance(opt2, basestring):
195 elif isinstance(opt2, basestring):
196 group_value = group_label = opt2
196 group_value = group_label = opt2
197 else:
197 else:
198 raise ValueError('invalid select option type %r' % type(opt2))
198 raise ValueError('invalid select option type %r' % type(opt2))
199
199
200 option_group.add_option(group_label, group_value)
200 option_group.add_option(group_label, group_value)
201 else:
201 else:
202 options.add_option(label, value)
202 options.add_option(label, value)
203
203
204 return raw_select(name, selected_values, options, id=id, **attrs)
204 return raw_select(name, selected_values, options, id=id, **attrs)
205
205
206
206
207 def branding(name, length=40):
207 def branding(name, length=40):
208 return truncate(name, length, indicator="")
208 return truncate(name, length, indicator="")
209
209
210
210
211 def FID(raw_id, path):
211 def FID(raw_id, path):
212 """
212 """
213 Creates a unique ID for filenode based on it's hash of path and commit
213 Creates a unique ID for filenode based on it's hash of path and commit
214 it's safe to use in urls
214 it's safe to use in urls
215
215
216 :param raw_id:
216 :param raw_id:
217 :param path:
217 :param path:
218 """
218 """
219
219
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221
221
222
222
223 class _GetError(object):
223 class _GetError(object):
224 """Get error from form_errors, and represent it as span wrapped error
224 """Get error from form_errors, and represent it as span wrapped error
225 message
225 message
226
226
227 :param field_name: field to fetch errors for
227 :param field_name: field to fetch errors for
228 :param form_errors: form errors dict
228 :param form_errors: form errors dict
229 """
229 """
230
230
231 def __call__(self, field_name, form_errors):
231 def __call__(self, field_name, form_errors):
232 tmpl = """<span class="error_msg">%s</span>"""
232 tmpl = """<span class="error_msg">%s</span>"""
233 if form_errors and field_name in form_errors:
233 if form_errors and field_name in form_errors:
234 return literal(tmpl % form_errors.get(field_name))
234 return literal(tmpl % form_errors.get(field_name))
235
235
236
236
237 get_error = _GetError()
237 get_error = _GetError()
238
238
239
239
240 class _ToolTip(object):
240 class _ToolTip(object):
241
241
242 def __call__(self, tooltip_title, trim_at=50):
242 def __call__(self, tooltip_title, trim_at=50):
243 """
243 """
244 Special function just to wrap our text into nice formatted
244 Special function just to wrap our text into nice formatted
245 autowrapped text
245 autowrapped text
246
246
247 :param tooltip_title:
247 :param tooltip_title:
248 """
248 """
249 tooltip_title = escape(tooltip_title)
249 tooltip_title = escape(tooltip_title)
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 return tooltip_title
251 return tooltip_title
252
252
253
253
254 tooltip = _ToolTip()
254 tooltip = _ToolTip()
255
255
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
257
257
258
258
259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
260 if isinstance(file_path, str):
260 if isinstance(file_path, str):
261 file_path = safe_unicode(file_path)
261 file_path = safe_unicode(file_path)
262
262
263 route_qry = {'at': at_ref} if at_ref else None
263 route_qry = {'at': at_ref} if at_ref else None
264
264
265 # first segment is a `..` link to repo files
265 # first segment is a `..` link to repo files
266 root_name = literal(u'<i class="icon-home"></i>')
266 root_name = literal(u'<i class="icon-home"></i>')
267 url_segments = [
267 url_segments = [
268 link_to(
268 link_to(
269 root_name,
269 root_name,
270 route_path(
270 route_path(
271 'repo_files',
271 'repo_files',
272 repo_name=repo_name,
272 repo_name=repo_name,
273 commit_id=commit_id,
273 commit_id=commit_id,
274 f_path='',
274 f_path='',
275 _query=route_qry),
275 _query=route_qry),
276 )]
276 )]
277
277
278 path_segments = file_path.split('/')
278 path_segments = file_path.split('/')
279 last_cnt = len(path_segments) - 1
279 last_cnt = len(path_segments) - 1
280 for cnt, segment in enumerate(path_segments):
280 for cnt, segment in enumerate(path_segments):
281 if not segment:
281 if not segment:
282 continue
282 continue
283 segment_html = escape(segment)
283 segment_html = escape(segment)
284
284
285 last_item = cnt == last_cnt
285 last_item = cnt == last_cnt
286
286
287 if last_item and linkify_last_item is False:
287 if last_item and linkify_last_item is False:
288 # plain version
288 # plain version
289 url_segments.append(segment_html)
289 url_segments.append(segment_html)
290 else:
290 else:
291 url_segments.append(
291 url_segments.append(
292 link_to(
292 link_to(
293 segment_html,
293 segment_html,
294 route_path(
294 route_path(
295 'repo_files',
295 'repo_files',
296 repo_name=repo_name,
296 repo_name=repo_name,
297 commit_id=commit_id,
297 commit_id=commit_id,
298 f_path='/'.join(path_segments[:cnt + 1]),
298 f_path='/'.join(path_segments[:cnt + 1]),
299 _query=route_qry),
299 _query=route_qry),
300 ))
300 ))
301
301
302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
303 if limit_items and len(limited_url_segments) < len(url_segments):
303 if limit_items and len(limited_url_segments) < len(url_segments):
304 url_segments = limited_url_segments
304 url_segments = limited_url_segments
305
305
306 full_path = file_path
306 full_path = file_path
307 icon = files_icon.format(escape(full_path))
307 icon = files_icon.format(escape(full_path))
308 if file_path == '':
308 if file_path == '':
309 return root_name
309 return root_name
310 else:
310 else:
311 return literal(' / '.join(url_segments) + icon)
311 return literal(' / '.join(url_segments) + icon)
312
312
313
313
314 def files_url_data(request):
314 def files_url_data(request):
315 matchdict = request.matchdict
315 matchdict = request.matchdict
316
316
317 if 'f_path' not in matchdict:
317 if 'f_path' not in matchdict:
318 matchdict['f_path'] = ''
318 matchdict['f_path'] = ''
319
319
320 if 'commit_id' not in matchdict:
320 if 'commit_id' not in matchdict:
321 matchdict['commit_id'] = 'tip'
321 matchdict['commit_id'] = 'tip'
322
322
323 return json.dumps(matchdict)
323 return json.dumps(matchdict)
324
324
325
325
326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
327 """
327 """
328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
329
329
330 If ``outfile`` is given and a valid file object (an object
330 If ``outfile`` is given and a valid file object (an object
331 with a ``write`` method), the result will be written to it, otherwise
331 with a ``write`` method), the result will be written to it, otherwise
332 it is returned as a string.
332 it is returned as a string.
333 """
333 """
334 if use_hl_filter:
334 if use_hl_filter:
335 # add HL filter
335 # add HL filter
336 from rhodecode.lib.index import search_utils
336 from rhodecode.lib.index import search_utils
337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
338 return pygments.format(pygments.lex(code, lexer), formatter)
338 return pygments.format(pygments.lex(code, lexer), formatter)
339
339
340
340
341 class CodeHtmlFormatter(HtmlFormatter):
341 class CodeHtmlFormatter(HtmlFormatter):
342 """
342 """
343 My code Html Formatter for source codes
343 My code Html Formatter for source codes
344 """
344 """
345
345
346 def wrap(self, source, outfile):
346 def wrap(self, source, outfile):
347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
348
348
349 def _wrap_code(self, source):
349 def _wrap_code(self, source):
350 for cnt, it in enumerate(source):
350 for cnt, it in enumerate(source):
351 i, t = it
351 i, t = it
352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
353 yield i, t
353 yield i, t
354
354
355 def _wrap_tablelinenos(self, inner):
355 def _wrap_tablelinenos(self, inner):
356 dummyoutfile = StringIO.StringIO()
356 dummyoutfile = StringIO.StringIO()
357 lncount = 0
357 lncount = 0
358 for t, line in inner:
358 for t, line in inner:
359 if t:
359 if t:
360 lncount += 1
360 lncount += 1
361 dummyoutfile.write(line)
361 dummyoutfile.write(line)
362
362
363 fl = self.linenostart
363 fl = self.linenostart
364 mw = len(str(lncount + fl - 1))
364 mw = len(str(lncount + fl - 1))
365 sp = self.linenospecial
365 sp = self.linenospecial
366 st = self.linenostep
366 st = self.linenostep
367 la = self.lineanchors
367 la = self.lineanchors
368 aln = self.anchorlinenos
368 aln = self.anchorlinenos
369 nocls = self.noclasses
369 nocls = self.noclasses
370 if sp:
370 if sp:
371 lines = []
371 lines = []
372
372
373 for i in range(fl, fl + lncount):
373 for i in range(fl, fl + lncount):
374 if i % st == 0:
374 if i % st == 0:
375 if i % sp == 0:
375 if i % sp == 0:
376 if aln:
376 if aln:
377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
378 (la, i, mw, i))
378 (la, i, mw, i))
379 else:
379 else:
380 lines.append('<span class="special">%*d</span>' % (mw, i))
380 lines.append('<span class="special">%*d</span>' % (mw, i))
381 else:
381 else:
382 if aln:
382 if aln:
383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
384 else:
384 else:
385 lines.append('%*d' % (mw, i))
385 lines.append('%*d' % (mw, i))
386 else:
386 else:
387 lines.append('')
387 lines.append('')
388 ls = '\n'.join(lines)
388 ls = '\n'.join(lines)
389 else:
389 else:
390 lines = []
390 lines = []
391 for i in range(fl, fl + lncount):
391 for i in range(fl, fl + lncount):
392 if i % st == 0:
392 if i % st == 0:
393 if aln:
393 if aln:
394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
395 else:
395 else:
396 lines.append('%*d' % (mw, i))
396 lines.append('%*d' % (mw, i))
397 else:
397 else:
398 lines.append('')
398 lines.append('')
399 ls = '\n'.join(lines)
399 ls = '\n'.join(lines)
400
400
401 # in case you wonder about the seemingly redundant <div> here: since the
401 # in case you wonder about the seemingly redundant <div> here: since the
402 # content in the other cell also is wrapped in a div, some browsers in
402 # content in the other cell also is wrapped in a div, some browsers in
403 # some configurations seem to mess up the formatting...
403 # some configurations seem to mess up the formatting...
404 if nocls:
404 if nocls:
405 yield 0, ('<table class="%stable">' % self.cssclass +
405 yield 0, ('<table class="%stable">' % self.cssclass +
406 '<tr><td><div class="linenodiv" '
406 '<tr><td><div class="linenodiv" '
407 'style="background-color: #f0f0f0; padding-right: 10px">'
407 'style="background-color: #f0f0f0; padding-right: 10px">'
408 '<pre style="line-height: 125%">' +
408 '<pre style="line-height: 125%">' +
409 ls + '</pre></div></td><td id="hlcode" class="code">')
409 ls + '</pre></div></td><td id="hlcode" class="code">')
410 else:
410 else:
411 yield 0, ('<table class="%stable">' % self.cssclass +
411 yield 0, ('<table class="%stable">' % self.cssclass +
412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
413 ls + '</pre></div></td><td id="hlcode" class="code">')
413 ls + '</pre></div></td><td id="hlcode" class="code">')
414 yield 0, dummyoutfile.getvalue()
414 yield 0, dummyoutfile.getvalue()
415 yield 0, '</td></tr></table>'
415 yield 0, '</td></tr></table>'
416
416
417
417
418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
419 def __init__(self, **kw):
419 def __init__(self, **kw):
420 # only show these line numbers if set
420 # only show these line numbers if set
421 self.only_lines = kw.pop('only_line_numbers', [])
421 self.only_lines = kw.pop('only_line_numbers', [])
422 self.query_terms = kw.pop('query_terms', [])
422 self.query_terms = kw.pop('query_terms', [])
423 self.max_lines = kw.pop('max_lines', 5)
423 self.max_lines = kw.pop('max_lines', 5)
424 self.line_context = kw.pop('line_context', 3)
424 self.line_context = kw.pop('line_context', 3)
425 self.url = kw.pop('url', None)
425 self.url = kw.pop('url', None)
426
426
427 super(CodeHtmlFormatter, self).__init__(**kw)
427 super(CodeHtmlFormatter, self).__init__(**kw)
428
428
429 def _wrap_code(self, source):
429 def _wrap_code(self, source):
430 for cnt, it in enumerate(source):
430 for cnt, it in enumerate(source):
431 i, t = it
431 i, t = it
432 t = '<pre>%s</pre>' % t
432 t = '<pre>%s</pre>' % t
433 yield i, t
433 yield i, t
434
434
435 def _wrap_tablelinenos(self, inner):
435 def _wrap_tablelinenos(self, inner):
436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
437
437
438 last_shown_line_number = 0
438 last_shown_line_number = 0
439 current_line_number = 1
439 current_line_number = 1
440
440
441 for t, line in inner:
441 for t, line in inner:
442 if not t:
442 if not t:
443 yield t, line
443 yield t, line
444 continue
444 continue
445
445
446 if current_line_number in self.only_lines:
446 if current_line_number in self.only_lines:
447 if last_shown_line_number + 1 != current_line_number:
447 if last_shown_line_number + 1 != current_line_number:
448 yield 0, '<tr>'
448 yield 0, '<tr>'
449 yield 0, '<td class="line">...</td>'
449 yield 0, '<td class="line">...</td>'
450 yield 0, '<td id="hlcode" class="code"></td>'
450 yield 0, '<td id="hlcode" class="code"></td>'
451 yield 0, '</tr>'
451 yield 0, '</tr>'
452
452
453 yield 0, '<tr>'
453 yield 0, '<tr>'
454 if self.url:
454 if self.url:
455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
456 self.url, current_line_number, current_line_number)
456 self.url, current_line_number, current_line_number)
457 else:
457 else:
458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
459 current_line_number)
459 current_line_number)
460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
461 yield 0, '</tr>'
461 yield 0, '</tr>'
462
462
463 last_shown_line_number = current_line_number
463 last_shown_line_number = current_line_number
464
464
465 current_line_number += 1
465 current_line_number += 1
466
466
467 yield 0, '</table>'
467 yield 0, '</table>'
468
468
469
469
470 def hsv_to_rgb(h, s, v):
470 def hsv_to_rgb(h, s, v):
471 """ Convert hsv color values to rgb """
471 """ Convert hsv color values to rgb """
472
472
473 if s == 0.0:
473 if s == 0.0:
474 return v, v, v
474 return v, v, v
475 i = int(h * 6.0) # XXX assume int() truncates!
475 i = int(h * 6.0) # XXX assume int() truncates!
476 f = (h * 6.0) - i
476 f = (h * 6.0) - i
477 p = v * (1.0 - s)
477 p = v * (1.0 - s)
478 q = v * (1.0 - s * f)
478 q = v * (1.0 - s * f)
479 t = v * (1.0 - s * (1.0 - f))
479 t = v * (1.0 - s * (1.0 - f))
480 i = i % 6
480 i = i % 6
481 if i == 0:
481 if i == 0:
482 return v, t, p
482 return v, t, p
483 if i == 1:
483 if i == 1:
484 return q, v, p
484 return q, v, p
485 if i == 2:
485 if i == 2:
486 return p, v, t
486 return p, v, t
487 if i == 3:
487 if i == 3:
488 return p, q, v
488 return p, q, v
489 if i == 4:
489 if i == 4:
490 return t, p, v
490 return t, p, v
491 if i == 5:
491 if i == 5:
492 return v, p, q
492 return v, p, q
493
493
494
494
495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
496 """
496 """
497 Generator for getting n of evenly distributed colors using
497 Generator for getting n of evenly distributed colors using
498 hsv color and golden ratio. It always return same order of colors
498 hsv color and golden ratio. It always return same order of colors
499
499
500 :param n: number of colors to generate
500 :param n: number of colors to generate
501 :param saturation: saturation of returned colors
501 :param saturation: saturation of returned colors
502 :param lightness: lightness of returned colors
502 :param lightness: lightness of returned colors
503 :returns: RGB tuple
503 :returns: RGB tuple
504 """
504 """
505
505
506 golden_ratio = 0.618033988749895
506 golden_ratio = 0.618033988749895
507 h = 0.22717784590367374
507 h = 0.22717784590367374
508
508
509 for _ in xrange(n):
509 for _ in xrange(n):
510 h += golden_ratio
510 h += golden_ratio
511 h %= 1
511 h %= 1
512 HSV_tuple = [h, saturation, lightness]
512 HSV_tuple = [h, saturation, lightness]
513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
515
515
516
516
517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
518 """
518 """
519 Returns a function which when called with an argument returns a unique
519 Returns a function which when called with an argument returns a unique
520 color for that argument, eg.
520 color for that argument, eg.
521
521
522 :param n: number of colors to generate
522 :param n: number of colors to generate
523 :param saturation: saturation of returned colors
523 :param saturation: saturation of returned colors
524 :param lightness: lightness of returned colors
524 :param lightness: lightness of returned colors
525 :returns: css RGB string
525 :returns: css RGB string
526
526
527 >>> color_hash = color_hasher()
527 >>> color_hash = color_hasher()
528 >>> color_hash('hello')
528 >>> color_hash('hello')
529 'rgb(34, 12, 59)'
529 'rgb(34, 12, 59)'
530 >>> color_hash('hello')
530 >>> color_hash('hello')
531 'rgb(34, 12, 59)'
531 'rgb(34, 12, 59)'
532 >>> color_hash('other')
532 >>> color_hash('other')
533 'rgb(90, 224, 159)'
533 'rgb(90, 224, 159)'
534 """
534 """
535
535
536 color_dict = {}
536 color_dict = {}
537 cgenerator = unique_color_generator(
537 cgenerator = unique_color_generator(
538 saturation=saturation, lightness=lightness)
538 saturation=saturation, lightness=lightness)
539
539
540 def get_color_string(thing):
540 def get_color_string(thing):
541 if thing in color_dict:
541 if thing in color_dict:
542 col = color_dict[thing]
542 col = color_dict[thing]
543 else:
543 else:
544 col = color_dict[thing] = cgenerator.next()
544 col = color_dict[thing] = cgenerator.next()
545 return "rgb(%s)" % (', '.join(col))
545 return "rgb(%s)" % (', '.join(col))
546
546
547 return get_color_string
547 return get_color_string
548
548
549
549
550 def get_lexer_safe(mimetype=None, filepath=None):
550 def get_lexer_safe(mimetype=None, filepath=None):
551 """
551 """
552 Tries to return a relevant pygments lexer using mimetype/filepath name,
552 Tries to return a relevant pygments lexer using mimetype/filepath name,
553 defaulting to plain text if none could be found
553 defaulting to plain text if none could be found
554 """
554 """
555 lexer = None
555 lexer = None
556 try:
556 try:
557 if mimetype:
557 if mimetype:
558 lexer = get_lexer_for_mimetype(mimetype)
558 lexer = get_lexer_for_mimetype(mimetype)
559 if not lexer:
559 if not lexer:
560 lexer = get_lexer_for_filename(filepath)
560 lexer = get_lexer_for_filename(filepath)
561 except pygments.util.ClassNotFound:
561 except pygments.util.ClassNotFound:
562 pass
562 pass
563
563
564 if not lexer:
564 if not lexer:
565 lexer = get_lexer_by_name('text')
565 lexer = get_lexer_by_name('text')
566
566
567 return lexer
567 return lexer
568
568
569
569
570 def get_lexer_for_filenode(filenode):
570 def get_lexer_for_filenode(filenode):
571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
572 return lexer
572 return lexer
573
573
574
574
575 def pygmentize(filenode, **kwargs):
575 def pygmentize(filenode, **kwargs):
576 """
576 """
577 pygmentize function using pygments
577 pygmentize function using pygments
578
578
579 :param filenode:
579 :param filenode:
580 """
580 """
581 lexer = get_lexer_for_filenode(filenode)
581 lexer = get_lexer_for_filenode(filenode)
582 return literal(code_highlight(filenode.content, lexer,
582 return literal(code_highlight(filenode.content, lexer,
583 CodeHtmlFormatter(**kwargs)))
583 CodeHtmlFormatter(**kwargs)))
584
584
585
585
586 def is_following_repo(repo_name, user_id):
586 def is_following_repo(repo_name, user_id):
587 from rhodecode.model.scm import ScmModel
587 from rhodecode.model.scm import ScmModel
588 return ScmModel().is_following_repo(repo_name, user_id)
588 return ScmModel().is_following_repo(repo_name, user_id)
589
589
590
590
591 class _Message(object):
591 class _Message(object):
592 """A message returned by ``Flash.pop_messages()``.
592 """A message returned by ``Flash.pop_messages()``.
593
593
594 Converting the message to a string returns the message text. Instances
594 Converting the message to a string returns the message text. Instances
595 also have the following attributes:
595 also have the following attributes:
596
596
597 * ``message``: the message text.
597 * ``message``: the message text.
598 * ``category``: the category specified when the message was created.
598 * ``category``: the category specified when the message was created.
599 """
599 """
600
600
601 def __init__(self, category, message):
601 def __init__(self, category, message):
602 self.category = category
602 self.category = category
603 self.message = message
603 self.message = message
604
604
605 def __str__(self):
605 def __str__(self):
606 return self.message
606 return self.message
607
607
608 __unicode__ = __str__
608 __unicode__ = __str__
609
609
610 def __html__(self):
610 def __html__(self):
611 return escape(safe_unicode(self.message))
611 return escape(safe_unicode(self.message))
612
612
613
613
614 class Flash(object):
614 class Flash(object):
615 # List of allowed categories. If None, allow any category.
615 # List of allowed categories. If None, allow any category.
616 categories = ["warning", "notice", "error", "success"]
616 categories = ["warning", "notice", "error", "success"]
617
617
618 # Default category if none is specified.
618 # Default category if none is specified.
619 default_category = "notice"
619 default_category = "notice"
620
620
621 def __init__(self, session_key="flash", categories=None,
621 def __init__(self, session_key="flash", categories=None,
622 default_category=None):
622 default_category=None):
623 """
623 """
624 Instantiate a ``Flash`` object.
624 Instantiate a ``Flash`` object.
625
625
626 ``session_key`` is the key to save the messages under in the user's
626 ``session_key`` is the key to save the messages under in the user's
627 session.
627 session.
628
628
629 ``categories`` is an optional list which overrides the default list
629 ``categories`` is an optional list which overrides the default list
630 of categories.
630 of categories.
631
631
632 ``default_category`` overrides the default category used for messages
632 ``default_category`` overrides the default category used for messages
633 when none is specified.
633 when none is specified.
634 """
634 """
635 self.session_key = session_key
635 self.session_key = session_key
636 if categories is not None:
636 if categories is not None:
637 self.categories = categories
637 self.categories = categories
638 if default_category is not None:
638 if default_category is not None:
639 self.default_category = default_category
639 self.default_category = default_category
640 if self.categories and self.default_category not in self.categories:
640 if self.categories and self.default_category not in self.categories:
641 raise ValueError(
641 raise ValueError(
642 "unrecognized default category %r" % (self.default_category,))
642 "unrecognized default category %r" % (self.default_category,))
643
643
644 def pop_messages(self, session=None, request=None):
644 def pop_messages(self, session=None, request=None):
645 """
645 """
646 Return all accumulated messages and delete them from the session.
646 Return all accumulated messages and delete them from the session.
647
647
648 The return value is a list of ``Message`` objects.
648 The return value is a list of ``Message`` objects.
649 """
649 """
650 messages = []
650 messages = []
651
651
652 if not session:
652 if not session:
653 if not request:
653 if not request:
654 request = get_current_request()
654 request = get_current_request()
655 session = request.session
655 session = request.session
656
656
657 # Pop the 'old' pylons flash messages. They are tuples of the form
657 # Pop the 'old' pylons flash messages. They are tuples of the form
658 # (category, message)
658 # (category, message)
659 for cat, msg in session.pop(self.session_key, []):
659 for cat, msg in session.pop(self.session_key, []):
660 messages.append(_Message(cat, msg))
660 messages.append(_Message(cat, msg))
661
661
662 # Pop the 'new' pyramid flash messages for each category as list
662 # Pop the 'new' pyramid flash messages for each category as list
663 # of strings.
663 # of strings.
664 for cat in self.categories:
664 for cat in self.categories:
665 for msg in session.pop_flash(queue=cat):
665 for msg in session.pop_flash(queue=cat):
666 messages.append(_Message(cat, msg))
666 messages.append(_Message(cat, msg))
667 # Map messages from the default queue to the 'notice' category.
667 # Map messages from the default queue to the 'notice' category.
668 for msg in session.pop_flash():
668 for msg in session.pop_flash():
669 messages.append(_Message('notice', msg))
669 messages.append(_Message('notice', msg))
670
670
671 session.save()
671 session.save()
672 return messages
672 return messages
673
673
674 def json_alerts(self, session=None, request=None):
674 def json_alerts(self, session=None, request=None):
675 payloads = []
675 payloads = []
676 messages = flash.pop_messages(session=session, request=request)
676 messages = flash.pop_messages(session=session, request=request)
677 if messages:
677 if messages:
678 for message in messages:
678 for message in messages:
679 subdata = {}
679 subdata = {}
680 if hasattr(message.message, 'rsplit'):
680 if hasattr(message.message, 'rsplit'):
681 flash_data = message.message.rsplit('|DELIM|', 1)
681 flash_data = message.message.rsplit('|DELIM|', 1)
682 org_message = flash_data[0]
682 org_message = flash_data[0]
683 if len(flash_data) > 1:
683 if len(flash_data) > 1:
684 subdata = json.loads(flash_data[1])
684 subdata = json.loads(flash_data[1])
685 else:
685 else:
686 org_message = message.message
686 org_message = message.message
687 payloads.append({
687 payloads.append({
688 'message': {
688 'message': {
689 'message': u'{}'.format(org_message),
689 'message': u'{}'.format(org_message),
690 'level': message.category,
690 'level': message.category,
691 'force': True,
691 'force': True,
692 'subdata': subdata
692 'subdata': subdata
693 }
693 }
694 })
694 })
695 return json.dumps(payloads)
695 return json.dumps(payloads)
696
696
697 def __call__(self, message, category=None, ignore_duplicate=True,
697 def __call__(self, message, category=None, ignore_duplicate=True,
698 session=None, request=None):
698 session=None, request=None):
699
699
700 if not session:
700 if not session:
701 if not request:
701 if not request:
702 request = get_current_request()
702 request = get_current_request()
703 session = request.session
703 session = request.session
704
704
705 session.flash(
705 session.flash(
706 message, queue=category, allow_duplicate=not ignore_duplicate)
706 message, queue=category, allow_duplicate=not ignore_duplicate)
707
707
708
708
709 flash = Flash()
709 flash = Flash()
710
710
711 #==============================================================================
711 #==============================================================================
712 # SCM FILTERS available via h.
712 # SCM FILTERS available via h.
713 #==============================================================================
713 #==============================================================================
714 from rhodecode.lib.vcs.utils import author_name, author_email
714 from rhodecode.lib.vcs.utils import author_name, author_email
715 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
715 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
716 from rhodecode.model.db import User, ChangesetStatus
716 from rhodecode.model.db import User, ChangesetStatus
717
717
718 capitalize = lambda x: x.capitalize()
718 capitalize = lambda x: x.capitalize()
719 email = author_email
719 email = author_email
720 short_id = lambda x: x[:12]
720 short_id = lambda x: x[:12]
721 hide_credentials = lambda x: ''.join(credentials_filter(x))
721 hide_credentials = lambda x: ''.join(credentials_filter(x))
722
722
723
723
724 import pytz
724 import pytz
725 import tzlocal
725 import tzlocal
726 local_timezone = tzlocal.get_localzone()
726 local_timezone = tzlocal.get_localzone()
727
727
728
728
729 def age_component(datetime_iso, value=None, time_is_local=False):
729 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
730 title = value or format_date(datetime_iso)
730 title = value or format_date(datetime_iso)
731 tzinfo = '+00:00'
731 tzinfo = '+00:00'
732
732
733 # detect if we have a timezone info, otherwise, add it
733 # detect if we have a timezone info, otherwise, add it
734 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
734 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
735 force_timezone = os.environ.get('RC_TIMEZONE', '')
735 force_timezone = os.environ.get('RC_TIMEZONE', '')
736 if force_timezone:
736 if force_timezone:
737 force_timezone = pytz.timezone(force_timezone)
737 force_timezone = pytz.timezone(force_timezone)
738 timezone = force_timezone or local_timezone
738 timezone = force_timezone or local_timezone
739 offset = timezone.localize(datetime_iso).strftime('%z')
739 offset = timezone.localize(datetime_iso).strftime('%z')
740 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
740 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
741
741
742 return literal(
742 return literal(
743 '<time class="timeago tooltip" '
743 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
744 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
744 cls='tooltip' if tooltip else '',
745 datetime_iso, title, tzinfo))
745 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
746 title=title, dt=datetime_iso, tzinfo=tzinfo
747 ))
746
748
747
749
748 def _shorten_commit_id(commit_id, commit_len=None):
750 def _shorten_commit_id(commit_id, commit_len=None):
749 if commit_len is None:
751 if commit_len is None:
750 request = get_current_request()
752 request = get_current_request()
751 commit_len = request.call_context.visual.show_sha_length
753 commit_len = request.call_context.visual.show_sha_length
752 return commit_id[:commit_len]
754 return commit_id[:commit_len]
753
755
754
756
755 def show_id(commit, show_idx=None, commit_len=None):
757 def show_id(commit, show_idx=None, commit_len=None):
756 """
758 """
757 Configurable function that shows ID
759 Configurable function that shows ID
758 by default it's r123:fffeeefffeee
760 by default it's r123:fffeeefffeee
759
761
760 :param commit: commit instance
762 :param commit: commit instance
761 """
763 """
762 if show_idx is None:
764 if show_idx is None:
763 request = get_current_request()
765 request = get_current_request()
764 show_idx = request.call_context.visual.show_revision_number
766 show_idx = request.call_context.visual.show_revision_number
765
767
766 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
768 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
767 if show_idx:
769 if show_idx:
768 return 'r%s:%s' % (commit.idx, raw_id)
770 return 'r%s:%s' % (commit.idx, raw_id)
769 else:
771 else:
770 return '%s' % (raw_id, )
772 return '%s' % (raw_id, )
771
773
772
774
773 def format_date(date):
775 def format_date(date):
774 """
776 """
775 use a standardized formatting for dates used in RhodeCode
777 use a standardized formatting for dates used in RhodeCode
776
778
777 :param date: date/datetime object
779 :param date: date/datetime object
778 :return: formatted date
780 :return: formatted date
779 """
781 """
780
782
781 if date:
783 if date:
782 _fmt = "%a, %d %b %Y %H:%M:%S"
784 _fmt = "%a, %d %b %Y %H:%M:%S"
783 return safe_unicode(date.strftime(_fmt))
785 return safe_unicode(date.strftime(_fmt))
784
786
785 return u""
787 return u""
786
788
787
789
788 class _RepoChecker(object):
790 class _RepoChecker(object):
789
791
790 def __init__(self, backend_alias):
792 def __init__(self, backend_alias):
791 self._backend_alias = backend_alias
793 self._backend_alias = backend_alias
792
794
793 def __call__(self, repository):
795 def __call__(self, repository):
794 if hasattr(repository, 'alias'):
796 if hasattr(repository, 'alias'):
795 _type = repository.alias
797 _type = repository.alias
796 elif hasattr(repository, 'repo_type'):
798 elif hasattr(repository, 'repo_type'):
797 _type = repository.repo_type
799 _type = repository.repo_type
798 else:
800 else:
799 _type = repository
801 _type = repository
800 return _type == self._backend_alias
802 return _type == self._backend_alias
801
803
802
804
803 is_git = _RepoChecker('git')
805 is_git = _RepoChecker('git')
804 is_hg = _RepoChecker('hg')
806 is_hg = _RepoChecker('hg')
805 is_svn = _RepoChecker('svn')
807 is_svn = _RepoChecker('svn')
806
808
807
809
808 def get_repo_type_by_name(repo_name):
810 def get_repo_type_by_name(repo_name):
809 repo = Repository.get_by_repo_name(repo_name)
811 repo = Repository.get_by_repo_name(repo_name)
810 if repo:
812 if repo:
811 return repo.repo_type
813 return repo.repo_type
812
814
813
815
814 def is_svn_without_proxy(repository):
816 def is_svn_without_proxy(repository):
815 if is_svn(repository):
817 if is_svn(repository):
816 from rhodecode.model.settings import VcsSettingsModel
818 from rhodecode.model.settings import VcsSettingsModel
817 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
819 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
818 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
820 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
819 return False
821 return False
820
822
821
823
822 def discover_user(author):
824 def discover_user(author):
823 """
825 """
824 Tries to discover RhodeCode User based on the author string. Author string
826 Tries to discover RhodeCode User based on the author string. Author string
825 is typically `FirstName LastName <email@address.com>`
827 is typically `FirstName LastName <email@address.com>`
826 """
828 """
827
829
828 # if author is already an instance use it for extraction
830 # if author is already an instance use it for extraction
829 if isinstance(author, User):
831 if isinstance(author, User):
830 return author
832 return author
831
833
832 # Valid email in the attribute passed, see if they're in the system
834 # Valid email in the attribute passed, see if they're in the system
833 _email = author_email(author)
835 _email = author_email(author)
834 if _email != '':
836 if _email != '':
835 user = User.get_by_email(_email, case_insensitive=True, cache=True)
837 user = User.get_by_email(_email, case_insensitive=True, cache=True)
836 if user is not None:
838 if user is not None:
837 return user
839 return user
838
840
839 # Maybe it's a username, we try to extract it and fetch by username ?
841 # Maybe it's a username, we try to extract it and fetch by username ?
840 _author = author_name(author)
842 _author = author_name(author)
841 user = User.get_by_username(_author, case_insensitive=True, cache=True)
843 user = User.get_by_username(_author, case_insensitive=True, cache=True)
842 if user is not None:
844 if user is not None:
843 return user
845 return user
844
846
845 return None
847 return None
846
848
847
849
848 def email_or_none(author):
850 def email_or_none(author):
849 # extract email from the commit string
851 # extract email from the commit string
850 _email = author_email(author)
852 _email = author_email(author)
851
853
852 # If we have an email, use it, otherwise
854 # If we have an email, use it, otherwise
853 # see if it contains a username we can get an email from
855 # see if it contains a username we can get an email from
854 if _email != '':
856 if _email != '':
855 return _email
857 return _email
856 else:
858 else:
857 user = User.get_by_username(
859 user = User.get_by_username(
858 author_name(author), case_insensitive=True, cache=True)
860 author_name(author), case_insensitive=True, cache=True)
859
861
860 if user is not None:
862 if user is not None:
861 return user.email
863 return user.email
862
864
863 # No valid email, not a valid user in the system, none!
865 # No valid email, not a valid user in the system, none!
864 return None
866 return None
865
867
866
868
867 def link_to_user(author, length=0, **kwargs):
869 def link_to_user(author, length=0, **kwargs):
868 user = discover_user(author)
870 user = discover_user(author)
869 # user can be None, but if we have it already it means we can re-use it
871 # user can be None, but if we have it already it means we can re-use it
870 # in the person() function, so we save 1 intensive-query
872 # in the person() function, so we save 1 intensive-query
871 if user:
873 if user:
872 author = user
874 author = user
873
875
874 display_person = person(author, 'username_or_name_or_email')
876 display_person = person(author, 'username_or_name_or_email')
875 if length:
877 if length:
876 display_person = shorter(display_person, length)
878 display_person = shorter(display_person, length)
877
879
878 if user:
880 if user:
879 return link_to(
881 return link_to(
880 escape(display_person),
882 escape(display_person),
881 route_path('user_profile', username=user.username),
883 route_path('user_profile', username=user.username),
882 **kwargs)
884 **kwargs)
883 else:
885 else:
884 return escape(display_person)
886 return escape(display_person)
885
887
886
888
887 def link_to_group(users_group_name, **kwargs):
889 def link_to_group(users_group_name, **kwargs):
888 return link_to(
890 return link_to(
889 escape(users_group_name),
891 escape(users_group_name),
890 route_path('user_group_profile', user_group_name=users_group_name),
892 route_path('user_group_profile', user_group_name=users_group_name),
891 **kwargs)
893 **kwargs)
892
894
893
895
894 def person(author, show_attr="username_and_name"):
896 def person(author, show_attr="username_and_name"):
895 user = discover_user(author)
897 user = discover_user(author)
896 if user:
898 if user:
897 return getattr(user, show_attr)
899 return getattr(user, show_attr)
898 else:
900 else:
899 _author = author_name(author)
901 _author = author_name(author)
900 _email = email(author)
902 _email = email(author)
901 return _author or _email
903 return _author or _email
902
904
903
905
904 def author_string(email):
906 def author_string(email):
905 if email:
907 if email:
906 user = User.get_by_email(email, case_insensitive=True, cache=True)
908 user = User.get_by_email(email, case_insensitive=True, cache=True)
907 if user:
909 if user:
908 if user.first_name or user.last_name:
910 if user.first_name or user.last_name:
909 return '%s %s &lt;%s&gt;' % (
911 return '%s %s &lt;%s&gt;' % (
910 user.first_name, user.last_name, email)
912 user.first_name, user.last_name, email)
911 else:
913 else:
912 return email
914 return email
913 else:
915 else:
914 return email
916 return email
915 else:
917 else:
916 return None
918 return None
917
919
918
920
919 def person_by_id(id_, show_attr="username_and_name"):
921 def person_by_id(id_, show_attr="username_and_name"):
920 # attr to return from fetched user
922 # attr to return from fetched user
921 person_getter = lambda usr: getattr(usr, show_attr)
923 person_getter = lambda usr: getattr(usr, show_attr)
922
924
923 #maybe it's an ID ?
925 #maybe it's an ID ?
924 if str(id_).isdigit() or isinstance(id_, int):
926 if str(id_).isdigit() or isinstance(id_, int):
925 id_ = int(id_)
927 id_ = int(id_)
926 user = User.get(id_)
928 user = User.get(id_)
927 if user is not None:
929 if user is not None:
928 return person_getter(user)
930 return person_getter(user)
929 return id_
931 return id_
930
932
931
933
932 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
934 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
933 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
935 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
934 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
936 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
935
937
936
938
937 tags_paterns = OrderedDict((
939 tags_paterns = OrderedDict((
938 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
940 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
939 '<div class="metatag" tag="lang">\\2</div>')),
941 '<div class="metatag" tag="lang">\\2</div>')),
940
942
941 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
943 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
942 '<div class="metatag" tag="see">see: \\1 </div>')),
944 '<div class="metatag" tag="see">see: \\1 </div>')),
943
945
944 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
946 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
945 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
947 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
946
948
947 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
949 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
948 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
950 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
949
951
950 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
952 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
951 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
953 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
952
954
953 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
955 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
954 '<div class="metatag" tag="state \\1">\\1</div>')),
956 '<div class="metatag" tag="state \\1">\\1</div>')),
955
957
956 # label in grey
958 # label in grey
957 ('label', (re.compile(r'\[([a-z]+)\]'),
959 ('label', (re.compile(r'\[([a-z]+)\]'),
958 '<div class="metatag" tag="label">\\1</div>')),
960 '<div class="metatag" tag="label">\\1</div>')),
959
961
960 # generic catch all in grey
962 # generic catch all in grey
961 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
963 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
962 '<div class="metatag" tag="generic">\\1</div>')),
964 '<div class="metatag" tag="generic">\\1</div>')),
963 ))
965 ))
964
966
965
967
966 def extract_metatags(value):
968 def extract_metatags(value):
967 """
969 """
968 Extract supported meta-tags from given text value
970 Extract supported meta-tags from given text value
969 """
971 """
970 tags = []
972 tags = []
971 if not value:
973 if not value:
972 return tags, ''
974 return tags, ''
973
975
974 for key, val in tags_paterns.items():
976 for key, val in tags_paterns.items():
975 pat, replace_html = val
977 pat, replace_html = val
976 tags.extend([(key, x.group()) for x in pat.finditer(value)])
978 tags.extend([(key, x.group()) for x in pat.finditer(value)])
977 value = pat.sub('', value)
979 value = pat.sub('', value)
978
980
979 return tags, value
981 return tags, value
980
982
981
983
982 def style_metatag(tag_type, value):
984 def style_metatag(tag_type, value):
983 """
985 """
984 converts tags from value into html equivalent
986 converts tags from value into html equivalent
985 """
987 """
986 if not value:
988 if not value:
987 return ''
989 return ''
988
990
989 html_value = value
991 html_value = value
990 tag_data = tags_paterns.get(tag_type)
992 tag_data = tags_paterns.get(tag_type)
991 if tag_data:
993 if tag_data:
992 pat, replace_html = tag_data
994 pat, replace_html = tag_data
993 # convert to plain `unicode` instead of a markup tag to be used in
995 # convert to plain `unicode` instead of a markup tag to be used in
994 # regex expressions. safe_unicode doesn't work here
996 # regex expressions. safe_unicode doesn't work here
995 html_value = pat.sub(replace_html, unicode(value))
997 html_value = pat.sub(replace_html, unicode(value))
996
998
997 return html_value
999 return html_value
998
1000
999
1001
1000 def bool2icon(value, show_at_false=True):
1002 def bool2icon(value, show_at_false=True):
1001 """
1003 """
1002 Returns boolean value of a given value, represented as html element with
1004 Returns boolean value of a given value, represented as html element with
1003 classes that will represent icons
1005 classes that will represent icons
1004
1006
1005 :param value: given value to convert to html node
1007 :param value: given value to convert to html node
1006 """
1008 """
1007
1009
1008 if value: # does bool conversion
1010 if value: # does bool conversion
1009 return HTML.tag('i', class_="icon-true", title='True')
1011 return HTML.tag('i', class_="icon-true", title='True')
1010 else: # not true as bool
1012 else: # not true as bool
1011 if show_at_false:
1013 if show_at_false:
1012 return HTML.tag('i', class_="icon-false", title='False')
1014 return HTML.tag('i', class_="icon-false", title='False')
1013 return HTML.tag('i')
1015 return HTML.tag('i')
1014
1016
1015 #==============================================================================
1017 #==============================================================================
1016 # PERMS
1018 # PERMS
1017 #==============================================================================
1019 #==============================================================================
1018 from rhodecode.lib.auth import (
1020 from rhodecode.lib.auth import (
1019 HasPermissionAny, HasPermissionAll,
1021 HasPermissionAny, HasPermissionAll,
1020 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1022 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1021 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1023 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1022 csrf_token_key, AuthUser)
1024 csrf_token_key, AuthUser)
1023
1025
1024
1026
1025 #==============================================================================
1027 #==============================================================================
1026 # GRAVATAR URL
1028 # GRAVATAR URL
1027 #==============================================================================
1029 #==============================================================================
1028 class InitialsGravatar(object):
1030 class InitialsGravatar(object):
1029 def __init__(self, email_address, first_name, last_name, size=30,
1031 def __init__(self, email_address, first_name, last_name, size=30,
1030 background=None, text_color='#fff'):
1032 background=None, text_color='#fff'):
1031 self.size = size
1033 self.size = size
1032 self.first_name = first_name
1034 self.first_name = first_name
1033 self.last_name = last_name
1035 self.last_name = last_name
1034 self.email_address = email_address
1036 self.email_address = email_address
1035 self.background = background or self.str2color(email_address)
1037 self.background = background or self.str2color(email_address)
1036 self.text_color = text_color
1038 self.text_color = text_color
1037
1039
1038 def get_color_bank(self):
1040 def get_color_bank(self):
1039 """
1041 """
1040 returns a predefined list of colors that gravatars can use.
1042 returns a predefined list of colors that gravatars can use.
1041 Those are randomized distinct colors that guarantee readability and
1043 Those are randomized distinct colors that guarantee readability and
1042 uniqueness.
1044 uniqueness.
1043
1045
1044 generated with: http://phrogz.net/css/distinct-colors.html
1046 generated with: http://phrogz.net/css/distinct-colors.html
1045 """
1047 """
1046 return [
1048 return [
1047 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1049 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1048 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1050 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1049 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1051 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1050 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1052 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1051 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1053 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1052 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1054 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1053 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1055 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1054 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1056 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1055 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1057 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1056 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1058 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1057 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1059 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1058 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1060 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1059 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1061 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1060 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1062 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1061 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1063 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1062 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1064 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1063 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1065 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1064 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1066 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1065 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1067 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1066 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1068 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1067 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1069 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1068 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1070 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1069 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1071 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1070 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1072 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1071 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1073 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1072 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1074 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1073 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1075 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1074 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1076 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1075 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1077 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1076 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1078 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1077 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1079 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1078 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1080 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1079 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1081 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1080 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1082 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1081 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1083 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1082 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1084 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1083 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1085 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1084 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1086 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1085 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1087 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1086 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1088 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1087 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1089 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1088 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1090 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1089 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1091 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1090 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1092 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1091 '#4f8c46', '#368dd9', '#5c0073'
1093 '#4f8c46', '#368dd9', '#5c0073'
1092 ]
1094 ]
1093
1095
1094 def rgb_to_hex_color(self, rgb_tuple):
1096 def rgb_to_hex_color(self, rgb_tuple):
1095 """
1097 """
1096 Converts an rgb_tuple passed to an hex color.
1098 Converts an rgb_tuple passed to an hex color.
1097
1099
1098 :param rgb_tuple: tuple with 3 ints represents rgb color space
1100 :param rgb_tuple: tuple with 3 ints represents rgb color space
1099 """
1101 """
1100 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1102 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1101
1103
1102 def email_to_int_list(self, email_str):
1104 def email_to_int_list(self, email_str):
1103 """
1105 """
1104 Get every byte of the hex digest value of email and turn it to integer.
1106 Get every byte of the hex digest value of email and turn it to integer.
1105 It's going to be always between 0-255
1107 It's going to be always between 0-255
1106 """
1108 """
1107 digest = md5_safe(email_str.lower())
1109 digest = md5_safe(email_str.lower())
1108 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1110 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1109
1111
1110 def pick_color_bank_index(self, email_str, color_bank):
1112 def pick_color_bank_index(self, email_str, color_bank):
1111 return self.email_to_int_list(email_str)[0] % len(color_bank)
1113 return self.email_to_int_list(email_str)[0] % len(color_bank)
1112
1114
1113 def str2color(self, email_str):
1115 def str2color(self, email_str):
1114 """
1116 """
1115 Tries to map in a stable algorithm an email to color
1117 Tries to map in a stable algorithm an email to color
1116
1118
1117 :param email_str:
1119 :param email_str:
1118 """
1120 """
1119 color_bank = self.get_color_bank()
1121 color_bank = self.get_color_bank()
1120 # pick position (module it's length so we always find it in the
1122 # pick position (module it's length so we always find it in the
1121 # bank even if it's smaller than 256 values
1123 # bank even if it's smaller than 256 values
1122 pos = self.pick_color_bank_index(email_str, color_bank)
1124 pos = self.pick_color_bank_index(email_str, color_bank)
1123 return color_bank[pos]
1125 return color_bank[pos]
1124
1126
1125 def normalize_email(self, email_address):
1127 def normalize_email(self, email_address):
1126 import unicodedata
1128 import unicodedata
1127 # default host used to fill in the fake/missing email
1129 # default host used to fill in the fake/missing email
1128 default_host = u'localhost'
1130 default_host = u'localhost'
1129
1131
1130 if not email_address:
1132 if not email_address:
1131 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1133 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1132
1134
1133 email_address = safe_unicode(email_address)
1135 email_address = safe_unicode(email_address)
1134
1136
1135 if u'@' not in email_address:
1137 if u'@' not in email_address:
1136 email_address = u'%s@%s' % (email_address, default_host)
1138 email_address = u'%s@%s' % (email_address, default_host)
1137
1139
1138 if email_address.endswith(u'@'):
1140 if email_address.endswith(u'@'):
1139 email_address = u'%s%s' % (email_address, default_host)
1141 email_address = u'%s%s' % (email_address, default_host)
1140
1142
1141 email_address = unicodedata.normalize('NFKD', email_address)\
1143 email_address = unicodedata.normalize('NFKD', email_address)\
1142 .encode('ascii', 'ignore')
1144 .encode('ascii', 'ignore')
1143 return email_address
1145 return email_address
1144
1146
1145 def get_initials(self):
1147 def get_initials(self):
1146 """
1148 """
1147 Returns 2 letter initials calculated based on the input.
1149 Returns 2 letter initials calculated based on the input.
1148 The algorithm picks first given email address, and takes first letter
1150 The algorithm picks first given email address, and takes first letter
1149 of part before @, and then the first letter of server name. In case
1151 of part before @, and then the first letter of server name. In case
1150 the part before @ is in a format of `somestring.somestring2` it replaces
1152 the part before @ is in a format of `somestring.somestring2` it replaces
1151 the server letter with first letter of somestring2
1153 the server letter with first letter of somestring2
1152
1154
1153 In case function was initialized with both first and lastname, this
1155 In case function was initialized with both first and lastname, this
1154 overrides the extraction from email by first letter of the first and
1156 overrides the extraction from email by first letter of the first and
1155 last name. We add special logic to that functionality, In case Full name
1157 last name. We add special logic to that functionality, In case Full name
1156 is compound, like Guido Von Rossum, we use last part of the last name
1158 is compound, like Guido Von Rossum, we use last part of the last name
1157 (Von Rossum) picking `R`.
1159 (Von Rossum) picking `R`.
1158
1160
1159 Function also normalizes the non-ascii characters to they ascii
1161 Function also normalizes the non-ascii characters to they ascii
1160 representation, eg Δ„ => A
1162 representation, eg Δ„ => A
1161 """
1163 """
1162 import unicodedata
1164 import unicodedata
1163 # replace non-ascii to ascii
1165 # replace non-ascii to ascii
1164 first_name = unicodedata.normalize(
1166 first_name = unicodedata.normalize(
1165 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1167 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1166 last_name = unicodedata.normalize(
1168 last_name = unicodedata.normalize(
1167 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1169 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1168
1170
1169 # do NFKD encoding, and also make sure email has proper format
1171 # do NFKD encoding, and also make sure email has proper format
1170 email_address = self.normalize_email(self.email_address)
1172 email_address = self.normalize_email(self.email_address)
1171
1173
1172 # first push the email initials
1174 # first push the email initials
1173 prefix, server = email_address.split('@', 1)
1175 prefix, server = email_address.split('@', 1)
1174
1176
1175 # check if prefix is maybe a 'first_name.last_name' syntax
1177 # check if prefix is maybe a 'first_name.last_name' syntax
1176 _dot_split = prefix.rsplit('.', 1)
1178 _dot_split = prefix.rsplit('.', 1)
1177 if len(_dot_split) == 2 and _dot_split[1]:
1179 if len(_dot_split) == 2 and _dot_split[1]:
1178 initials = [_dot_split[0][0], _dot_split[1][0]]
1180 initials = [_dot_split[0][0], _dot_split[1][0]]
1179 else:
1181 else:
1180 initials = [prefix[0], server[0]]
1182 initials = [prefix[0], server[0]]
1181
1183
1182 # then try to replace either first_name or last_name
1184 # then try to replace either first_name or last_name
1183 fn_letter = (first_name or " ")[0].strip()
1185 fn_letter = (first_name or " ")[0].strip()
1184 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1186 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1185
1187
1186 if fn_letter:
1188 if fn_letter:
1187 initials[0] = fn_letter
1189 initials[0] = fn_letter
1188
1190
1189 if ln_letter:
1191 if ln_letter:
1190 initials[1] = ln_letter
1192 initials[1] = ln_letter
1191
1193
1192 return ''.join(initials).upper()
1194 return ''.join(initials).upper()
1193
1195
1194 def get_img_data_by_type(self, font_family, img_type):
1196 def get_img_data_by_type(self, font_family, img_type):
1195 default_user = """
1197 default_user = """
1196 <svg xmlns="http://www.w3.org/2000/svg"
1198 <svg xmlns="http://www.w3.org/2000/svg"
1197 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1199 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1198 viewBox="-15 -10 439.165 429.164"
1200 viewBox="-15 -10 439.165 429.164"
1199
1201
1200 xml:space="preserve"
1202 xml:space="preserve"
1201 style="background:{background};" >
1203 style="background:{background};" >
1202
1204
1203 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1205 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1204 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1206 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1205 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1207 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1206 168.596,153.916,216.671,
1208 168.596,153.916,216.671,
1207 204.583,216.671z" fill="{text_color}"/>
1209 204.583,216.671z" fill="{text_color}"/>
1208 <path d="M407.164,374.717L360.88,
1210 <path d="M407.164,374.717L360.88,
1209 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1211 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1210 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1212 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1211 15.366-44.203,23.488-69.076,23.488c-24.877,
1213 15.366-44.203,23.488-69.076,23.488c-24.877,
1212 0-48.762-8.122-69.078-23.488
1214 0-48.762-8.122-69.078-23.488
1213 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1215 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1214 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1216 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1215 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1217 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1216 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1218 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1217 19.402-10.527 C409.699,390.129,
1219 19.402-10.527 C409.699,390.129,
1218 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1220 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1219 </svg>""".format(
1221 </svg>""".format(
1220 size=self.size,
1222 size=self.size,
1221 background='#979797', # @grey4
1223 background='#979797', # @grey4
1222 text_color=self.text_color,
1224 text_color=self.text_color,
1223 font_family=font_family)
1225 font_family=font_family)
1224
1226
1225 return {
1227 return {
1226 "default_user": default_user
1228 "default_user": default_user
1227 }[img_type]
1229 }[img_type]
1228
1230
1229 def get_img_data(self, svg_type=None):
1231 def get_img_data(self, svg_type=None):
1230 """
1232 """
1231 generates the svg metadata for image
1233 generates the svg metadata for image
1232 """
1234 """
1233 fonts = [
1235 fonts = [
1234 '-apple-system',
1236 '-apple-system',
1235 'BlinkMacSystemFont',
1237 'BlinkMacSystemFont',
1236 'Segoe UI',
1238 'Segoe UI',
1237 'Roboto',
1239 'Roboto',
1238 'Oxygen-Sans',
1240 'Oxygen-Sans',
1239 'Ubuntu',
1241 'Ubuntu',
1240 'Cantarell',
1242 'Cantarell',
1241 'Helvetica Neue',
1243 'Helvetica Neue',
1242 'sans-serif'
1244 'sans-serif'
1243 ]
1245 ]
1244 font_family = ','.join(fonts)
1246 font_family = ','.join(fonts)
1245 if svg_type:
1247 if svg_type:
1246 return self.get_img_data_by_type(font_family, svg_type)
1248 return self.get_img_data_by_type(font_family, svg_type)
1247
1249
1248 initials = self.get_initials()
1250 initials = self.get_initials()
1249 img_data = """
1251 img_data = """
1250 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1252 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1251 width="{size}" height="{size}"
1253 width="{size}" height="{size}"
1252 style="width: 100%; height: 100%; background-color: {background}"
1254 style="width: 100%; height: 100%; background-color: {background}"
1253 viewBox="0 0 {size} {size}">
1255 viewBox="0 0 {size} {size}">
1254 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1256 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1255 pointer-events="auto" fill="{text_color}"
1257 pointer-events="auto" fill="{text_color}"
1256 font-family="{font_family}"
1258 font-family="{font_family}"
1257 style="font-weight: 400; font-size: {f_size}px;">{text}
1259 style="font-weight: 400; font-size: {f_size}px;">{text}
1258 </text>
1260 </text>
1259 </svg>""".format(
1261 </svg>""".format(
1260 size=self.size,
1262 size=self.size,
1261 f_size=self.size/2.05, # scale the text inside the box nicely
1263 f_size=self.size/2.05, # scale the text inside the box nicely
1262 background=self.background,
1264 background=self.background,
1263 text_color=self.text_color,
1265 text_color=self.text_color,
1264 text=initials.upper(),
1266 text=initials.upper(),
1265 font_family=font_family)
1267 font_family=font_family)
1266
1268
1267 return img_data
1269 return img_data
1268
1270
1269 def generate_svg(self, svg_type=None):
1271 def generate_svg(self, svg_type=None):
1270 img_data = self.get_img_data(svg_type)
1272 img_data = self.get_img_data(svg_type)
1271 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1273 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1272
1274
1273
1275
1274 def initials_gravatar(email_address, first_name, last_name, size=30):
1276 def initials_gravatar(email_address, first_name, last_name, size=30):
1275 svg_type = None
1277 svg_type = None
1276 if email_address == User.DEFAULT_USER_EMAIL:
1278 if email_address == User.DEFAULT_USER_EMAIL:
1277 svg_type = 'default_user'
1279 svg_type = 'default_user'
1278 klass = InitialsGravatar(email_address, first_name, last_name, size)
1280 klass = InitialsGravatar(email_address, first_name, last_name, size)
1279 return klass.generate_svg(svg_type=svg_type)
1281 return klass.generate_svg(svg_type=svg_type)
1280
1282
1281
1283
1282 def gravatar_url(email_address, size=30, request=None):
1284 def gravatar_url(email_address, size=30, request=None):
1283 request = get_current_request()
1285 request = get_current_request()
1284 _use_gravatar = request.call_context.visual.use_gravatar
1286 _use_gravatar = request.call_context.visual.use_gravatar
1285 _gravatar_url = request.call_context.visual.gravatar_url
1287 _gravatar_url = request.call_context.visual.gravatar_url
1286
1288
1287 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1289 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1288
1290
1289 email_address = email_address or User.DEFAULT_USER_EMAIL
1291 email_address = email_address or User.DEFAULT_USER_EMAIL
1290 if isinstance(email_address, unicode):
1292 if isinstance(email_address, unicode):
1291 # hashlib crashes on unicode items
1293 # hashlib crashes on unicode items
1292 email_address = safe_str(email_address)
1294 email_address = safe_str(email_address)
1293
1295
1294 # empty email or default user
1296 # empty email or default user
1295 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1297 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1296 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1298 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1297
1299
1298 if _use_gravatar:
1300 if _use_gravatar:
1299 # TODO: Disuse pyramid thread locals. Think about another solution to
1301 # TODO: Disuse pyramid thread locals. Think about another solution to
1300 # get the host and schema here.
1302 # get the host and schema here.
1301 request = get_current_request()
1303 request = get_current_request()
1302 tmpl = safe_str(_gravatar_url)
1304 tmpl = safe_str(_gravatar_url)
1303 tmpl = tmpl.replace('{email}', email_address)\
1305 tmpl = tmpl.replace('{email}', email_address)\
1304 .replace('{md5email}', md5_safe(email_address.lower())) \
1306 .replace('{md5email}', md5_safe(email_address.lower())) \
1305 .replace('{netloc}', request.host)\
1307 .replace('{netloc}', request.host)\
1306 .replace('{scheme}', request.scheme)\
1308 .replace('{scheme}', request.scheme)\
1307 .replace('{size}', safe_str(size))
1309 .replace('{size}', safe_str(size))
1308 return tmpl
1310 return tmpl
1309 else:
1311 else:
1310 return initials_gravatar(email_address, '', '', size=size)
1312 return initials_gravatar(email_address, '', '', size=size)
1311
1313
1312
1314
1313 def breadcrumb_repo_link(repo):
1315 def breadcrumb_repo_link(repo):
1314 """
1316 """
1315 Makes a breadcrumbs path link to repo
1317 Makes a breadcrumbs path link to repo
1316
1318
1317 ex::
1319 ex::
1318 group >> subgroup >> repo
1320 group >> subgroup >> repo
1319
1321
1320 :param repo: a Repository instance
1322 :param repo: a Repository instance
1321 """
1323 """
1322
1324
1323 path = [
1325 path = [
1324 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1326 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1325 title='last change:{}'.format(format_date(group.last_commit_change)))
1327 title='last change:{}'.format(format_date(group.last_commit_change)))
1326 for group in repo.groups_with_parents
1328 for group in repo.groups_with_parents
1327 ] + [
1329 ] + [
1328 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1330 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1329 title='last change:{}'.format(format_date(repo.last_commit_change)))
1331 title='last change:{}'.format(format_date(repo.last_commit_change)))
1330 ]
1332 ]
1331
1333
1332 return literal(' &raquo; '.join(path))
1334 return literal(' &raquo; '.join(path))
1333
1335
1334
1336
1335 def breadcrumb_repo_group_link(repo_group):
1337 def breadcrumb_repo_group_link(repo_group):
1336 """
1338 """
1337 Makes a breadcrumbs path link to repo
1339 Makes a breadcrumbs path link to repo
1338
1340
1339 ex::
1341 ex::
1340 group >> subgroup
1342 group >> subgroup
1341
1343
1342 :param repo_group: a Repository Group instance
1344 :param repo_group: a Repository Group instance
1343 """
1345 """
1344
1346
1345 path = [
1347 path = [
1346 link_to(group.name,
1348 link_to(group.name,
1347 route_path('repo_group_home', repo_group_name=group.group_name),
1349 route_path('repo_group_home', repo_group_name=group.group_name),
1348 title='last change:{}'.format(format_date(group.last_commit_change)))
1350 title='last change:{}'.format(format_date(group.last_commit_change)))
1349 for group in repo_group.parents
1351 for group in repo_group.parents
1350 ] + [
1352 ] + [
1351 link_to(repo_group.name,
1353 link_to(repo_group.name,
1352 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1354 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1353 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1355 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1354 ]
1356 ]
1355
1357
1356 return literal(' &raquo; '.join(path))
1358 return literal(' &raquo; '.join(path))
1357
1359
1358
1360
1359 def format_byte_size_binary(file_size):
1361 def format_byte_size_binary(file_size):
1360 """
1362 """
1361 Formats file/folder sizes to standard.
1363 Formats file/folder sizes to standard.
1362 """
1364 """
1363 if file_size is None:
1365 if file_size is None:
1364 file_size = 0
1366 file_size = 0
1365
1367
1366 formatted_size = format_byte_size(file_size, binary=True)
1368 formatted_size = format_byte_size(file_size, binary=True)
1367 return formatted_size
1369 return formatted_size
1368
1370
1369
1371
1370 def urlify_text(text_, safe=True, **href_attrs):
1372 def urlify_text(text_, safe=True, **href_attrs):
1371 """
1373 """
1372 Extract urls from text and make html links out of them
1374 Extract urls from text and make html links out of them
1373 """
1375 """
1374
1376
1375 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1377 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1376 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1378 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1377
1379
1378 def url_func(match_obj):
1380 def url_func(match_obj):
1379 url_full = match_obj.groups()[0]
1381 url_full = match_obj.groups()[0]
1380 a_options = dict(href_attrs)
1382 a_options = dict(href_attrs)
1381 a_options['href'] = url_full
1383 a_options['href'] = url_full
1382 a_text = url_full
1384 a_text = url_full
1383 return HTML.tag("a", a_text, **a_options)
1385 return HTML.tag("a", a_text, **a_options)
1384
1386
1385 _new_text = url_pat.sub(url_func, text_)
1387 _new_text = url_pat.sub(url_func, text_)
1386
1388
1387 if safe:
1389 if safe:
1388 return literal(_new_text)
1390 return literal(_new_text)
1389 return _new_text
1391 return _new_text
1390
1392
1391
1393
1392 def urlify_commits(text_, repo_name):
1394 def urlify_commits(text_, repo_name):
1393 """
1395 """
1394 Extract commit ids from text and make link from them
1396 Extract commit ids from text and make link from them
1395
1397
1396 :param text_:
1398 :param text_:
1397 :param repo_name: repo name to build the URL with
1399 :param repo_name: repo name to build the URL with
1398 """
1400 """
1399
1401
1400 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1402 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1401
1403
1402 def url_func(match_obj):
1404 def url_func(match_obj):
1403 commit_id = match_obj.groups()[1]
1405 commit_id = match_obj.groups()[1]
1404 pref = match_obj.groups()[0]
1406 pref = match_obj.groups()[0]
1405 suf = match_obj.groups()[2]
1407 suf = match_obj.groups()[2]
1406
1408
1407 tmpl = (
1409 tmpl = (
1408 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1410 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1409 '%(commit_id)s</a>%(suf)s'
1411 '%(commit_id)s</a>%(suf)s'
1410 )
1412 )
1411 return tmpl % {
1413 return tmpl % {
1412 'pref': pref,
1414 'pref': pref,
1413 'cls': 'revision-link',
1415 'cls': 'revision-link',
1414 'url': route_url(
1416 'url': route_url(
1415 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1417 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1416 'commit_id': commit_id,
1418 'commit_id': commit_id,
1417 'suf': suf,
1419 'suf': suf,
1418 'hovercard_alt': 'Commit: {}'.format(commit_id),
1420 'hovercard_alt': 'Commit: {}'.format(commit_id),
1419 'hovercard_url': route_url(
1421 'hovercard_url': route_url(
1420 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1422 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1421 }
1423 }
1422
1424
1423 new_text = url_pat.sub(url_func, text_)
1425 new_text = url_pat.sub(url_func, text_)
1424
1426
1425 return new_text
1427 return new_text
1426
1428
1427
1429
1428 def _process_url_func(match_obj, repo_name, uid, entry,
1430 def _process_url_func(match_obj, repo_name, uid, entry,
1429 return_raw_data=False, link_format='html'):
1431 return_raw_data=False, link_format='html'):
1430 pref = ''
1432 pref = ''
1431 if match_obj.group().startswith(' '):
1433 if match_obj.group().startswith(' '):
1432 pref = ' '
1434 pref = ' '
1433
1435
1434 issue_id = ''.join(match_obj.groups())
1436 issue_id = ''.join(match_obj.groups())
1435
1437
1436 if link_format == 'html':
1438 if link_format == 'html':
1437 tmpl = (
1439 tmpl = (
1438 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1440 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1439 '%(issue-prefix)s%(id-repr)s'
1441 '%(issue-prefix)s%(id-repr)s'
1440 '</a>')
1442 '</a>')
1441 elif link_format == 'html+hovercard':
1443 elif link_format == 'html+hovercard':
1442 tmpl = (
1444 tmpl = (
1443 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1445 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1444 '%(issue-prefix)s%(id-repr)s'
1446 '%(issue-prefix)s%(id-repr)s'
1445 '</a>')
1447 '</a>')
1446 elif link_format in ['rst', 'rst+hovercard']:
1448 elif link_format in ['rst', 'rst+hovercard']:
1447 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1449 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1448 elif link_format in ['markdown', 'markdown+hovercard']:
1450 elif link_format in ['markdown', 'markdown+hovercard']:
1449 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1451 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1450 else:
1452 else:
1451 raise ValueError('Bad link_format:{}'.format(link_format))
1453 raise ValueError('Bad link_format:{}'.format(link_format))
1452
1454
1453 (repo_name_cleaned,
1455 (repo_name_cleaned,
1454 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1456 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1455
1457
1456 # variables replacement
1458 # variables replacement
1457 named_vars = {
1459 named_vars = {
1458 'id': issue_id,
1460 'id': issue_id,
1459 'repo': repo_name,
1461 'repo': repo_name,
1460 'repo_name': repo_name_cleaned,
1462 'repo_name': repo_name_cleaned,
1461 'group_name': parent_group_name,
1463 'group_name': parent_group_name,
1462 # set dummy keys so we always have them
1464 # set dummy keys so we always have them
1463 'hostname': '',
1465 'hostname': '',
1464 'netloc': '',
1466 'netloc': '',
1465 'scheme': ''
1467 'scheme': ''
1466 }
1468 }
1467
1469
1468 request = get_current_request()
1470 request = get_current_request()
1469 if request:
1471 if request:
1470 # exposes, hostname, netloc, scheme
1472 # exposes, hostname, netloc, scheme
1471 host_data = get_host_info(request)
1473 host_data = get_host_info(request)
1472 named_vars.update(host_data)
1474 named_vars.update(host_data)
1473
1475
1474 # named regex variables
1476 # named regex variables
1475 named_vars.update(match_obj.groupdict())
1477 named_vars.update(match_obj.groupdict())
1476 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1478 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1477 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1479 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1478 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1480 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1479
1481
1480 def quote_cleaner(input_str):
1482 def quote_cleaner(input_str):
1481 """Remove quotes as it's HTML"""
1483 """Remove quotes as it's HTML"""
1482 return input_str.replace('"', '')
1484 return input_str.replace('"', '')
1483
1485
1484 data = {
1486 data = {
1485 'pref': pref,
1487 'pref': pref,
1486 'cls': quote_cleaner('issue-tracker-link'),
1488 'cls': quote_cleaner('issue-tracker-link'),
1487 'url': quote_cleaner(_url),
1489 'url': quote_cleaner(_url),
1488 'id-repr': issue_id,
1490 'id-repr': issue_id,
1489 'issue-prefix': entry['pref'],
1491 'issue-prefix': entry['pref'],
1490 'serv': entry['url'],
1492 'serv': entry['url'],
1491 'title': desc,
1493 'title': desc,
1492 'hovercard_url': hovercard_url
1494 'hovercard_url': hovercard_url
1493 }
1495 }
1494
1496
1495 if return_raw_data:
1497 if return_raw_data:
1496 return {
1498 return {
1497 'id': issue_id,
1499 'id': issue_id,
1498 'url': _url
1500 'url': _url
1499 }
1501 }
1500 return tmpl % data
1502 return tmpl % data
1501
1503
1502
1504
1503 def get_active_pattern_entries(repo_name):
1505 def get_active_pattern_entries(repo_name):
1504 repo = None
1506 repo = None
1505 if repo_name:
1507 if repo_name:
1506 # Retrieving repo_name to avoid invalid repo_name to explode on
1508 # Retrieving repo_name to avoid invalid repo_name to explode on
1507 # IssueTrackerSettingsModel but still passing invalid name further down
1509 # IssueTrackerSettingsModel but still passing invalid name further down
1508 repo = Repository.get_by_repo_name(repo_name, cache=True)
1510 repo = Repository.get_by_repo_name(repo_name, cache=True)
1509
1511
1510 settings_model = IssueTrackerSettingsModel(repo=repo)
1512 settings_model = IssueTrackerSettingsModel(repo=repo)
1511 active_entries = settings_model.get_settings(cache=True)
1513 active_entries = settings_model.get_settings(cache=True)
1512 return active_entries
1514 return active_entries
1513
1515
1514
1516
1515 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1517 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1516
1518
1517 allowed_formats = ['html', 'rst', 'markdown',
1519 allowed_formats = ['html', 'rst', 'markdown',
1518 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1520 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1519 if link_format not in allowed_formats:
1521 if link_format not in allowed_formats:
1520 raise ValueError('Link format can be only one of:{} got {}'.format(
1522 raise ValueError('Link format can be only one of:{} got {}'.format(
1521 allowed_formats, link_format))
1523 allowed_formats, link_format))
1522
1524
1523 active_entries = active_entries or get_active_pattern_entries(repo_name)
1525 active_entries = active_entries or get_active_pattern_entries(repo_name)
1524 issues_data = []
1526 issues_data = []
1525 new_text = text_string
1527 new_text = text_string
1526
1528
1527 log.debug('Got %s entries to process', len(active_entries))
1529 log.debug('Got %s entries to process', len(active_entries))
1528 for uid, entry in active_entries.items():
1530 for uid, entry in active_entries.items():
1529 log.debug('found issue tracker entry with uid %s', uid)
1531 log.debug('found issue tracker entry with uid %s', uid)
1530
1532
1531 if not (entry['pat'] and entry['url']):
1533 if not (entry['pat'] and entry['url']):
1532 log.debug('skipping due to missing data')
1534 log.debug('skipping due to missing data')
1533 continue
1535 continue
1534
1536
1535 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1537 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1536 uid, entry['pat'], entry['url'], entry['pref'])
1538 uid, entry['pat'], entry['url'], entry['pref'])
1537
1539
1538 try:
1540 try:
1539 pattern = re.compile(r'%s' % entry['pat'])
1541 pattern = re.compile(r'%s' % entry['pat'])
1540 except re.error:
1542 except re.error:
1541 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1543 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1542 continue
1544 continue
1543
1545
1544 data_func = partial(
1546 data_func = partial(
1545 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1547 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1546 return_raw_data=True)
1548 return_raw_data=True)
1547
1549
1548 for match_obj in pattern.finditer(text_string):
1550 for match_obj in pattern.finditer(text_string):
1549 issues_data.append(data_func(match_obj))
1551 issues_data.append(data_func(match_obj))
1550
1552
1551 url_func = partial(
1553 url_func = partial(
1552 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1554 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1553 link_format=link_format)
1555 link_format=link_format)
1554
1556
1555 new_text = pattern.sub(url_func, new_text)
1557 new_text = pattern.sub(url_func, new_text)
1556 log.debug('processed prefix:uid `%s`', uid)
1558 log.debug('processed prefix:uid `%s`', uid)
1557
1559
1558 # finally use global replace, eg !123 -> pr-link, those will not catch
1560 # finally use global replace, eg !123 -> pr-link, those will not catch
1559 # if already similar pattern exists
1561 # if already similar pattern exists
1560 server_url = '${scheme}://${netloc}'
1562 server_url = '${scheme}://${netloc}'
1561 pr_entry = {
1563 pr_entry = {
1562 'pref': '!',
1564 'pref': '!',
1563 'url': server_url + '/_admin/pull-requests/${id}',
1565 'url': server_url + '/_admin/pull-requests/${id}',
1564 'desc': 'Pull Request !${id}',
1566 'desc': 'Pull Request !${id}',
1565 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1567 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1566 }
1568 }
1567 pr_url_func = partial(
1569 pr_url_func = partial(
1568 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1570 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1569 link_format=link_format+'+hovercard')
1571 link_format=link_format+'+hovercard')
1570 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1572 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1571 log.debug('processed !pr pattern')
1573 log.debug('processed !pr pattern')
1572
1574
1573 return new_text, issues_data
1575 return new_text, issues_data
1574
1576
1575
1577
1576 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1578 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1577 """
1579 """
1578 Parses given text message and makes proper links.
1580 Parses given text message and makes proper links.
1579 issues are linked to given issue-server, and rest is a commit link
1581 issues are linked to given issue-server, and rest is a commit link
1580 """
1582 """
1581 def escaper(_text):
1583 def escaper(_text):
1582 return _text.replace('<', '&lt;').replace('>', '&gt;')
1584 return _text.replace('<', '&lt;').replace('>', '&gt;')
1583
1585
1584 new_text = escaper(commit_text)
1586 new_text = escaper(commit_text)
1585
1587
1586 # extract http/https links and make them real urls
1588 # extract http/https links and make them real urls
1587 new_text = urlify_text(new_text, safe=False)
1589 new_text = urlify_text(new_text, safe=False)
1588
1590
1589 # urlify commits - extract commit ids and make link out of them, if we have
1591 # urlify commits - extract commit ids and make link out of them, if we have
1590 # the scope of repository present.
1592 # the scope of repository present.
1591 if repository:
1593 if repository:
1592 new_text = urlify_commits(new_text, repository)
1594 new_text = urlify_commits(new_text, repository)
1593
1595
1594 # process issue tracker patterns
1596 # process issue tracker patterns
1595 new_text, issues = process_patterns(new_text, repository or '',
1597 new_text, issues = process_patterns(new_text, repository or '',
1596 active_entries=active_pattern_entries)
1598 active_entries=active_pattern_entries)
1597
1599
1598 return literal(new_text)
1600 return literal(new_text)
1599
1601
1600
1602
1601 def render_binary(repo_name, file_obj):
1603 def render_binary(repo_name, file_obj):
1602 """
1604 """
1603 Choose how to render a binary file
1605 Choose how to render a binary file
1604 """
1606 """
1605
1607
1606 filename = file_obj.name
1608 filename = file_obj.name
1607
1609
1608 # images
1610 # images
1609 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1611 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1610 if fnmatch.fnmatch(filename, pat=ext):
1612 if fnmatch.fnmatch(filename, pat=ext):
1611 alt = escape(filename)
1613 alt = escape(filename)
1612 src = route_path(
1614 src = route_path(
1613 'repo_file_raw', repo_name=repo_name,
1615 'repo_file_raw', repo_name=repo_name,
1614 commit_id=file_obj.commit.raw_id,
1616 commit_id=file_obj.commit.raw_id,
1615 f_path=file_obj.path)
1617 f_path=file_obj.path)
1616 return literal(
1618 return literal(
1617 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1619 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1618
1620
1619
1621
1620 def renderer_from_filename(filename, exclude=None):
1622 def renderer_from_filename(filename, exclude=None):
1621 """
1623 """
1622 choose a renderer based on filename, this works only for text based files
1624 choose a renderer based on filename, this works only for text based files
1623 """
1625 """
1624
1626
1625 # ipython
1627 # ipython
1626 for ext in ['*.ipynb']:
1628 for ext in ['*.ipynb']:
1627 if fnmatch.fnmatch(filename, pat=ext):
1629 if fnmatch.fnmatch(filename, pat=ext):
1628 return 'jupyter'
1630 return 'jupyter'
1629
1631
1630 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1632 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1631 if is_markup:
1633 if is_markup:
1632 return is_markup
1634 return is_markup
1633 return None
1635 return None
1634
1636
1635
1637
1636 def render(source, renderer='rst', mentions=False, relative_urls=None,
1638 def render(source, renderer='rst', mentions=False, relative_urls=None,
1637 repo_name=None):
1639 repo_name=None):
1638
1640
1639 def maybe_convert_relative_links(html_source):
1641 def maybe_convert_relative_links(html_source):
1640 if relative_urls:
1642 if relative_urls:
1641 return relative_links(html_source, relative_urls)
1643 return relative_links(html_source, relative_urls)
1642 return html_source
1644 return html_source
1643
1645
1644 if renderer == 'plain':
1646 if renderer == 'plain':
1645 return literal(
1647 return literal(
1646 MarkupRenderer.plain(source, leading_newline=False))
1648 MarkupRenderer.plain(source, leading_newline=False))
1647
1649
1648 elif renderer == 'rst':
1650 elif renderer == 'rst':
1649 if repo_name:
1651 if repo_name:
1650 # process patterns on comments if we pass in repo name
1652 # process patterns on comments if we pass in repo name
1651 source, issues = process_patterns(
1653 source, issues = process_patterns(
1652 source, repo_name, link_format='rst')
1654 source, repo_name, link_format='rst')
1653
1655
1654 return literal(
1656 return literal(
1655 '<div class="rst-block">%s</div>' %
1657 '<div class="rst-block">%s</div>' %
1656 maybe_convert_relative_links(
1658 maybe_convert_relative_links(
1657 MarkupRenderer.rst(source, mentions=mentions)))
1659 MarkupRenderer.rst(source, mentions=mentions)))
1658
1660
1659 elif renderer == 'markdown':
1661 elif renderer == 'markdown':
1660 if repo_name:
1662 if repo_name:
1661 # process patterns on comments if we pass in repo name
1663 # process patterns on comments if we pass in repo name
1662 source, issues = process_patterns(
1664 source, issues = process_patterns(
1663 source, repo_name, link_format='markdown')
1665 source, repo_name, link_format='markdown')
1664
1666
1665 return literal(
1667 return literal(
1666 '<div class="markdown-block">%s</div>' %
1668 '<div class="markdown-block">%s</div>' %
1667 maybe_convert_relative_links(
1669 maybe_convert_relative_links(
1668 MarkupRenderer.markdown(source, flavored=True,
1670 MarkupRenderer.markdown(source, flavored=True,
1669 mentions=mentions)))
1671 mentions=mentions)))
1670
1672
1671 elif renderer == 'jupyter':
1673 elif renderer == 'jupyter':
1672 return literal(
1674 return literal(
1673 '<div class="ipynb">%s</div>' %
1675 '<div class="ipynb">%s</div>' %
1674 maybe_convert_relative_links(
1676 maybe_convert_relative_links(
1675 MarkupRenderer.jupyter(source)))
1677 MarkupRenderer.jupyter(source)))
1676
1678
1677 # None means just show the file-source
1679 # None means just show the file-source
1678 return None
1680 return None
1679
1681
1680
1682
1681 def commit_status(repo, commit_id):
1683 def commit_status(repo, commit_id):
1682 return ChangesetStatusModel().get_status(repo, commit_id)
1684 return ChangesetStatusModel().get_status(repo, commit_id)
1683
1685
1684
1686
1685 def commit_status_lbl(commit_status):
1687 def commit_status_lbl(commit_status):
1686 return dict(ChangesetStatus.STATUSES).get(commit_status)
1688 return dict(ChangesetStatus.STATUSES).get(commit_status)
1687
1689
1688
1690
1689 def commit_time(repo_name, commit_id):
1691 def commit_time(repo_name, commit_id):
1690 repo = Repository.get_by_repo_name(repo_name)
1692 repo = Repository.get_by_repo_name(repo_name)
1691 commit = repo.get_commit(commit_id=commit_id)
1693 commit = repo.get_commit(commit_id=commit_id)
1692 return commit.date
1694 return commit.date
1693
1695
1694
1696
1695 def get_permission_name(key):
1697 def get_permission_name(key):
1696 return dict(Permission.PERMS).get(key)
1698 return dict(Permission.PERMS).get(key)
1697
1699
1698
1700
1699 def journal_filter_help(request):
1701 def journal_filter_help(request):
1700 _ = request.translate
1702 _ = request.translate
1701 from rhodecode.lib.audit_logger import ACTIONS
1703 from rhodecode.lib.audit_logger import ACTIONS
1702 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1704 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1703
1705
1704 return _(
1706 return _(
1705 'Example filter terms:\n' +
1707 'Example filter terms:\n' +
1706 ' repository:vcs\n' +
1708 ' repository:vcs\n' +
1707 ' username:marcin\n' +
1709 ' username:marcin\n' +
1708 ' username:(NOT marcin)\n' +
1710 ' username:(NOT marcin)\n' +
1709 ' action:*push*\n' +
1711 ' action:*push*\n' +
1710 ' ip:127.0.0.1\n' +
1712 ' ip:127.0.0.1\n' +
1711 ' date:20120101\n' +
1713 ' date:20120101\n' +
1712 ' date:[20120101100000 TO 20120102]\n' +
1714 ' date:[20120101100000 TO 20120102]\n' +
1713 '\n' +
1715 '\n' +
1714 'Actions: {actions}\n' +
1716 'Actions: {actions}\n' +
1715 '\n' +
1717 '\n' +
1716 'Generate wildcards using \'*\' character:\n' +
1718 'Generate wildcards using \'*\' character:\n' +
1717 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1719 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1718 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1720 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1719 '\n' +
1721 '\n' +
1720 'Optional AND / OR operators in queries\n' +
1722 'Optional AND / OR operators in queries\n' +
1721 ' "repository:vcs OR repository:test"\n' +
1723 ' "repository:vcs OR repository:test"\n' +
1722 ' "username:test AND repository:test*"\n'
1724 ' "username:test AND repository:test*"\n'
1723 ).format(actions=actions)
1725 ).format(actions=actions)
1724
1726
1725
1727
1726 def not_mapped_error(repo_name):
1728 def not_mapped_error(repo_name):
1727 from rhodecode.translation import _
1729 from rhodecode.translation import _
1728 flash(_('%s repository is not mapped to db perhaps'
1730 flash(_('%s repository is not mapped to db perhaps'
1729 ' it was created or renamed from the filesystem'
1731 ' it was created or renamed from the filesystem'
1730 ' please run the application again'
1732 ' please run the application again'
1731 ' in order to rescan repositories') % repo_name, category='error')
1733 ' in order to rescan repositories') % repo_name, category='error')
1732
1734
1733
1735
1734 def ip_range(ip_addr):
1736 def ip_range(ip_addr):
1735 from rhodecode.model.db import UserIpMap
1737 from rhodecode.model.db import UserIpMap
1736 s, e = UserIpMap._get_ip_range(ip_addr)
1738 s, e = UserIpMap._get_ip_range(ip_addr)
1737 return '%s - %s' % (s, e)
1739 return '%s - %s' % (s, e)
1738
1740
1739
1741
1740 def form(url, method='post', needs_csrf_token=True, **attrs):
1742 def form(url, method='post', needs_csrf_token=True, **attrs):
1741 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1743 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1742 if method.lower() != 'get' and needs_csrf_token:
1744 if method.lower() != 'get' and needs_csrf_token:
1743 raise Exception(
1745 raise Exception(
1744 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1746 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1745 'CSRF token. If the endpoint does not require such token you can ' +
1747 'CSRF token. If the endpoint does not require such token you can ' +
1746 'explicitly set the parameter needs_csrf_token to false.')
1748 'explicitly set the parameter needs_csrf_token to false.')
1747
1749
1748 return insecure_form(url, method=method, **attrs)
1750 return insecure_form(url, method=method, **attrs)
1749
1751
1750
1752
1751 def secure_form(form_url, method="POST", multipart=False, **attrs):
1753 def secure_form(form_url, method="POST", multipart=False, **attrs):
1752 """Start a form tag that points the action to an url. This
1754 """Start a form tag that points the action to an url. This
1753 form tag will also include the hidden field containing
1755 form tag will also include the hidden field containing
1754 the auth token.
1756 the auth token.
1755
1757
1756 The url options should be given either as a string, or as a
1758 The url options should be given either as a string, or as a
1757 ``url()`` function. The method for the form defaults to POST.
1759 ``url()`` function. The method for the form defaults to POST.
1758
1760
1759 Options:
1761 Options:
1760
1762
1761 ``multipart``
1763 ``multipart``
1762 If set to True, the enctype is set to "multipart/form-data".
1764 If set to True, the enctype is set to "multipart/form-data".
1763 ``method``
1765 ``method``
1764 The method to use when submitting the form, usually either
1766 The method to use when submitting the form, usually either
1765 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1767 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1766 hidden input with name _method is added to simulate the verb
1768 hidden input with name _method is added to simulate the verb
1767 over POST.
1769 over POST.
1768
1770
1769 """
1771 """
1770
1772
1771 if 'request' in attrs:
1773 if 'request' in attrs:
1772 session = attrs['request'].session
1774 session = attrs['request'].session
1773 del attrs['request']
1775 del attrs['request']
1774 else:
1776 else:
1775 raise ValueError(
1777 raise ValueError(
1776 'Calling this form requires request= to be passed as argument')
1778 'Calling this form requires request= to be passed as argument')
1777
1779
1778 _form = insecure_form(form_url, method, multipart, **attrs)
1780 _form = insecure_form(form_url, method, multipart, **attrs)
1779 token = literal(
1781 token = literal(
1780 '<input type="hidden" name="{}" value="{}">'.format(
1782 '<input type="hidden" name="{}" value="{}">'.format(
1781 csrf_token_key, get_csrf_token(session)))
1783 csrf_token_key, get_csrf_token(session)))
1782
1784
1783 return literal("%s\n%s" % (_form, token))
1785 return literal("%s\n%s" % (_form, token))
1784
1786
1785
1787
1786 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1788 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1787 select_html = select(name, selected, options, **attrs)
1789 select_html = select(name, selected, options, **attrs)
1788
1790
1789 select2 = """
1791 select2 = """
1790 <script>
1792 <script>
1791 $(document).ready(function() {
1793 $(document).ready(function() {
1792 $('#%s').select2({
1794 $('#%s').select2({
1793 containerCssClass: 'drop-menu %s',
1795 containerCssClass: 'drop-menu %s',
1794 dropdownCssClass: 'drop-menu-dropdown',
1796 dropdownCssClass: 'drop-menu-dropdown',
1795 dropdownAutoWidth: true%s
1797 dropdownAutoWidth: true%s
1796 });
1798 });
1797 });
1799 });
1798 </script>
1800 </script>
1799 """
1801 """
1800
1802
1801 filter_option = """,
1803 filter_option = """,
1802 minimumResultsForSearch: -1
1804 minimumResultsForSearch: -1
1803 """
1805 """
1804 input_id = attrs.get('id') or name
1806 input_id = attrs.get('id') or name
1805 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1807 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1806 filter_enabled = "" if enable_filter else filter_option
1808 filter_enabled = "" if enable_filter else filter_option
1807 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1809 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1808
1810
1809 return literal(select_html+select_script)
1811 return literal(select_html+select_script)
1810
1812
1811
1813
1812 def get_visual_attr(tmpl_context_var, attr_name):
1814 def get_visual_attr(tmpl_context_var, attr_name):
1813 """
1815 """
1814 A safe way to get a variable from visual variable of template context
1816 A safe way to get a variable from visual variable of template context
1815
1817
1816 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1818 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1817 :param attr_name: name of the attribute we fetch from the c.visual
1819 :param attr_name: name of the attribute we fetch from the c.visual
1818 """
1820 """
1819 visual = getattr(tmpl_context_var, 'visual', None)
1821 visual = getattr(tmpl_context_var, 'visual', None)
1820 if not visual:
1822 if not visual:
1821 return
1823 return
1822 else:
1824 else:
1823 return getattr(visual, attr_name, None)
1825 return getattr(visual, attr_name, None)
1824
1826
1825
1827
1826 def get_last_path_part(file_node):
1828 def get_last_path_part(file_node):
1827 if not file_node.path:
1829 if not file_node.path:
1828 return u'/'
1830 return u'/'
1829
1831
1830 path = safe_unicode(file_node.path.split('/')[-1])
1832 path = safe_unicode(file_node.path.split('/')[-1])
1831 return u'../' + path
1833 return u'../' + path
1832
1834
1833
1835
1834 def route_url(*args, **kwargs):
1836 def route_url(*args, **kwargs):
1835 """
1837 """
1836 Wrapper around pyramids `route_url` (fully qualified url) function.
1838 Wrapper around pyramids `route_url` (fully qualified url) function.
1837 """
1839 """
1838 req = get_current_request()
1840 req = get_current_request()
1839 return req.route_url(*args, **kwargs)
1841 return req.route_url(*args, **kwargs)
1840
1842
1841
1843
1842 def route_path(*args, **kwargs):
1844 def route_path(*args, **kwargs):
1843 """
1845 """
1844 Wrapper around pyramids `route_path` function.
1846 Wrapper around pyramids `route_path` function.
1845 """
1847 """
1846 req = get_current_request()
1848 req = get_current_request()
1847 return req.route_path(*args, **kwargs)
1849 return req.route_path(*args, **kwargs)
1848
1850
1849
1851
1850 def route_path_or_none(*args, **kwargs):
1852 def route_path_or_none(*args, **kwargs):
1851 try:
1853 try:
1852 return route_path(*args, **kwargs)
1854 return route_path(*args, **kwargs)
1853 except KeyError:
1855 except KeyError:
1854 return None
1856 return None
1855
1857
1856
1858
1857 def current_route_path(request, **kw):
1859 def current_route_path(request, **kw):
1858 new_args = request.GET.mixed()
1860 new_args = request.GET.mixed()
1859 new_args.update(kw)
1861 new_args.update(kw)
1860 return request.current_route_path(_query=new_args)
1862 return request.current_route_path(_query=new_args)
1861
1863
1862
1864
1863 def curl_api_example(method, args):
1865 def curl_api_example(method, args):
1864 args_json = json.dumps(OrderedDict([
1866 args_json = json.dumps(OrderedDict([
1865 ('id', 1),
1867 ('id', 1),
1866 ('auth_token', 'SECRET'),
1868 ('auth_token', 'SECRET'),
1867 ('method', method),
1869 ('method', method),
1868 ('args', args)
1870 ('args', args)
1869 ]))
1871 ]))
1870
1872
1871 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1873 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1872 api_url=route_url('apiv2'),
1874 api_url=route_url('apiv2'),
1873 args_json=args_json
1875 args_json=args_json
1874 )
1876 )
1875
1877
1876
1878
1877 def api_call_example(method, args):
1879 def api_call_example(method, args):
1878 """
1880 """
1879 Generates an API call example via CURL
1881 Generates an API call example via CURL
1880 """
1882 """
1881 curl_call = curl_api_example(method, args)
1883 curl_call = curl_api_example(method, args)
1882
1884
1883 return literal(
1885 return literal(
1884 curl_call +
1886 curl_call +
1885 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1887 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1886 "and needs to be of `api calls` role."
1888 "and needs to be of `api calls` role."
1887 .format(token_url=route_url('my_account_auth_tokens')))
1889 .format(token_url=route_url('my_account_auth_tokens')))
1888
1890
1889
1891
1890 def notification_description(notification, request):
1892 def notification_description(notification, request):
1891 """
1893 """
1892 Generate notification human readable description based on notification type
1894 Generate notification human readable description based on notification type
1893 """
1895 """
1894 from rhodecode.model.notification import NotificationModel
1896 from rhodecode.model.notification import NotificationModel
1895 return NotificationModel().make_description(
1897 return NotificationModel().make_description(
1896 notification, translate=request.translate)
1898 notification, translate=request.translate)
1897
1899
1898
1900
1899 def go_import_header(request, db_repo=None):
1901 def go_import_header(request, db_repo=None):
1900 """
1902 """
1901 Creates a header for go-import functionality in Go Lang
1903 Creates a header for go-import functionality in Go Lang
1902 """
1904 """
1903
1905
1904 if not db_repo:
1906 if not db_repo:
1905 return
1907 return
1906 if 'go-get' not in request.GET:
1908 if 'go-get' not in request.GET:
1907 return
1909 return
1908
1910
1909 clone_url = db_repo.clone_url()
1911 clone_url = db_repo.clone_url()
1910 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1912 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1911 # we have a repo and go-get flag,
1913 # we have a repo and go-get flag,
1912 return literal('<meta name="go-import" content="{} {} {}">'.format(
1914 return literal('<meta name="go-import" content="{} {} {}">'.format(
1913 prefix, db_repo.repo_type, clone_url))
1915 prefix, db_repo.repo_type, clone_url))
1914
1916
1915
1917
1916 def reviewer_as_json(*args, **kwargs):
1918 def reviewer_as_json(*args, **kwargs):
1917 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1919 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1918 return _reviewer_as_json(*args, **kwargs)
1920 return _reviewer_as_json(*args, **kwargs)
1919
1921
1920
1922
1921 def get_repo_view_type(request):
1923 def get_repo_view_type(request):
1922 route_name = request.matched_route.name
1924 route_name = request.matched_route.name
1923 route_to_view_type = {
1925 route_to_view_type = {
1924 'repo_changelog': 'commits',
1926 'repo_changelog': 'commits',
1925 'repo_commits': 'commits',
1927 'repo_commits': 'commits',
1926 'repo_files': 'files',
1928 'repo_files': 'files',
1927 'repo_summary': 'summary',
1929 'repo_summary': 'summary',
1928 'repo_commit': 'commit'
1930 'repo_commit': 'commit'
1929 }
1931 }
1930
1932
1931 return route_to_view_type.get(route_name)
1933 return route_to_view_type.get(route_name)
1932
1934
1933
1935
1934 def is_active(menu_entry, selected):
1936 def is_active(menu_entry, selected):
1935 """
1937 """
1936 Returns active class for selecting menus in templates
1938 Returns active class for selecting menus in templates
1937 <li class=${h.is_active('settings', current_active)}></li>
1939 <li class=${h.is_active('settings', current_active)}></li>
1938 """
1940 """
1939 if not isinstance(menu_entry, list):
1941 if not isinstance(menu_entry, list):
1940 menu_entry = [menu_entry]
1942 menu_entry = [menu_entry]
1941
1943
1942 if selected in menu_entry:
1944 if selected in menu_entry:
1943 return "active"
1945 return "active"
@@ -1,2937 +1,2985 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'variables';
8 @import 'variables';
9 @import 'bootstrap-variables';
9 @import 'bootstrap-variables';
10 @import 'form-bootstrap';
10 @import 'form-bootstrap';
11 @import 'codemirror';
11 @import 'codemirror';
12 @import 'legacy_code_styles';
12 @import 'legacy_code_styles';
13 @import 'readme-box';
13 @import 'readme-box';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29 @import 'tooltips';
29 @import 'tooltips';
30
30
31 //--- BASE ------------------//
31 //--- BASE ------------------//
32 .noscript-error {
32 .noscript-error {
33 top: 0;
33 top: 0;
34 left: 0;
34 left: 0;
35 width: 100%;
35 width: 100%;
36 z-index: 101;
36 z-index: 101;
37 text-align: center;
37 text-align: center;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 font-weight: @text-semibold-weight;
42 font-weight: @text-semibold-weight;
43 font-family: @text-semibold;
43 font-family: @text-semibold;
44 }
44 }
45
45
46 html {
46 html {
47 display: table;
47 display: table;
48 height: 100%;
48 height: 100%;
49 width: 100%;
49 width: 100%;
50 }
50 }
51
51
52 body {
52 body {
53 display: table-cell;
53 display: table-cell;
54 width: 100%;
54 width: 100%;
55 }
55 }
56
56
57 //--- LAYOUT ------------------//
57 //--- LAYOUT ------------------//
58
58
59 .hidden{
59 .hidden{
60 display: none !important;
60 display: none !important;
61 }
61 }
62
62
63 .box{
63 .box{
64 float: left;
64 float: left;
65 width: 100%;
65 width: 100%;
66 }
66 }
67
67
68 .browser-header {
68 .browser-header {
69 clear: both;
69 clear: both;
70 }
70 }
71 .main {
71 .main {
72 clear: both;
72 clear: both;
73 padding:0 0 @pagepadding;
73 padding:0 0 @pagepadding;
74 height: auto;
74 height: auto;
75
75
76 &:after { //clearfix
76 &:after { //clearfix
77 content:"";
77 content:"";
78 clear:both;
78 clear:both;
79 width:100%;
79 width:100%;
80 display:block;
80 display:block;
81 }
81 }
82 }
82 }
83
83
84 .action-link{
84 .action-link{
85 margin-left: @padding;
85 margin-left: @padding;
86 padding-left: @padding;
86 padding-left: @padding;
87 border-left: @border-thickness solid @border-default-color;
87 border-left: @border-thickness solid @border-default-color;
88 }
88 }
89
89
90 input + .action-link, .action-link.first{
90 input + .action-link, .action-link.first{
91 border-left: none;
91 border-left: none;
92 }
92 }
93
93
94 .action-link.last{
94 .action-link.last{
95 margin-right: @padding;
95 margin-right: @padding;
96 padding-right: @padding;
96 padding-right: @padding;
97 }
97 }
98
98
99 .action-link.active,
99 .action-link.active,
100 .action-link.active a{
100 .action-link.active a{
101 color: @grey4;
101 color: @grey4;
102 }
102 }
103
103
104 .action-link.disabled {
104 .action-link.disabled {
105 color: @grey4;
105 color: @grey4;
106 cursor: inherit;
106 cursor: inherit;
107 }
107 }
108
108
109
109
110 .clipboard-action {
110 .clipboard-action {
111 cursor: pointer;
111 cursor: pointer;
112 margin-left: 5px;
112 margin-left: 5px;
113
113
114 &:not(.no-grey) {
114 &:not(.no-grey) {
115
115
116 &:hover {
116 &:hover {
117 color: @grey2;
117 color: @grey2;
118 }
118 }
119 color: @grey4;
119 color: @grey4;
120 }
120 }
121 }
121 }
122
122
123 ul.simple-list{
123 ul.simple-list{
124 list-style: none;
124 list-style: none;
125 margin: 0;
125 margin: 0;
126 padding: 0;
126 padding: 0;
127 }
127 }
128
128
129 .main-content {
129 .main-content {
130 padding-bottom: @pagepadding;
130 padding-bottom: @pagepadding;
131 }
131 }
132
132
133 .wide-mode-wrapper {
133 .wide-mode-wrapper {
134 max-width:4000px !important;
134 max-width:4000px !important;
135 }
135 }
136
136
137 .wrapper {
137 .wrapper {
138 position: relative;
138 position: relative;
139 max-width: @wrapper-maxwidth;
139 max-width: @wrapper-maxwidth;
140 margin: 0 auto;
140 margin: 0 auto;
141 }
141 }
142
142
143 #content {
143 #content {
144 clear: both;
144 clear: both;
145 padding: 0 @contentpadding;
145 padding: 0 @contentpadding;
146 }
146 }
147
147
148 .advanced-settings-fields{
148 .advanced-settings-fields{
149 input{
149 input{
150 margin-left: @textmargin;
150 margin-left: @textmargin;
151 margin-right: @padding/2;
151 margin-right: @padding/2;
152 }
152 }
153 }
153 }
154
154
155 .cs_files_title {
155 .cs_files_title {
156 margin: @pagepadding 0 0;
156 margin: @pagepadding 0 0;
157 }
157 }
158
158
159 input.inline[type="file"] {
159 input.inline[type="file"] {
160 display: inline;
160 display: inline;
161 }
161 }
162
162
163 .error_page {
163 .error_page {
164 margin: 10% auto;
164 margin: 10% auto;
165
165
166 h1 {
166 h1 {
167 color: @grey2;
167 color: @grey2;
168 }
168 }
169
169
170 .alert {
170 .alert {
171 margin: @padding 0;
171 margin: @padding 0;
172 }
172 }
173
173
174 .error-branding {
174 .error-branding {
175 color: @grey4;
175 color: @grey4;
176 font-weight: @text-semibold-weight;
176 font-weight: @text-semibold-weight;
177 font-family: @text-semibold;
177 font-family: @text-semibold;
178 }
178 }
179
179
180 .error_message {
180 .error_message {
181 font-family: @text-regular;
181 font-family: @text-regular;
182 }
182 }
183
183
184 .sidebar {
184 .sidebar {
185 min-height: 275px;
185 min-height: 275px;
186 margin: 0;
186 margin: 0;
187 padding: 0 0 @sidebarpadding @sidebarpadding;
187 padding: 0 0 @sidebarpadding @sidebarpadding;
188 border: none;
188 border: none;
189 }
189 }
190
190
191 .main-content {
191 .main-content {
192 position: relative;
192 position: relative;
193 margin: 0 @sidebarpadding @sidebarpadding;
193 margin: 0 @sidebarpadding @sidebarpadding;
194 padding: 0 0 0 @sidebarpadding;
194 padding: 0 0 0 @sidebarpadding;
195 border-left: @border-thickness solid @grey5;
195 border-left: @border-thickness solid @grey5;
196
196
197 @media (max-width:767px) {
197 @media (max-width:767px) {
198 clear: both;
198 clear: both;
199 width: 100%;
199 width: 100%;
200 margin: 0;
200 margin: 0;
201 border: none;
201 border: none;
202 }
202 }
203 }
203 }
204
204
205 .inner-column {
205 .inner-column {
206 float: left;
206 float: left;
207 width: 29.75%;
207 width: 29.75%;
208 min-height: 150px;
208 min-height: 150px;
209 margin: @sidebarpadding 2% 0 0;
209 margin: @sidebarpadding 2% 0 0;
210 padding: 0 2% 0 0;
210 padding: 0 2% 0 0;
211 border-right: @border-thickness solid @grey5;
211 border-right: @border-thickness solid @grey5;
212
212
213 @media (max-width:767px) {
213 @media (max-width:767px) {
214 clear: both;
214 clear: both;
215 width: 100%;
215 width: 100%;
216 border: none;
216 border: none;
217 }
217 }
218
218
219 ul {
219 ul {
220 padding-left: 1.25em;
220 padding-left: 1.25em;
221 }
221 }
222
222
223 &:last-child {
223 &:last-child {
224 margin: @sidebarpadding 0 0;
224 margin: @sidebarpadding 0 0;
225 border: none;
225 border: none;
226 }
226 }
227
227
228 h4 {
228 h4 {
229 margin: 0 0 @padding;
229 margin: 0 0 @padding;
230 font-weight: @text-semibold-weight;
230 font-weight: @text-semibold-weight;
231 font-family: @text-semibold;
231 font-family: @text-semibold;
232 }
232 }
233 }
233 }
234 }
234 }
235 .error-page-logo {
235 .error-page-logo {
236 width: 130px;
236 width: 130px;
237 height: 160px;
237 height: 160px;
238 }
238 }
239
239
240 // HEADER
240 // HEADER
241 .header {
241 .header {
242
242
243 // TODO: johbo: Fix login pages, so that they work without a min-height
243 // TODO: johbo: Fix login pages, so that they work without a min-height
244 // for the header and then remove the min-height. I chose a smaller value
244 // for the header and then remove the min-height. I chose a smaller value
245 // intentionally here to avoid rendering issues in the main navigation.
245 // intentionally here to avoid rendering issues in the main navigation.
246 min-height: 49px;
246 min-height: 49px;
247 min-width: 1024px;
247 min-width: 1024px;
248
248
249 position: relative;
249 position: relative;
250 vertical-align: bottom;
250 vertical-align: bottom;
251 padding: 0 @header-padding;
251 padding: 0 @header-padding;
252 background-color: @grey1;
252 background-color: @grey1;
253 color: @grey5;
253 color: @grey5;
254
254
255 .title {
255 .title {
256 overflow: visible;
256 overflow: visible;
257 }
257 }
258
258
259 &:before,
259 &:before,
260 &:after {
260 &:after {
261 content: "";
261 content: "";
262 clear: both;
262 clear: both;
263 width: 100%;
263 width: 100%;
264 }
264 }
265
265
266 // TODO: johbo: Avoids breaking "Repositories" chooser
266 // TODO: johbo: Avoids breaking "Repositories" chooser
267 .select2-container .select2-choice .select2-arrow {
267 .select2-container .select2-choice .select2-arrow {
268 display: none;
268 display: none;
269 }
269 }
270 }
270 }
271
271
272 #header-inner {
272 #header-inner {
273 &.title {
273 &.title {
274 margin: 0;
274 margin: 0;
275 }
275 }
276 &:before,
276 &:before,
277 &:after {
277 &:after {
278 content: "";
278 content: "";
279 clear: both;
279 clear: both;
280 }
280 }
281 }
281 }
282
282
283 // Gists
283 // Gists
284 #files_data {
284 #files_data {
285 clear: both; //for firefox
285 clear: both; //for firefox
286 padding-top: 10px;
286 padding-top: 10px;
287 }
287 }
288
288
289 #gistid {
289 #gistid {
290 margin-right: @padding;
290 margin-right: @padding;
291 }
291 }
292
292
293 // Global Settings Editor
293 // Global Settings Editor
294 .textarea.editor {
294 .textarea.editor {
295 float: left;
295 float: left;
296 position: relative;
296 position: relative;
297 max-width: @texteditor-width;
297 max-width: @texteditor-width;
298
298
299 select {
299 select {
300 position: absolute;
300 position: absolute;
301 top:10px;
301 top:10px;
302 right:0;
302 right:0;
303 }
303 }
304
304
305 .CodeMirror {
305 .CodeMirror {
306 margin: 0;
306 margin: 0;
307 }
307 }
308
308
309 .help-block {
309 .help-block {
310 margin: 0 0 @padding;
310 margin: 0 0 @padding;
311 padding:.5em;
311 padding:.5em;
312 background-color: @grey6;
312 background-color: @grey6;
313 &.pre-formatting {
313 &.pre-formatting {
314 white-space: pre;
314 white-space: pre;
315 }
315 }
316 }
316 }
317 }
317 }
318
318
319 ul.auth_plugins {
319 ul.auth_plugins {
320 margin: @padding 0 @padding @legend-width;
320 margin: @padding 0 @padding @legend-width;
321 padding: 0;
321 padding: 0;
322
322
323 li {
323 li {
324 margin-bottom: @padding;
324 margin-bottom: @padding;
325 line-height: 1em;
325 line-height: 1em;
326 list-style-type: none;
326 list-style-type: none;
327
327
328 .auth_buttons .btn {
328 .auth_buttons .btn {
329 margin-right: @padding;
329 margin-right: @padding;
330 }
330 }
331
331
332 }
332 }
333 }
333 }
334
334
335
335
336 // My Account PR list
336 // My Account PR list
337
337
338 #show_closed {
338 #show_closed {
339 margin: 0 1em 0 0;
339 margin: 0 1em 0 0;
340 }
340 }
341
341
342 #pull_request_list_table {
342 #pull_request_list_table {
343 .closed {
343 .closed {
344 background-color: @grey6;
344 background-color: @grey6;
345 }
345 }
346
346
347 .state-creating,
347 .state-creating,
348 .state-updating,
348 .state-updating,
349 .state-merging
349 .state-merging
350 {
350 {
351 background-color: @grey6;
351 background-color: @grey6;
352 }
352 }
353
353
354 .td-status {
354 .td-status {
355 padding-left: .5em;
355 padding-left: .5em;
356 }
356 }
357 .log-container .truncate {
357 .log-container .truncate {
358 height: 2.75em;
358 height: 2.75em;
359 white-space: pre-line;
359 white-space: pre-line;
360 }
360 }
361 table.rctable .user {
361 table.rctable .user {
362 padding-left: 0;
362 padding-left: 0;
363 }
363 }
364 table.rctable {
364 table.rctable {
365 td.td-description,
365 td.td-description,
366 .rc-user {
366 .rc-user {
367 min-width: auto;
367 min-width: auto;
368 }
368 }
369 }
369 }
370 }
370 }
371
371
372 // Pull Requests
372 // Pull Requests
373
373
374 .pullrequests_section_head {
374 .pullrequests_section_head {
375 display: block;
375 display: block;
376 clear: both;
376 clear: both;
377 margin: @padding 0;
377 margin: @padding 0;
378 font-weight: @text-bold-weight;
378 font-weight: @text-bold-weight;
379 font-family: @text-bold;
379 font-family: @text-bold;
380 }
380 }
381
381
382 .pr-origininfo, .pr-targetinfo {
382 .pr-commit-flow {
383 position: relative;
383 position: relative;
384 font-weight: 600;
384
385
385 .tag {
386 .tag {
386 display: inline-block;
387 display: inline-block;
387 margin: 0 1em .5em 0;
388 margin: 0 1em .5em 0;
388 }
389 }
389
390
390 .clone-url {
391 .clone-url {
391 display: inline-block;
392 display: inline-block;
392 margin: 0 0 .5em 0;
393 margin: 0 0 .5em 0;
393 padding: 0;
394 padding: 0;
394 line-height: 1.2em;
395 line-height: 1.2em;
395 }
396 }
396 }
397 }
397
398
398 .pr-mergeinfo {
399 .pr-mergeinfo {
399 min-width: 95% !important;
400 min-width: 95% !important;
400 padding: 0 !important;
401 padding: 0 !important;
401 border: 0;
402 border: 0;
402 }
403 }
403 .pr-mergeinfo-copy {
404 .pr-mergeinfo-copy {
404 padding: 0 0;
405 padding: 0 0;
405 }
406 }
406
407
407 .pr-pullinfo {
408 .pr-pullinfo {
408 min-width: 95% !important;
409 min-width: 95% !important;
409 padding: 0 !important;
410 padding: 0 !important;
410 border: 0;
411 border: 0;
411 }
412 }
412 .pr-pullinfo-copy {
413 .pr-pullinfo-copy {
413 padding: 0 0;
414 padding: 0 0;
414 }
415 }
415
416
416
417 .pr-title-input {
417 .pr-title-input {
418 width: 80%;
418 width: 80%;
419 font-size: 1em;
419 font-size: 1em;
420 margin: 0 0 4px 0;
420 margin: 0 0 4px 0;
421 padding: 0;
421 padding: 0;
422 line-height: 1.7em;
422 line-height: 1.7em;
423 color: @text-color;
423 color: @text-color;
424 letter-spacing: .02em;
424 letter-spacing: .02em;
425 font-weight: @text-bold-weight;
425 font-weight: @text-bold-weight;
426 font-family: @text-bold;
426 font-family: @text-bold;
427
427
428 &:hover {
428 &:hover {
429 box-shadow: none;
429 box-shadow: none;
430 }
430 }
431 }
431 }
432
432
433 #pr-title {
433 #pr-title {
434 input {
434 input {
435 border: 1px transparent;
435 border: 1px transparent;
436 color: black;
436 color: black;
437 opacity: 1
437 opacity: 1
438 }
438 }
439 }
439 }
440
440
441 .pr-title-closed-tag {
442 font-size: 16px;
443 }
444
445 #pr-desc {
446 padding: 10px 0;
447
448 .markdown-block {
449 padding: 0;
450 margin-bottom: -30px;
451 }
452 }
453
441 #pullrequest_title {
454 #pullrequest_title {
442 width: 100%;
455 width: 100%;
443 box-sizing: border-box;
456 box-sizing: border-box;
444 }
457 }
445
458
446 #pr_open_message {
459 #pr_open_message {
447 border: @border-thickness solid #fff;
460 border: @border-thickness solid #fff;
448 border-radius: @border-radius;
461 border-radius: @border-radius;
449 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
462 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
450 text-align: left;
463 text-align: left;
451 overflow: hidden;
464 overflow: hidden;
452 }
465 }
453
466
467 .pr-details-title {
468 height: 16px
469 }
470
471 .pr-details-title-author-pref {
472 padding-right: 10px
473 }
474
475 .label-pr-detail {
476 display: table-cell;
477 width: 120px;
478 padding-top: 7.5px;
479 padding-bottom: 7.5px;
480 padding-right: 7.5px;
481 }
482
483 .source-details ul {
484 padding: 10px 16px;
485 }
486
487 .source-details-action {
488 color: @grey4;
489 font-size: 11px
490 }
491
454 .pr-submit-button {
492 .pr-submit-button {
455 float: right;
493 float: right;
456 margin: 0 0 0 5px;
494 margin: 0 0 0 5px;
457 }
495 }
458
496
459 .pr-spacing-container {
497 .pr-spacing-container {
460 padding: 20px;
498 padding: 20px;
461 clear: both
499 clear: both
462 }
500 }
463
501
464 #pr-description-input {
502 #pr-description-input {
465 margin-bottom: 0;
503 margin-bottom: 0;
466 }
504 }
467
505
468 .pr-description-label {
506 .pr-description-label {
469 vertical-align: top;
507 vertical-align: top;
470 }
508 }
471
509
510 #close_edit_pullrequest {
511 padding-left: 1em
512 }
513
514 #delete_pullrequest {
515 clear: inherit;
516 padding: 0
517 }
518
472 .perms_section_head {
519 .perms_section_head {
473 min-width: 625px;
520 min-width: 625px;
474
521
475 h2 {
522 h2 {
476 margin-bottom: 0;
523 margin-bottom: 0;
477 }
524 }
478
525
479 .label-checkbox {
526 .label-checkbox {
480 float: left;
527 float: left;
481 }
528 }
482
529
483 &.field {
530 &.field {
484 margin: @space 0 @padding;
531 margin: @space 0 @padding;
485 }
532 }
486
533
487 &:first-child.field {
534 &:first-child.field {
488 margin-top: 0;
535 margin-top: 0;
489
536
490 .label {
537 .label {
491 margin-top: 0;
538 margin-top: 0;
492 padding-top: 0;
539 padding-top: 0;
493 }
540 }
494
541
495 .radios {
542 .radios {
496 padding-top: 0;
543 padding-top: 0;
497 }
544 }
498 }
545 }
499
546
500 .radios {
547 .radios {
501 position: relative;
548 position: relative;
502 width: 505px;
549 width: 505px;
503 }
550 }
504 }
551 }
505
552
506 //--- MODULES ------------------//
553 //--- MODULES ------------------//
507
554
508
555
509 // Server Announcement
556 // Server Announcement
510 #server-announcement {
557 #server-announcement {
511 width: 95%;
558 width: 95%;
512 margin: @padding auto;
559 margin: @padding auto;
513 padding: @padding;
560 padding: @padding;
514 border-width: 2px;
561 border-width: 2px;
515 border-style: solid;
562 border-style: solid;
516 .border-radius(2px);
563 .border-radius(2px);
517 font-weight: @text-bold-weight;
564 font-weight: @text-bold-weight;
518 font-family: @text-bold;
565 font-family: @text-bold;
519
566
520 &.info { border-color: @alert4; background-color: @alert4-inner; }
567 &.info { border-color: @alert4; background-color: @alert4-inner; }
521 &.warning { border-color: @alert3; background-color: @alert3-inner; }
568 &.warning { border-color: @alert3; background-color: @alert3-inner; }
522 &.error { border-color: @alert2; background-color: @alert2-inner; }
569 &.error { border-color: @alert2; background-color: @alert2-inner; }
523 &.success { border-color: @alert1; background-color: @alert1-inner; }
570 &.success { border-color: @alert1; background-color: @alert1-inner; }
524 &.neutral { border-color: @grey3; background-color: @grey6; }
571 &.neutral { border-color: @grey3; background-color: @grey6; }
525 }
572 }
526
573
527 // Fixed Sidebar Column
574 // Fixed Sidebar Column
528 .sidebar-col-wrapper {
575 .sidebar-col-wrapper {
529 padding-left: @sidebar-all-width;
576 padding-left: @sidebar-all-width;
530
577
531 .sidebar {
578 .sidebar {
532 width: @sidebar-width;
579 width: @sidebar-width;
533 margin-left: -@sidebar-all-width;
580 margin-left: -@sidebar-all-width;
534 }
581 }
535 }
582 }
536
583
537 .sidebar-col-wrapper.scw-small {
584 .sidebar-col-wrapper.scw-small {
538 padding-left: @sidebar-small-all-width;
585 padding-left: @sidebar-small-all-width;
539
586
540 .sidebar {
587 .sidebar {
541 width: @sidebar-small-width;
588 width: @sidebar-small-width;
542 margin-left: -@sidebar-small-all-width;
589 margin-left: -@sidebar-small-all-width;
543 }
590 }
544 }
591 }
545
592
546
593
547 // FOOTER
594 // FOOTER
548 #footer {
595 #footer {
549 padding: 0;
596 padding: 0;
550 text-align: center;
597 text-align: center;
551 vertical-align: middle;
598 vertical-align: middle;
552 color: @grey2;
599 color: @grey2;
553 font-size: 11px;
600 font-size: 11px;
554
601
555 p {
602 p {
556 margin: 0;
603 margin: 0;
557 padding: 1em;
604 padding: 1em;
558 line-height: 1em;
605 line-height: 1em;
559 }
606 }
560
607
561 .server-instance { //server instance
608 .server-instance { //server instance
562 display: none;
609 display: none;
563 }
610 }
564
611
565 .title {
612 .title {
566 float: none;
613 float: none;
567 margin: 0 auto;
614 margin: 0 auto;
568 }
615 }
569 }
616 }
570
617
571 button.close {
618 button.close {
572 padding: 0;
619 padding: 0;
573 cursor: pointer;
620 cursor: pointer;
574 background: transparent;
621 background: transparent;
575 border: 0;
622 border: 0;
576 .box-shadow(none);
623 .box-shadow(none);
577 -webkit-appearance: none;
624 -webkit-appearance: none;
578 }
625 }
579
626
580 .close {
627 .close {
581 float: right;
628 float: right;
582 font-size: 21px;
629 font-size: 21px;
583 font-family: @text-bootstrap;
630 font-family: @text-bootstrap;
584 line-height: 1em;
631 line-height: 1em;
585 font-weight: bold;
632 font-weight: bold;
586 color: @grey2;
633 color: @grey2;
587
634
588 &:hover,
635 &:hover,
589 &:focus {
636 &:focus {
590 color: @grey1;
637 color: @grey1;
591 text-decoration: none;
638 text-decoration: none;
592 cursor: pointer;
639 cursor: pointer;
593 }
640 }
594 }
641 }
595
642
596 // GRID
643 // GRID
597 .sorting,
644 .sorting,
598 .sorting_desc,
645 .sorting_desc,
599 .sorting_asc {
646 .sorting_asc {
600 cursor: pointer;
647 cursor: pointer;
601 }
648 }
602 .sorting_desc:after {
649 .sorting_desc:after {
603 content: "\00A0\25B2";
650 content: "\00A0\25B2";
604 font-size: .75em;
651 font-size: .75em;
605 }
652 }
606 .sorting_asc:after {
653 .sorting_asc:after {
607 content: "\00A0\25BC";
654 content: "\00A0\25BC";
608 font-size: .68em;
655 font-size: .68em;
609 }
656 }
610
657
611
658
612 .user_auth_tokens {
659 .user_auth_tokens {
613
660
614 &.truncate {
661 &.truncate {
615 white-space: nowrap;
662 white-space: nowrap;
616 overflow: hidden;
663 overflow: hidden;
617 text-overflow: ellipsis;
664 text-overflow: ellipsis;
618 }
665 }
619
666
620 .fields .field .input {
667 .fields .field .input {
621 margin: 0;
668 margin: 0;
622 }
669 }
623
670
624 input#description {
671 input#description {
625 width: 100px;
672 width: 100px;
626 margin: 0;
673 margin: 0;
627 }
674 }
628
675
629 .drop-menu {
676 .drop-menu {
630 // TODO: johbo: Remove this, should work out of the box when
677 // TODO: johbo: Remove this, should work out of the box when
631 // having multiple inputs inline
678 // having multiple inputs inline
632 margin: 0 0 0 5px;
679 margin: 0 0 0 5px;
633 }
680 }
634 }
681 }
635 #user_list_table {
682 #user_list_table {
636 .closed {
683 .closed {
637 background-color: @grey6;
684 background-color: @grey6;
638 }
685 }
639 }
686 }
640
687
641
688
642 input, textarea {
689 input, textarea {
643 &.disabled {
690 &.disabled {
644 opacity: .5;
691 opacity: .5;
645 }
692 }
646
693
647 &:hover {
694 &:hover {
648 border-color: @grey3;
695 border-color: @grey3;
649 box-shadow: @button-shadow;
696 box-shadow: @button-shadow;
650 }
697 }
651
698
652 &:focus {
699 &:focus {
653 border-color: @rcblue;
700 border-color: @rcblue;
654 box-shadow: @button-shadow;
701 box-shadow: @button-shadow;
655 }
702 }
656 }
703 }
657
704
658 // remove extra padding in firefox
705 // remove extra padding in firefox
659 input::-moz-focus-inner { border:0; padding:0 }
706 input::-moz-focus-inner { border:0; padding:0 }
660
707
661 .adjacent input {
708 .adjacent input {
662 margin-bottom: @padding;
709 margin-bottom: @padding;
663 }
710 }
664
711
665 .permissions_boxes {
712 .permissions_boxes {
666 display: block;
713 display: block;
667 }
714 }
668
715
669 //FORMS
716 //FORMS
670
717
671 .medium-inline,
718 .medium-inline,
672 input#description.medium-inline {
719 input#description.medium-inline {
673 display: inline;
720 display: inline;
674 width: @medium-inline-input-width;
721 width: @medium-inline-input-width;
675 min-width: 100px;
722 min-width: 100px;
676 }
723 }
677
724
678 select {
725 select {
679 //reset
726 //reset
680 -webkit-appearance: none;
727 -webkit-appearance: none;
681 -moz-appearance: none;
728 -moz-appearance: none;
682
729
683 display: inline-block;
730 display: inline-block;
684 height: 28px;
731 height: 28px;
685 width: auto;
732 width: auto;
686 margin: 0 @padding @padding 0;
733 margin: 0 @padding @padding 0;
687 padding: 0 18px 0 8px;
734 padding: 0 18px 0 8px;
688 line-height:1em;
735 line-height:1em;
689 font-size: @basefontsize;
736 font-size: @basefontsize;
690 border: @border-thickness solid @grey5;
737 border: @border-thickness solid @grey5;
691 border-radius: @border-radius;
738 border-radius: @border-radius;
692 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
739 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
693 color: @grey4;
740 color: @grey4;
694 box-shadow: @button-shadow;
741 box-shadow: @button-shadow;
695
742
696 &:after {
743 &:after {
697 content: "\00A0\25BE";
744 content: "\00A0\25BE";
698 }
745 }
699
746
700 &:focus, &:hover {
747 &:focus, &:hover {
701 outline: none;
748 outline: none;
702 border-color: @grey4;
749 border-color: @grey4;
703 color: @rcdarkblue;
750 color: @rcdarkblue;
704 }
751 }
705 }
752 }
706
753
707 option {
754 option {
708 &:focus {
755 &:focus {
709 outline: none;
756 outline: none;
710 }
757 }
711 }
758 }
712
759
713 input,
760 input,
714 textarea {
761 textarea {
715 padding: @input-padding;
762 padding: @input-padding;
716 border: @input-border-thickness solid @border-highlight-color;
763 border: @input-border-thickness solid @border-highlight-color;
717 .border-radius (@border-radius);
764 .border-radius (@border-radius);
718 font-family: @text-light;
765 font-family: @text-light;
719 font-size: @basefontsize;
766 font-size: @basefontsize;
720
767
721 &.input-sm {
768 &.input-sm {
722 padding: 5px;
769 padding: 5px;
723 }
770 }
724
771
725 &#description {
772 &#description {
726 min-width: @input-description-minwidth;
773 min-width: @input-description-minwidth;
727 min-height: 1em;
774 min-height: 1em;
728 padding: 10px;
775 padding: 10px;
729 }
776 }
730 }
777 }
731
778
732 .field-sm {
779 .field-sm {
733 input,
780 input,
734 textarea {
781 textarea {
735 padding: 5px;
782 padding: 5px;
736 }
783 }
737 }
784 }
738
785
739 textarea {
786 textarea {
740 display: block;
787 display: block;
741 clear: both;
788 clear: both;
742 width: 100%;
789 width: 100%;
743 min-height: 100px;
790 min-height: 100px;
744 margin-bottom: @padding;
791 margin-bottom: @padding;
745 .box-sizing(border-box);
792 .box-sizing(border-box);
746 overflow: auto;
793 overflow: auto;
747 }
794 }
748
795
749 label {
796 label {
750 font-family: @text-light;
797 font-family: @text-light;
751 }
798 }
752
799
753 // GRAVATARS
800 // GRAVATARS
754 // centers gravatar on username to the right
801 // centers gravatar on username to the right
755
802
756 .gravatar {
803 .gravatar {
757 display: inline;
804 display: inline;
758 min-width: 16px;
805 min-width: 16px;
759 min-height: 16px;
806 min-height: 16px;
760 margin: -5px 0;
807 margin: -5px 0;
761 padding: 0;
808 padding: 0;
762 line-height: 1em;
809 line-height: 1em;
763 box-sizing: content-box;
810 box-sizing: content-box;
764 border-radius: 50%;
811 border-radius: 50%;
765
812
766 &.gravatar-large {
813 &.gravatar-large {
767 margin: -0.5em .25em -0.5em 0;
814 margin: -0.5em .25em -0.5em 0;
768 }
815 }
769
816
770 & + .user {
817 & + .user {
771 display: inline;
818 display: inline;
772 margin: 0;
819 margin: 0;
773 padding: 0 0 0 .17em;
820 padding: 0 0 0 .17em;
774 line-height: 1em;
821 line-height: 1em;
775 }
822 }
776 }
823 }
777
824
778 .user-inline-data {
825 .user-inline-data {
779 display: inline-block;
826 display: inline-block;
780 float: left;
827 float: left;
781 padding-left: .5em;
828 padding-left: .5em;
782 line-height: 1.3em;
829 line-height: 1.3em;
783 }
830 }
784
831
785 .rc-user { // gravatar + user wrapper
832 .rc-user { // gravatar + user wrapper
786 float: left;
833 float: left;
787 position: relative;
834 position: relative;
788 min-width: 100px;
835 min-width: 100px;
789 max-width: 200px;
836 max-width: 200px;
790 min-height: (@gravatar-size + @border-thickness * 2); // account for border
837 min-height: (@gravatar-size + @border-thickness * 2); // account for border
791 display: block;
838 display: block;
792 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
839 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
793
840
794
841
795 .gravatar {
842 .gravatar {
796 display: block;
843 display: block;
797 position: absolute;
844 position: absolute;
798 top: 0;
845 top: 0;
799 left: 0;
846 left: 0;
800 min-width: @gravatar-size;
847 min-width: @gravatar-size;
801 min-height: @gravatar-size;
848 min-height: @gravatar-size;
802 margin: 0;
849 margin: 0;
803 }
850 }
804
851
805 .user {
852 .user {
806 display: block;
853 display: block;
807 max-width: 175px;
854 max-width: 175px;
808 padding-top: 2px;
855 padding-top: 2px;
809 overflow: hidden;
856 overflow: hidden;
810 text-overflow: ellipsis;
857 text-overflow: ellipsis;
811 }
858 }
812 }
859 }
813
860
814 .gist-gravatar,
861 .gist-gravatar,
815 .journal_container {
862 .journal_container {
816 .gravatar-large {
863 .gravatar-large {
817 margin: 0 .5em -10px 0;
864 margin: 0 .5em -10px 0;
818 }
865 }
819 }
866 }
820
867
821 .gist-type-fields {
868 .gist-type-fields {
822 line-height: 30px;
869 line-height: 30px;
823 height: 30px;
870 height: 30px;
824
871
825 .gist-type-fields-wrapper {
872 .gist-type-fields-wrapper {
826 vertical-align: middle;
873 vertical-align: middle;
827 display: inline-block;
874 display: inline-block;
828 line-height: 25px;
875 line-height: 25px;
829 }
876 }
830 }
877 }
831
878
832 // ADMIN SETTINGS
879 // ADMIN SETTINGS
833
880
834 // Tag Patterns
881 // Tag Patterns
835 .tag_patterns {
882 .tag_patterns {
836 .tag_input {
883 .tag_input {
837 margin-bottom: @padding;
884 margin-bottom: @padding;
838 }
885 }
839 }
886 }
840
887
841 .locked_input {
888 .locked_input {
842 position: relative;
889 position: relative;
843
890
844 input {
891 input {
845 display: inline;
892 display: inline;
846 margin: 3px 5px 0px 0px;
893 margin: 3px 5px 0px 0px;
847 }
894 }
848
895
849 br {
896 br {
850 display: none;
897 display: none;
851 }
898 }
852
899
853 .error-message {
900 .error-message {
854 float: left;
901 float: left;
855 width: 100%;
902 width: 100%;
856 }
903 }
857
904
858 .lock_input_button {
905 .lock_input_button {
859 display: inline;
906 display: inline;
860 }
907 }
861
908
862 .help-block {
909 .help-block {
863 clear: both;
910 clear: both;
864 }
911 }
865 }
912 }
866
913
867 // Notifications
914 // Notifications
868
915
869 .notifications_buttons {
916 .notifications_buttons {
870 margin: 0 0 @space 0;
917 margin: 0 0 @space 0;
871 padding: 0;
918 padding: 0;
872
919
873 .btn {
920 .btn {
874 display: inline-block;
921 display: inline-block;
875 }
922 }
876 }
923 }
877
924
878 .notification-list {
925 .notification-list {
879
926
880 div {
927 div {
881 vertical-align: middle;
928 vertical-align: middle;
882 }
929 }
883
930
884 .container {
931 .container {
885 display: block;
932 display: block;
886 margin: 0 0 @padding 0;
933 margin: 0 0 @padding 0;
887 }
934 }
888
935
889 .delete-notifications {
936 .delete-notifications {
890 margin-left: @padding;
937 margin-left: @padding;
891 text-align: right;
938 text-align: right;
892 cursor: pointer;
939 cursor: pointer;
893 }
940 }
894
941
895 .read-notifications {
942 .read-notifications {
896 margin-left: @padding/2;
943 margin-left: @padding/2;
897 text-align: right;
944 text-align: right;
898 width: 35px;
945 width: 35px;
899 cursor: pointer;
946 cursor: pointer;
900 }
947 }
901
948
902 .icon-minus-sign {
949 .icon-minus-sign {
903 color: @alert2;
950 color: @alert2;
904 }
951 }
905
952
906 .icon-ok-sign {
953 .icon-ok-sign {
907 color: @alert1;
954 color: @alert1;
908 }
955 }
909 }
956 }
910
957
911 .user_settings {
958 .user_settings {
912 float: left;
959 float: left;
913 clear: both;
960 clear: both;
914 display: block;
961 display: block;
915 width: 100%;
962 width: 100%;
916
963
917 .gravatar_box {
964 .gravatar_box {
918 margin-bottom: @padding;
965 margin-bottom: @padding;
919
966
920 &:after {
967 &:after {
921 content: " ";
968 content: " ";
922 clear: both;
969 clear: both;
923 width: 100%;
970 width: 100%;
924 }
971 }
925 }
972 }
926
973
927 .fields .field {
974 .fields .field {
928 clear: both;
975 clear: both;
929 }
976 }
930 }
977 }
931
978
932 .advanced_settings {
979 .advanced_settings {
933 margin-bottom: @space;
980 margin-bottom: @space;
934
981
935 .help-block {
982 .help-block {
936 margin-left: 0;
983 margin-left: 0;
937 }
984 }
938
985
939 button + .help-block {
986 button + .help-block {
940 margin-top: @padding;
987 margin-top: @padding;
941 }
988 }
942 }
989 }
943
990
944 // admin settings radio buttons and labels
991 // admin settings radio buttons and labels
945 .label-2 {
992 .label-2 {
946 float: left;
993 float: left;
947 width: @label2-width;
994 width: @label2-width;
948
995
949 label {
996 label {
950 color: @grey1;
997 color: @grey1;
951 }
998 }
952 }
999 }
953 .checkboxes {
1000 .checkboxes {
954 float: left;
1001 float: left;
955 width: @checkboxes-width;
1002 width: @checkboxes-width;
956 margin-bottom: @padding;
1003 margin-bottom: @padding;
957
1004
958 .checkbox {
1005 .checkbox {
959 width: 100%;
1006 width: 100%;
960
1007
961 label {
1008 label {
962 margin: 0;
1009 margin: 0;
963 padding: 0;
1010 padding: 0;
964 }
1011 }
965 }
1012 }
966
1013
967 .checkbox + .checkbox {
1014 .checkbox + .checkbox {
968 display: inline-block;
1015 display: inline-block;
969 }
1016 }
970
1017
971 label {
1018 label {
972 margin-right: 1em;
1019 margin-right: 1em;
973 }
1020 }
974 }
1021 }
975
1022
976 // CHANGELOG
1023 // CHANGELOG
977 .container_header {
1024 .container_header {
978 float: left;
1025 float: left;
979 display: block;
1026 display: block;
980 width: 100%;
1027 width: 100%;
981 margin: @padding 0 @padding;
1028 margin: @padding 0 @padding;
982
1029
983 #filter_changelog {
1030 #filter_changelog {
984 float: left;
1031 float: left;
985 margin-right: @padding;
1032 margin-right: @padding;
986 }
1033 }
987
1034
988 .breadcrumbs_light {
1035 .breadcrumbs_light {
989 display: inline-block;
1036 display: inline-block;
990 }
1037 }
991 }
1038 }
992
1039
993 .info_box {
1040 .info_box {
994 float: right;
1041 float: right;
995 }
1042 }
996
1043
997
1044
998
1045
999 #graph_content{
1046 #graph_content{
1000
1047
1001 // adjust for table headers so that graph renders properly
1048 // adjust for table headers so that graph renders properly
1002 // #graph_nodes padding - table cell padding
1049 // #graph_nodes padding - table cell padding
1003 padding-top: (@space - (@basefontsize * 2.4));
1050 padding-top: (@space - (@basefontsize * 2.4));
1004
1051
1005 &.graph_full_width {
1052 &.graph_full_width {
1006 width: 100%;
1053 width: 100%;
1007 max-width: 100%;
1054 max-width: 100%;
1008 }
1055 }
1009 }
1056 }
1010
1057
1011 #graph {
1058 #graph {
1012
1059
1013 .pagination-left {
1060 .pagination-left {
1014 float: left;
1061 float: left;
1015 clear: both;
1062 clear: both;
1016 }
1063 }
1017
1064
1018 .log-container {
1065 .log-container {
1019 max-width: 345px;
1066 max-width: 345px;
1020
1067
1021 .message{
1068 .message{
1022 max-width: 340px;
1069 max-width: 340px;
1023 }
1070 }
1024 }
1071 }
1025
1072
1026 .graph-col-wrapper {
1073 .graph-col-wrapper {
1027
1074
1028 #graph_nodes {
1075 #graph_nodes {
1029 width: 100px;
1076 width: 100px;
1030 position: absolute;
1077 position: absolute;
1031 left: 70px;
1078 left: 70px;
1032 z-index: -1;
1079 z-index: -1;
1033 }
1080 }
1034 }
1081 }
1035
1082
1036 .load-more-commits {
1083 .load-more-commits {
1037 text-align: center;
1084 text-align: center;
1038 }
1085 }
1039 .load-more-commits:hover {
1086 .load-more-commits:hover {
1040 background-color: @grey7;
1087 background-color: @grey7;
1041 }
1088 }
1042 .load-more-commits {
1089 .load-more-commits {
1043 a {
1090 a {
1044 display: block;
1091 display: block;
1045 }
1092 }
1046 }
1093 }
1047 }
1094 }
1048
1095
1049 .obsolete-toggle {
1096 .obsolete-toggle {
1050 line-height: 30px;
1097 line-height: 30px;
1051 margin-left: -15px;
1098 margin-left: -15px;
1052 }
1099 }
1053
1100
1054 #rev_range_container, #rev_range_clear, #rev_range_more {
1101 #rev_range_container, #rev_range_clear, #rev_range_more {
1055 margin-top: -5px;
1102 margin-top: -5px;
1056 margin-bottom: -5px;
1103 margin-bottom: -5px;
1057 }
1104 }
1058
1105
1059 #filter_changelog {
1106 #filter_changelog {
1060 float: left;
1107 float: left;
1061 }
1108 }
1062
1109
1063
1110
1064 //--- THEME ------------------//
1111 //--- THEME ------------------//
1065
1112
1066 #logo {
1113 #logo {
1067 float: left;
1114 float: left;
1068 margin: 9px 0 0 0;
1115 margin: 9px 0 0 0;
1069
1116
1070 .header {
1117 .header {
1071 background-color: transparent;
1118 background-color: transparent;
1072 }
1119 }
1073
1120
1074 a {
1121 a {
1075 display: inline-block;
1122 display: inline-block;
1076 }
1123 }
1077
1124
1078 img {
1125 img {
1079 height:30px;
1126 height:30px;
1080 }
1127 }
1081 }
1128 }
1082
1129
1083 .logo-wrapper {
1130 .logo-wrapper {
1084 float:left;
1131 float:left;
1085 }
1132 }
1086
1133
1087 .branding {
1134 .branding {
1088 float: left;
1135 float: left;
1089 padding: 9px 2px;
1136 padding: 9px 2px;
1090 line-height: 1em;
1137 line-height: 1em;
1091 font-size: @navigation-fontsize;
1138 font-size: @navigation-fontsize;
1092
1139
1093 a {
1140 a {
1094 color: @grey5
1141 color: @grey5
1095 }
1142 }
1096 @media screen and (max-width: 1200px) {
1143 @media screen and (max-width: 1200px) {
1097 display: none;
1144 display: none;
1098 }
1145 }
1099 }
1146 }
1100
1147
1101 img {
1148 img {
1102 border: none;
1149 border: none;
1103 outline: none;
1150 outline: none;
1104 }
1151 }
1105 user-profile-header
1152 user-profile-header
1106 label {
1153 label {
1107
1154
1108 input[type="checkbox"] {
1155 input[type="checkbox"] {
1109 margin-right: 1em;
1156 margin-right: 1em;
1110 }
1157 }
1111 input[type="radio"] {
1158 input[type="radio"] {
1112 margin-right: 1em;
1159 margin-right: 1em;
1113 }
1160 }
1114 }
1161 }
1115
1162
1116 .review-status {
1163 .review-status {
1117 &.under_review {
1164 &.under_review {
1118 color: @alert3;
1165 color: @alert3;
1119 }
1166 }
1120 &.approved {
1167 &.approved {
1121 color: @alert1;
1168 color: @alert1;
1122 }
1169 }
1123 &.rejected,
1170 &.rejected,
1124 &.forced_closed{
1171 &.forced_closed{
1125 color: @alert2;
1172 color: @alert2;
1126 }
1173 }
1127 &.not_reviewed {
1174 &.not_reviewed {
1128 color: @grey5;
1175 color: @grey5;
1129 }
1176 }
1130 }
1177 }
1131
1178
1132 .review-status-under_review {
1179 .review-status-under_review {
1133 color: @alert3;
1180 color: @alert3;
1134 }
1181 }
1135 .status-tag-under_review {
1182 .status-tag-under_review {
1136 border-color: @alert3;
1183 border-color: @alert3;
1137 }
1184 }
1138
1185
1139 .review-status-approved {
1186 .review-status-approved {
1140 color: @alert1;
1187 color: @alert1;
1141 }
1188 }
1142 .status-tag-approved {
1189 .status-tag-approved {
1143 border-color: @alert1;
1190 border-color: @alert1;
1144 }
1191 }
1145
1192
1146 .review-status-rejected,
1193 .review-status-rejected,
1147 .review-status-forced_closed {
1194 .review-status-forced_closed {
1148 color: @alert2;
1195 color: @alert2;
1149 }
1196 }
1150 .status-tag-rejected,
1197 .status-tag-rejected,
1151 .status-tag-forced_closed {
1198 .status-tag-forced_closed {
1152 border-color: @alert2;
1199 border-color: @alert2;
1153 }
1200 }
1154
1201
1155 .review-status-not_reviewed {
1202 .review-status-not_reviewed {
1156 color: @grey5;
1203 color: @grey5;
1157 }
1204 }
1158 .status-tag-not_reviewed {
1205 .status-tag-not_reviewed {
1159 border-color: @grey5;
1206 border-color: @grey5;
1160 }
1207 }
1161
1208
1162 .test_pattern_preview {
1209 .test_pattern_preview {
1163 margin: @space 0;
1210 margin: @space 0;
1164
1211
1165 p {
1212 p {
1166 margin-bottom: 0;
1213 margin-bottom: 0;
1167 border-bottom: @border-thickness solid @border-default-color;
1214 border-bottom: @border-thickness solid @border-default-color;
1168 color: @grey3;
1215 color: @grey3;
1169 }
1216 }
1170
1217
1171 .btn {
1218 .btn {
1172 margin-bottom: @padding;
1219 margin-bottom: @padding;
1173 }
1220 }
1174 }
1221 }
1175 #test_pattern_result {
1222 #test_pattern_result {
1176 display: none;
1223 display: none;
1177 &:extend(pre);
1224 &:extend(pre);
1178 padding: .9em;
1225 padding: .9em;
1179 color: @grey3;
1226 color: @grey3;
1180 background-color: @grey7;
1227 background-color: @grey7;
1181 border-right: @border-thickness solid @border-default-color;
1228 border-right: @border-thickness solid @border-default-color;
1182 border-bottom: @border-thickness solid @border-default-color;
1229 border-bottom: @border-thickness solid @border-default-color;
1183 border-left: @border-thickness solid @border-default-color;
1230 border-left: @border-thickness solid @border-default-color;
1184 }
1231 }
1185
1232
1186 #repo_vcs_settings {
1233 #repo_vcs_settings {
1187 #inherit_overlay_vcs_default {
1234 #inherit_overlay_vcs_default {
1188 display: none;
1235 display: none;
1189 }
1236 }
1190 #inherit_overlay_vcs_custom {
1237 #inherit_overlay_vcs_custom {
1191 display: custom;
1238 display: custom;
1192 }
1239 }
1193 &.inherited {
1240 &.inherited {
1194 #inherit_overlay_vcs_default {
1241 #inherit_overlay_vcs_default {
1195 display: block;
1242 display: block;
1196 }
1243 }
1197 #inherit_overlay_vcs_custom {
1244 #inherit_overlay_vcs_custom {
1198 display: none;
1245 display: none;
1199 }
1246 }
1200 }
1247 }
1201 }
1248 }
1202
1249
1203 .issue-tracker-link {
1250 .issue-tracker-link {
1204 color: @rcblue;
1251 color: @rcblue;
1205 }
1252 }
1206
1253
1207 // Issue Tracker Table Show/Hide
1254 // Issue Tracker Table Show/Hide
1208 #repo_issue_tracker {
1255 #repo_issue_tracker {
1209 #inherit_overlay {
1256 #inherit_overlay {
1210 display: none;
1257 display: none;
1211 }
1258 }
1212 #custom_overlay {
1259 #custom_overlay {
1213 display: custom;
1260 display: custom;
1214 }
1261 }
1215 &.inherited {
1262 &.inherited {
1216 #inherit_overlay {
1263 #inherit_overlay {
1217 display: block;
1264 display: block;
1218 }
1265 }
1219 #custom_overlay {
1266 #custom_overlay {
1220 display: none;
1267 display: none;
1221 }
1268 }
1222 }
1269 }
1223 }
1270 }
1224 table.issuetracker {
1271 table.issuetracker {
1225 &.readonly {
1272 &.readonly {
1226 tr, td {
1273 tr, td {
1227 color: @grey3;
1274 color: @grey3;
1228 }
1275 }
1229 }
1276 }
1230 .edit {
1277 .edit {
1231 display: none;
1278 display: none;
1232 }
1279 }
1233 .editopen {
1280 .editopen {
1234 .edit {
1281 .edit {
1235 display: inline;
1282 display: inline;
1236 }
1283 }
1237 .entry {
1284 .entry {
1238 display: none;
1285 display: none;
1239 }
1286 }
1240 }
1287 }
1241 tr td.td-action {
1288 tr td.td-action {
1242 min-width: 117px;
1289 min-width: 117px;
1243 }
1290 }
1244 td input {
1291 td input {
1245 max-width: none;
1292 max-width: none;
1246 min-width: 30px;
1293 min-width: 30px;
1247 width: 80%;
1294 width: 80%;
1248 }
1295 }
1249 .issuetracker_pref input {
1296 .issuetracker_pref input {
1250 width: 40%;
1297 width: 40%;
1251 }
1298 }
1252 input.edit_issuetracker_update {
1299 input.edit_issuetracker_update {
1253 margin-right: 0;
1300 margin-right: 0;
1254 width: auto;
1301 width: auto;
1255 }
1302 }
1256 }
1303 }
1257
1304
1258 table.integrations {
1305 table.integrations {
1259 .td-icon {
1306 .td-icon {
1260 width: 20px;
1307 width: 20px;
1261 .integration-icon {
1308 .integration-icon {
1262 height: 20px;
1309 height: 20px;
1263 width: 20px;
1310 width: 20px;
1264 }
1311 }
1265 }
1312 }
1266 }
1313 }
1267
1314
1268 .integrations {
1315 .integrations {
1269 a.integration-box {
1316 a.integration-box {
1270 color: @text-color;
1317 color: @text-color;
1271 &:hover {
1318 &:hover {
1272 .panel {
1319 .panel {
1273 background: #fbfbfb;
1320 background: #fbfbfb;
1274 }
1321 }
1275 }
1322 }
1276 .integration-icon {
1323 .integration-icon {
1277 width: 30px;
1324 width: 30px;
1278 height: 30px;
1325 height: 30px;
1279 margin-right: 20px;
1326 margin-right: 20px;
1280 float: left;
1327 float: left;
1281 }
1328 }
1282
1329
1283 .panel-body {
1330 .panel-body {
1284 padding: 10px;
1331 padding: 10px;
1285 }
1332 }
1286 .panel {
1333 .panel {
1287 margin-bottom: 10px;
1334 margin-bottom: 10px;
1288 }
1335 }
1289 h2 {
1336 h2 {
1290 display: inline-block;
1337 display: inline-block;
1291 margin: 0;
1338 margin: 0;
1292 min-width: 140px;
1339 min-width: 140px;
1293 }
1340 }
1294 }
1341 }
1295 a.integration-box.dummy-integration {
1342 a.integration-box.dummy-integration {
1296 color: @grey4
1343 color: @grey4
1297 }
1344 }
1298 }
1345 }
1299
1346
1300 //Permissions Settings
1347 //Permissions Settings
1301 #add_perm {
1348 #add_perm {
1302 margin: 0 0 @padding;
1349 margin: 0 0 @padding;
1303 cursor: pointer;
1350 cursor: pointer;
1304 }
1351 }
1305
1352
1306 .perm_ac {
1353 .perm_ac {
1307 input {
1354 input {
1308 width: 95%;
1355 width: 95%;
1309 }
1356 }
1310 }
1357 }
1311
1358
1312 .autocomplete-suggestions {
1359 .autocomplete-suggestions {
1313 width: auto !important; // overrides autocomplete.js
1360 width: auto !important; // overrides autocomplete.js
1314 min-width: 278px;
1361 min-width: 278px;
1315 margin: 0;
1362 margin: 0;
1316 border: @border-thickness solid @grey5;
1363 border: @border-thickness solid @grey5;
1317 border-radius: @border-radius;
1364 border-radius: @border-radius;
1318 color: @grey2;
1365 color: @grey2;
1319 background-color: white;
1366 background-color: white;
1320 }
1367 }
1321
1368
1322 .autocomplete-qfilter-suggestions {
1369 .autocomplete-qfilter-suggestions {
1323 width: auto !important; // overrides autocomplete.js
1370 width: auto !important; // overrides autocomplete.js
1324 max-height: 100% !important;
1371 max-height: 100% !important;
1325 min-width: 376px;
1372 min-width: 376px;
1326 margin: 0;
1373 margin: 0;
1327 border: @border-thickness solid @grey5;
1374 border: @border-thickness solid @grey5;
1328 color: @grey2;
1375 color: @grey2;
1329 background-color: white;
1376 background-color: white;
1330 }
1377 }
1331
1378
1332 .autocomplete-selected {
1379 .autocomplete-selected {
1333 background: #F0F0F0;
1380 background: #F0F0F0;
1334 }
1381 }
1335
1382
1336 .ac-container-wrap {
1383 .ac-container-wrap {
1337 margin: 0;
1384 margin: 0;
1338 padding: 8px;
1385 padding: 8px;
1339 border-bottom: @border-thickness solid @grey5;
1386 border-bottom: @border-thickness solid @grey5;
1340 list-style-type: none;
1387 list-style-type: none;
1341 cursor: pointer;
1388 cursor: pointer;
1342
1389
1343 &:hover {
1390 &:hover {
1344 background-color: @grey7;
1391 background-color: @grey7;
1345 }
1392 }
1346
1393
1347 img {
1394 img {
1348 height: @gravatar-size;
1395 height: @gravatar-size;
1349 width: @gravatar-size;
1396 width: @gravatar-size;
1350 margin-right: 1em;
1397 margin-right: 1em;
1351 }
1398 }
1352
1399
1353 strong {
1400 strong {
1354 font-weight: normal;
1401 font-weight: normal;
1355 }
1402 }
1356 }
1403 }
1357
1404
1358 // Settings Dropdown
1405 // Settings Dropdown
1359 .user-menu .container {
1406 .user-menu .container {
1360 padding: 0 4px;
1407 padding: 0 4px;
1361 margin: 0;
1408 margin: 0;
1362 }
1409 }
1363
1410
1364 .user-menu .gravatar {
1411 .user-menu .gravatar {
1365 cursor: pointer;
1412 cursor: pointer;
1366 }
1413 }
1367
1414
1368 .codeblock {
1415 .codeblock {
1369 margin-bottom: @padding;
1416 margin-bottom: @padding;
1370 clear: both;
1417 clear: both;
1371
1418
1372 .stats {
1419 .stats {
1373 overflow: hidden;
1420 overflow: hidden;
1374 }
1421 }
1375
1422
1376 .message{
1423 .message{
1377 textarea{
1424 textarea{
1378 margin: 0;
1425 margin: 0;
1379 }
1426 }
1380 }
1427 }
1381
1428
1382 .code-header {
1429 .code-header {
1383 .stats {
1430 .stats {
1384 line-height: 2em;
1431 line-height: 2em;
1385
1432
1386 .revision_id {
1433 .revision_id {
1387 margin-left: 0;
1434 margin-left: 0;
1388 }
1435 }
1389 .buttons {
1436 .buttons {
1390 padding-right: 0;
1437 padding-right: 0;
1391 }
1438 }
1392 }
1439 }
1393
1440
1394 .item{
1441 .item{
1395 margin-right: 0.5em;
1442 margin-right: 0.5em;
1396 }
1443 }
1397 }
1444 }
1398
1445
1399 #editor_container {
1446 #editor_container {
1400 position: relative;
1447 position: relative;
1401 margin: @padding 10px;
1448 margin: @padding 10px;
1402 }
1449 }
1403 }
1450 }
1404
1451
1405 #file_history_container {
1452 #file_history_container {
1406 display: none;
1453 display: none;
1407 }
1454 }
1408
1455
1409 .file-history-inner {
1456 .file-history-inner {
1410 margin-bottom: 10px;
1457 margin-bottom: 10px;
1411 }
1458 }
1412
1459
1413 // Pull Requests
1460 // Pull Requests
1414 .summary-details {
1461 .summary-details {
1415 width: 72%;
1462 width: 72%;
1416 }
1463 }
1417 .pr-summary {
1464 .pr-summary {
1418 border-bottom: @border-thickness solid @grey5;
1465 border-bottom: @border-thickness solid @grey5;
1419 margin-bottom: @space;
1466 margin-bottom: @space;
1420 }
1467 }
1421 .reviewers-title {
1468 .reviewers-title {
1422 width: 25%;
1469 width: 25%;
1423 min-width: 200px;
1470 min-width: 200px;
1424 }
1471 }
1425 .reviewers {
1472 .reviewers {
1426 width: 25%;
1473 width: 25%;
1427 min-width: 200px;
1474 min-width: 200px;
1428 }
1475 }
1429 .reviewers ul li {
1476 .reviewers ul li {
1430 position: relative;
1477 position: relative;
1431 width: 100%;
1478 width: 100%;
1432 padding-bottom: 8px;
1479 padding-bottom: 8px;
1433 list-style-type: none;
1480 list-style-type: none;
1434 }
1481 }
1435
1482
1436 .reviewer_entry {
1483 .reviewer_entry {
1437 min-height: 55px;
1484 min-height: 55px;
1438 }
1485 }
1439
1486
1440 .reviewers_member {
1487 .reviewers_member {
1441 width: 100%;
1488 width: 100%;
1442 overflow: auto;
1489 overflow: auto;
1443 }
1490 }
1444 .reviewer_reason {
1491 .reviewer_reason {
1445 padding-left: 20px;
1492 padding-left: 20px;
1446 line-height: 1.5em;
1493 line-height: 1.5em;
1447 }
1494 }
1448 .reviewer_status {
1495 .reviewer_status {
1449 display: inline-block;
1496 display: inline-block;
1450 width: 25px;
1497 width: 25px;
1451 min-width: 25px;
1498 min-width: 25px;
1452 height: 1.2em;
1499 height: 1.2em;
1453 line-height: 1em;
1500 line-height: 1em;
1454 }
1501 }
1455
1502
1456 .reviewer_name {
1503 .reviewer_name {
1457 display: inline-block;
1504 display: inline-block;
1458 max-width: 83%;
1505 max-width: 83%;
1459 padding-right: 20px;
1506 padding-right: 20px;
1460 vertical-align: middle;
1507 vertical-align: middle;
1461 line-height: 1;
1508 line-height: 1;
1462
1509
1463 .rc-user {
1510 .rc-user {
1464 min-width: 0;
1511 min-width: 0;
1465 margin: -2px 1em 0 0;
1512 margin: -2px 1em 0 0;
1466 }
1513 }
1467
1514
1468 .reviewer {
1515 .reviewer {
1469 float: left;
1516 float: left;
1470 }
1517 }
1471 }
1518 }
1472
1519
1473 .reviewer_member_mandatory {
1520 .reviewer_member_mandatory {
1474 position: absolute;
1521 position: absolute;
1475 left: 15px;
1522 left: 15px;
1476 top: 8px;
1523 top: 8px;
1477 width: 16px;
1524 width: 16px;
1478 font-size: 11px;
1525 font-size: 11px;
1479 margin: 0;
1526 margin: 0;
1480 padding: 0;
1527 padding: 0;
1481 color: black;
1528 color: black;
1482 }
1529 }
1483
1530
1484 .reviewer_member_mandatory_remove,
1531 .reviewer_member_mandatory_remove,
1485 .reviewer_member_remove {
1532 .reviewer_member_remove {
1486 position: absolute;
1533 position: absolute;
1487 right: 0;
1534 right: 0;
1488 top: 0;
1535 top: 0;
1489 width: 16px;
1536 width: 16px;
1490 margin-bottom: 10px;
1537 margin-bottom: 10px;
1491 padding: 0;
1538 padding: 0;
1492 color: black;
1539 color: black;
1493 }
1540 }
1494
1541
1495 .reviewer_member_mandatory_remove {
1542 .reviewer_member_mandatory_remove {
1496 color: @grey4;
1543 color: @grey4;
1497 }
1544 }
1498
1545
1499 .reviewer_member_status {
1546 .reviewer_member_status {
1500 margin-top: 5px;
1547 margin-top: 5px;
1501 }
1548 }
1502 .pr-summary #summary{
1549 .pr-summary #summary{
1503 width: 100%;
1550 width: 100%;
1504 }
1551 }
1505 .pr-summary .action_button:hover {
1552 .pr-summary .action_button:hover {
1506 border: 0;
1553 border: 0;
1507 cursor: pointer;
1554 cursor: pointer;
1508 }
1555 }
1509 .pr-details-title {
1556 .pr-details-title {
1510 padding-bottom: 8px;
1557 padding-bottom: 8px;
1511 border-bottom: @border-thickness solid @grey5;
1558 border-bottom: @border-thickness solid @grey5;
1512
1559
1513 .action_button.disabled {
1560 .action_button.disabled {
1514 color: @grey4;
1561 color: @grey4;
1515 cursor: inherit;
1562 cursor: inherit;
1516 }
1563 }
1517 .action_button {
1564 .action_button {
1518 color: @rcblue;
1565 color: @rcblue;
1519 }
1566 }
1520 }
1567 }
1521 .pr-details-content {
1568 .pr-details-content {
1522 margin-top: @textmargin;
1569 margin-top: @textmargin;
1523 margin-bottom: @textmargin;
1570 margin-bottom: @textmargin;
1524 }
1571 }
1525
1572
1526 .pr-reviewer-rules {
1573 .pr-reviewer-rules {
1527 padding: 10px 0px 20px 0px;
1574 padding: 10px 0px 20px 0px;
1528 }
1575 }
1529
1576
1530 .group_members {
1577 .group_members {
1531 margin-top: 0;
1578 margin-top: 0;
1532 padding: 0;
1579 padding: 0;
1533 list-style: outside none none;
1580 list-style: outside none none;
1534
1581
1535 img {
1582 img {
1536 height: @gravatar-size;
1583 height: @gravatar-size;
1537 width: @gravatar-size;
1584 width: @gravatar-size;
1538 margin-right: .5em;
1585 margin-right: .5em;
1539 margin-left: 3px;
1586 margin-left: 3px;
1540 }
1587 }
1541
1588
1542 .to-delete {
1589 .to-delete {
1543 .user {
1590 .user {
1544 text-decoration: line-through;
1591 text-decoration: line-through;
1545 }
1592 }
1546 }
1593 }
1547 }
1594 }
1548
1595
1549 .compare_view_commits_title {
1596 .compare_view_commits_title {
1550 .disabled {
1597 .disabled {
1551 cursor: inherit;
1598 cursor: inherit;
1552 &:hover{
1599 &:hover{
1553 background-color: inherit;
1600 background-color: inherit;
1554 color: inherit;
1601 color: inherit;
1555 }
1602 }
1556 }
1603 }
1557 }
1604 }
1558
1605
1559 .subtitle-compare {
1606 .subtitle-compare {
1560 margin: -15px 0px 0px 0px;
1607 margin: -15px 0px 0px 0px;
1561 }
1608 }
1562
1609
1563 // new entry in group_members
1610 // new entry in group_members
1564 .td-author-new-entry {
1611 .td-author-new-entry {
1565 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1612 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1566 }
1613 }
1567
1614
1568 .usergroup_member_remove {
1615 .usergroup_member_remove {
1569 width: 16px;
1616 width: 16px;
1570 margin-bottom: 10px;
1617 margin-bottom: 10px;
1571 padding: 0;
1618 padding: 0;
1572 color: black !important;
1619 color: black !important;
1573 cursor: pointer;
1620 cursor: pointer;
1574 }
1621 }
1575
1622
1576 .reviewer_ac .ac-input {
1623 .reviewer_ac .ac-input {
1577 width: 92%;
1624 width: 92%;
1578 margin-bottom: 1em;
1625 margin-bottom: 1em;
1579 }
1626 }
1580
1627
1581 .compare_view_commits tr{
1628 .compare_view_commits tr{
1582 height: 20px;
1629 height: 20px;
1583 }
1630 }
1584 .compare_view_commits td {
1631 .compare_view_commits td {
1585 vertical-align: top;
1632 vertical-align: top;
1586 padding-top: 10px;
1633 padding-top: 10px;
1587 }
1634 }
1588 .compare_view_commits .author {
1635 .compare_view_commits .author {
1589 margin-left: 5px;
1636 margin-left: 5px;
1590 }
1637 }
1591
1638
1592 .compare_view_commits {
1639 .compare_view_commits {
1593 .color-a {
1640 .color-a {
1594 color: @alert1;
1641 color: @alert1;
1595 }
1642 }
1596
1643
1597 .color-c {
1644 .color-c {
1598 color: @color3;
1645 color: @color3;
1599 }
1646 }
1600
1647
1601 .color-r {
1648 .color-r {
1602 color: @color5;
1649 color: @color5;
1603 }
1650 }
1604
1651
1605 .color-a-bg {
1652 .color-a-bg {
1606 background-color: @alert1;
1653 background-color: @alert1;
1607 }
1654 }
1608
1655
1609 .color-c-bg {
1656 .color-c-bg {
1610 background-color: @alert3;
1657 background-color: @alert3;
1611 }
1658 }
1612
1659
1613 .color-r-bg {
1660 .color-r-bg {
1614 background-color: @alert2;
1661 background-color: @alert2;
1615 }
1662 }
1616
1663
1617 .color-a-border {
1664 .color-a-border {
1618 border: 1px solid @alert1;
1665 border: 1px solid @alert1;
1619 }
1666 }
1620
1667
1621 .color-c-border {
1668 .color-c-border {
1622 border: 1px solid @alert3;
1669 border: 1px solid @alert3;
1623 }
1670 }
1624
1671
1625 .color-r-border {
1672 .color-r-border {
1626 border: 1px solid @alert2;
1673 border: 1px solid @alert2;
1627 }
1674 }
1628
1675
1629 .commit-change-indicator {
1676 .commit-change-indicator {
1630 width: 15px;
1677 width: 15px;
1631 height: 15px;
1678 height: 15px;
1632 position: relative;
1679 position: relative;
1633 left: 15px;
1680 left: 15px;
1634 }
1681 }
1635
1682
1636 .commit-change-content {
1683 .commit-change-content {
1637 text-align: center;
1684 text-align: center;
1638 vertical-align: middle;
1685 vertical-align: middle;
1639 line-height: 15px;
1686 line-height: 15px;
1640 }
1687 }
1641 }
1688 }
1642
1689
1643 .compare_view_filepath {
1690 .compare_view_filepath {
1644 color: @grey1;
1691 color: @grey1;
1645 }
1692 }
1646
1693
1647 .show_more {
1694 .show_more {
1648 display: inline-block;
1695 display: inline-block;
1649 width: 0;
1696 width: 0;
1650 height: 0;
1697 height: 0;
1651 vertical-align: middle;
1698 vertical-align: middle;
1652 content: "";
1699 content: "";
1653 border: 4px solid;
1700 border: 4px solid;
1654 border-right-color: transparent;
1701 border-right-color: transparent;
1655 border-bottom-color: transparent;
1702 border-bottom-color: transparent;
1656 border-left-color: transparent;
1703 border-left-color: transparent;
1657 font-size: 0;
1704 font-size: 0;
1658 }
1705 }
1659
1706
1660 .journal_more .show_more {
1707 .journal_more .show_more {
1661 display: inline;
1708 display: inline;
1662
1709
1663 &:after {
1710 &:after {
1664 content: none;
1711 content: none;
1665 }
1712 }
1666 }
1713 }
1667
1714
1668 .compare_view_commits .collapse_commit:after {
1715 .compare_view_commits .collapse_commit:after {
1669 cursor: pointer;
1716 cursor: pointer;
1670 content: "\00A0\25B4";
1717 content: "\00A0\25B4";
1671 margin-left: -3px;
1718 margin-left: -3px;
1672 font-size: 17px;
1719 font-size: 17px;
1673 color: @grey4;
1720 color: @grey4;
1674 }
1721 }
1675
1722
1676 .diff_links {
1723 .diff_links {
1677 margin-left: 8px;
1724 margin-left: 8px;
1678 }
1725 }
1679
1726
1680 #pull_request_overview {
1727 #pull_request_overview {
1681 div.ancestor {
1728 div.ancestor {
1682 margin: -33px 0;
1729 margin: -33px 0;
1683 }
1730 }
1684 }
1731 }
1685
1732
1686 div.ancestor {
1733 div.ancestor {
1687 line-height: 33px;
1734 line-height: 33px;
1688 }
1735 }
1689
1736
1690 .cs_icon_td input[type="checkbox"] {
1737 .cs_icon_td input[type="checkbox"] {
1691 display: none;
1738 display: none;
1692 }
1739 }
1693
1740
1694 .cs_icon_td .expand_file_icon:after {
1741 .cs_icon_td .expand_file_icon:after {
1695 cursor: pointer;
1742 cursor: pointer;
1696 content: "\00A0\25B6";
1743 content: "\00A0\25B6";
1697 font-size: 12px;
1744 font-size: 12px;
1698 color: @grey4;
1745 color: @grey4;
1699 }
1746 }
1700
1747
1701 .cs_icon_td .collapse_file_icon:after {
1748 .cs_icon_td .collapse_file_icon:after {
1702 cursor: pointer;
1749 cursor: pointer;
1703 content: "\00A0\25BC";
1750 content: "\00A0\25BC";
1704 font-size: 12px;
1751 font-size: 12px;
1705 color: @grey4;
1752 color: @grey4;
1706 }
1753 }
1707
1754
1708 /*new binary
1755 /*new binary
1709 NEW_FILENODE = 1
1756 NEW_FILENODE = 1
1710 DEL_FILENODE = 2
1757 DEL_FILENODE = 2
1711 MOD_FILENODE = 3
1758 MOD_FILENODE = 3
1712 RENAMED_FILENODE = 4
1759 RENAMED_FILENODE = 4
1713 COPIED_FILENODE = 5
1760 COPIED_FILENODE = 5
1714 CHMOD_FILENODE = 6
1761 CHMOD_FILENODE = 6
1715 BIN_FILENODE = 7
1762 BIN_FILENODE = 7
1716 */
1763 */
1717 .cs_files_expand {
1764 .cs_files_expand {
1718 font-size: @basefontsize + 5px;
1765 font-size: @basefontsize + 5px;
1719 line-height: 1.8em;
1766 line-height: 1.8em;
1720 float: right;
1767 float: right;
1721 }
1768 }
1722
1769
1723 .cs_files_expand span{
1770 .cs_files_expand span{
1724 color: @rcblue;
1771 color: @rcblue;
1725 cursor: pointer;
1772 cursor: pointer;
1726 }
1773 }
1727 .cs_files {
1774 .cs_files {
1728 clear: both;
1775 clear: both;
1729 padding-bottom: @padding;
1776 padding-bottom: @padding;
1730
1777
1731 .cur_cs {
1778 .cur_cs {
1732 margin: 10px 2px;
1779 margin: 10px 2px;
1733 font-weight: bold;
1780 font-weight: bold;
1734 }
1781 }
1735
1782
1736 .node {
1783 .node {
1737 float: left;
1784 float: left;
1738 }
1785 }
1739
1786
1740 .changes {
1787 .changes {
1741 float: right;
1788 float: right;
1742 color: white;
1789 color: white;
1743 font-size: @basefontsize - 4px;
1790 font-size: @basefontsize - 4px;
1744 margin-top: 4px;
1791 margin-top: 4px;
1745 opacity: 0.6;
1792 opacity: 0.6;
1746 filter: Alpha(opacity=60); /* IE8 and earlier */
1793 filter: Alpha(opacity=60); /* IE8 and earlier */
1747
1794
1748 .added {
1795 .added {
1749 background-color: @alert1;
1796 background-color: @alert1;
1750 float: left;
1797 float: left;
1751 text-align: center;
1798 text-align: center;
1752 }
1799 }
1753
1800
1754 .deleted {
1801 .deleted {
1755 background-color: @alert2;
1802 background-color: @alert2;
1756 float: left;
1803 float: left;
1757 text-align: center;
1804 text-align: center;
1758 }
1805 }
1759
1806
1760 .bin {
1807 .bin {
1761 background-color: @alert1;
1808 background-color: @alert1;
1762 text-align: center;
1809 text-align: center;
1763 }
1810 }
1764
1811
1765 /*new binary*/
1812 /*new binary*/
1766 .bin.bin1 {
1813 .bin.bin1 {
1767 background-color: @alert1;
1814 background-color: @alert1;
1768 text-align: center;
1815 text-align: center;
1769 }
1816 }
1770
1817
1771 /*deleted binary*/
1818 /*deleted binary*/
1772 .bin.bin2 {
1819 .bin.bin2 {
1773 background-color: @alert2;
1820 background-color: @alert2;
1774 text-align: center;
1821 text-align: center;
1775 }
1822 }
1776
1823
1777 /*mod binary*/
1824 /*mod binary*/
1778 .bin.bin3 {
1825 .bin.bin3 {
1779 background-color: @grey2;
1826 background-color: @grey2;
1780 text-align: center;
1827 text-align: center;
1781 }
1828 }
1782
1829
1783 /*rename file*/
1830 /*rename file*/
1784 .bin.bin4 {
1831 .bin.bin4 {
1785 background-color: @alert4;
1832 background-color: @alert4;
1786 text-align: center;
1833 text-align: center;
1787 }
1834 }
1788
1835
1789 /*copied file*/
1836 /*copied file*/
1790 .bin.bin5 {
1837 .bin.bin5 {
1791 background-color: @alert4;
1838 background-color: @alert4;
1792 text-align: center;
1839 text-align: center;
1793 }
1840 }
1794
1841
1795 /*chmod file*/
1842 /*chmod file*/
1796 .bin.bin6 {
1843 .bin.bin6 {
1797 background-color: @grey2;
1844 background-color: @grey2;
1798 text-align: center;
1845 text-align: center;
1799 }
1846 }
1800 }
1847 }
1801 }
1848 }
1802
1849
1803 .cs_files .cs_added, .cs_files .cs_A,
1850 .cs_files .cs_added, .cs_files .cs_A,
1804 .cs_files .cs_added, .cs_files .cs_M,
1851 .cs_files .cs_added, .cs_files .cs_M,
1805 .cs_files .cs_added, .cs_files .cs_D {
1852 .cs_files .cs_added, .cs_files .cs_D {
1806 height: 16px;
1853 height: 16px;
1807 padding-right: 10px;
1854 padding-right: 10px;
1808 margin-top: 7px;
1855 margin-top: 7px;
1809 text-align: left;
1856 text-align: left;
1810 }
1857 }
1811
1858
1812 .cs_icon_td {
1859 .cs_icon_td {
1813 min-width: 16px;
1860 min-width: 16px;
1814 width: 16px;
1861 width: 16px;
1815 }
1862 }
1816
1863
1817 .pull-request-merge {
1864 .pull-request-merge {
1818 border: 1px solid @grey5;
1865 border: 1px solid @grey5;
1819 padding: 10px 0px 20px;
1866 padding: 10px 0px 20px;
1820 margin-top: 10px;
1867 margin-top: 10px;
1821 margin-bottom: 20px;
1868 margin-bottom: 20px;
1822 }
1869 }
1823
1870
1824 .pull-request-merge-refresh {
1871 .pull-request-merge-refresh {
1825 margin: 2px 7px;
1872 margin: 2px 7px;
1826 a {
1873 a {
1827 color: @grey3;
1874 color: @grey3;
1828 }
1875 }
1829 }
1876 }
1830
1877
1831 .pull-request-merge ul {
1878 .pull-request-merge ul {
1832 padding: 0px 0px;
1879 padding: 0px 0px;
1833 }
1880 }
1834
1881
1835 .pull-request-merge li {
1882 .pull-request-merge li {
1836 list-style-type: none;
1883 list-style-type: none;
1837 }
1884 }
1838
1885
1839 .pull-request-merge .pull-request-wrap {
1886 .pull-request-merge .pull-request-wrap {
1840 height: auto;
1887 height: auto;
1841 padding: 0px 0px;
1888 padding: 0px 0px;
1842 text-align: right;
1889 text-align: right;
1843 }
1890 }
1844
1891
1845 .pull-request-merge span {
1892 .pull-request-merge span {
1846 margin-right: 5px;
1893 margin-right: 5px;
1847 }
1894 }
1848
1895
1849 .pull-request-merge-actions {
1896 .pull-request-merge-actions {
1850 min-height: 30px;
1897 min-height: 30px;
1851 padding: 0px 0px;
1898 padding: 0px 0px;
1852 }
1899 }
1853
1900
1854 .pull-request-merge-info {
1901 .pull-request-merge-info {
1855 padding: 0px 5px 5px 0px;
1902 padding: 0px 5px 5px 0px;
1856 }
1903 }
1857
1904
1858 .merge-status {
1905 .merge-status {
1859 margin-right: 5px;
1906 margin-right: 5px;
1860 }
1907 }
1861
1908
1862 .merge-message {
1909 .merge-message {
1863 font-size: 1.2em
1910 font-size: 1.2em
1864 }
1911 }
1865
1912
1866 .merge-message.success i,
1913 .merge-message.success i,
1867 .merge-icon.success i {
1914 .merge-icon.success i {
1868 color:@alert1;
1915 color:@alert1;
1869 }
1916 }
1870
1917
1871 .merge-message.warning i,
1918 .merge-message.warning i,
1872 .merge-icon.warning i {
1919 .merge-icon.warning i {
1873 color: @alert3;
1920 color: @alert3;
1874 }
1921 }
1875
1922
1876 .merge-message.error i,
1923 .merge-message.error i,
1877 .merge-icon.error i {
1924 .merge-icon.error i {
1878 color:@alert2;
1925 color:@alert2;
1879 }
1926 }
1880
1927
1881 .pr-versions {
1928 .pr-versions {
1882 font-size: 1.1em;
1929 font-size: 1.1em;
1930 padding: 7.5px;
1883
1931
1884 table {
1932 table {
1885 padding: 0px 5px;
1933
1886 }
1934 }
1887
1935
1888 td {
1936 td {
1889 line-height: 15px;
1937 line-height: 15px;
1890 }
1938 }
1891
1939
1892 .compare-radio-button {
1940 .compare-radio-button {
1893 position: relative;
1941 position: relative;
1894 top: -3px;
1942 top: -3px;
1895 }
1943 }
1896 }
1944 }
1897
1945
1898
1946
1899 #close_pull_request {
1947 #close_pull_request {
1900 margin-right: 0px;
1948 margin-right: 0px;
1901 }
1949 }
1902
1950
1903 .empty_data {
1951 .empty_data {
1904 color: @grey4;
1952 color: @grey4;
1905 }
1953 }
1906
1954
1907 #changeset_compare_view_content {
1955 #changeset_compare_view_content {
1908 clear: both;
1956 clear: both;
1909 width: 100%;
1957 width: 100%;
1910 box-sizing: border-box;
1958 box-sizing: border-box;
1911 .border-radius(@border-radius);
1959 .border-radius(@border-radius);
1912
1960
1913 .help-block {
1961 .help-block {
1914 margin: @padding 0;
1962 margin: @padding 0;
1915 color: @text-color;
1963 color: @text-color;
1916 &.pre-formatting {
1964 &.pre-formatting {
1917 white-space: pre;
1965 white-space: pre;
1918 }
1966 }
1919 }
1967 }
1920
1968
1921 .empty_data {
1969 .empty_data {
1922 margin: @padding 0;
1970 margin: @padding 0;
1923 }
1971 }
1924
1972
1925 .alert {
1973 .alert {
1926 margin-bottom: @space;
1974 margin-bottom: @space;
1927 }
1975 }
1928 }
1976 }
1929
1977
1930 .table_disp {
1978 .table_disp {
1931 .status {
1979 .status {
1932 width: auto;
1980 width: auto;
1933 }
1981 }
1934 }
1982 }
1935
1983
1936
1984
1937 .creation_in_progress {
1985 .creation_in_progress {
1938 color: @grey4
1986 color: @grey4
1939 }
1987 }
1940
1988
1941 .status_box_menu {
1989 .status_box_menu {
1942 margin: 0;
1990 margin: 0;
1943 }
1991 }
1944
1992
1945 .notification-table{
1993 .notification-table{
1946 margin-bottom: @space;
1994 margin-bottom: @space;
1947 display: table;
1995 display: table;
1948 width: 100%;
1996 width: 100%;
1949
1997
1950 .container{
1998 .container{
1951 display: table-row;
1999 display: table-row;
1952
2000
1953 .notification-header{
2001 .notification-header{
1954 border-bottom: @border-thickness solid @border-default-color;
2002 border-bottom: @border-thickness solid @border-default-color;
1955 }
2003 }
1956
2004
1957 .notification-subject{
2005 .notification-subject{
1958 display: table-cell;
2006 display: table-cell;
1959 }
2007 }
1960 }
2008 }
1961 }
2009 }
1962
2010
1963 // Notifications
2011 // Notifications
1964 .notification-header{
2012 .notification-header{
1965 display: table;
2013 display: table;
1966 width: 100%;
2014 width: 100%;
1967 padding: floor(@basefontsize/2) 0;
2015 padding: floor(@basefontsize/2) 0;
1968 line-height: 1em;
2016 line-height: 1em;
1969
2017
1970 .desc, .delete-notifications, .read-notifications{
2018 .desc, .delete-notifications, .read-notifications{
1971 display: table-cell;
2019 display: table-cell;
1972 text-align: left;
2020 text-align: left;
1973 }
2021 }
1974
2022
1975 .delete-notifications, .read-notifications{
2023 .delete-notifications, .read-notifications{
1976 width: 35px;
2024 width: 35px;
1977 min-width: 35px; //fixes when only one button is displayed
2025 min-width: 35px; //fixes when only one button is displayed
1978 }
2026 }
1979 }
2027 }
1980
2028
1981 .notification-body {
2029 .notification-body {
1982 .markdown-block,
2030 .markdown-block,
1983 .rst-block {
2031 .rst-block {
1984 padding: @padding 0;
2032 padding: @padding 0;
1985 }
2033 }
1986
2034
1987 .notification-subject {
2035 .notification-subject {
1988 padding: @textmargin 0;
2036 padding: @textmargin 0;
1989 border-bottom: @border-thickness solid @border-default-color;
2037 border-bottom: @border-thickness solid @border-default-color;
1990 }
2038 }
1991 }
2039 }
1992
2040
1993
2041
1994 .notifications_buttons{
2042 .notifications_buttons{
1995 float: right;
2043 float: right;
1996 }
2044 }
1997
2045
1998 #notification-status{
2046 #notification-status{
1999 display: inline;
2047 display: inline;
2000 }
2048 }
2001
2049
2002 // Repositories
2050 // Repositories
2003
2051
2004 #summary.fields{
2052 #summary.fields{
2005 display: table;
2053 display: table;
2006
2054
2007 .field{
2055 .field{
2008 display: table-row;
2056 display: table-row;
2009
2057
2010 .label-summary{
2058 .label-summary{
2011 display: table-cell;
2059 display: table-cell;
2012 min-width: @label-summary-minwidth;
2060 min-width: @label-summary-minwidth;
2013 padding-top: @padding/2;
2061 padding-top: @padding/2;
2014 padding-bottom: @padding/2;
2062 padding-bottom: @padding/2;
2015 padding-right: @padding/2;
2063 padding-right: @padding/2;
2016 }
2064 }
2017
2065
2018 .input{
2066 .input{
2019 display: table-cell;
2067 display: table-cell;
2020 padding: @padding/2;
2068 padding: @padding/2;
2021
2069
2022 input{
2070 input{
2023 min-width: 29em;
2071 min-width: 29em;
2024 padding: @padding/4;
2072 padding: @padding/4;
2025 }
2073 }
2026 }
2074 }
2027 .statistics, .downloads{
2075 .statistics, .downloads{
2028 .disabled{
2076 .disabled{
2029 color: @grey4;
2077 color: @grey4;
2030 }
2078 }
2031 }
2079 }
2032 }
2080 }
2033 }
2081 }
2034
2082
2035 #summary{
2083 #summary{
2036 width: 70%;
2084 width: 70%;
2037 }
2085 }
2038
2086
2039
2087
2040 // Journal
2088 // Journal
2041 .journal.title {
2089 .journal.title {
2042 h5 {
2090 h5 {
2043 float: left;
2091 float: left;
2044 margin: 0;
2092 margin: 0;
2045 width: 70%;
2093 width: 70%;
2046 }
2094 }
2047
2095
2048 ul {
2096 ul {
2049 float: right;
2097 float: right;
2050 display: inline-block;
2098 display: inline-block;
2051 margin: 0;
2099 margin: 0;
2052 width: 30%;
2100 width: 30%;
2053 text-align: right;
2101 text-align: right;
2054
2102
2055 li {
2103 li {
2056 display: inline;
2104 display: inline;
2057 font-size: @journal-fontsize;
2105 font-size: @journal-fontsize;
2058 line-height: 1em;
2106 line-height: 1em;
2059
2107
2060 list-style-type: none;
2108 list-style-type: none;
2061 }
2109 }
2062 }
2110 }
2063 }
2111 }
2064
2112
2065 .filterexample {
2113 .filterexample {
2066 position: absolute;
2114 position: absolute;
2067 top: 95px;
2115 top: 95px;
2068 left: @contentpadding;
2116 left: @contentpadding;
2069 color: @rcblue;
2117 color: @rcblue;
2070 font-size: 11px;
2118 font-size: 11px;
2071 font-family: @text-regular;
2119 font-family: @text-regular;
2072 cursor: help;
2120 cursor: help;
2073
2121
2074 &:hover {
2122 &:hover {
2075 color: @rcdarkblue;
2123 color: @rcdarkblue;
2076 }
2124 }
2077
2125
2078 @media (max-width:768px) {
2126 @media (max-width:768px) {
2079 position: relative;
2127 position: relative;
2080 top: auto;
2128 top: auto;
2081 left: auto;
2129 left: auto;
2082 display: block;
2130 display: block;
2083 }
2131 }
2084 }
2132 }
2085
2133
2086
2134
2087 #journal{
2135 #journal{
2088 margin-bottom: @space;
2136 margin-bottom: @space;
2089
2137
2090 .journal_day{
2138 .journal_day{
2091 margin-bottom: @textmargin/2;
2139 margin-bottom: @textmargin/2;
2092 padding-bottom: @textmargin/2;
2140 padding-bottom: @textmargin/2;
2093 font-size: @journal-fontsize;
2141 font-size: @journal-fontsize;
2094 border-bottom: @border-thickness solid @border-default-color;
2142 border-bottom: @border-thickness solid @border-default-color;
2095 }
2143 }
2096
2144
2097 .journal_container{
2145 .journal_container{
2098 margin-bottom: @space;
2146 margin-bottom: @space;
2099
2147
2100 .journal_user{
2148 .journal_user{
2101 display: inline-block;
2149 display: inline-block;
2102 }
2150 }
2103 .journal_action_container{
2151 .journal_action_container{
2104 display: block;
2152 display: block;
2105 margin-top: @textmargin;
2153 margin-top: @textmargin;
2106
2154
2107 div{
2155 div{
2108 display: inline;
2156 display: inline;
2109 }
2157 }
2110
2158
2111 div.journal_action_params{
2159 div.journal_action_params{
2112 display: block;
2160 display: block;
2113 }
2161 }
2114
2162
2115 div.journal_repo:after{
2163 div.journal_repo:after{
2116 content: "\A";
2164 content: "\A";
2117 white-space: pre;
2165 white-space: pre;
2118 }
2166 }
2119
2167
2120 div.date{
2168 div.date{
2121 display: block;
2169 display: block;
2122 margin-bottom: @textmargin;
2170 margin-bottom: @textmargin;
2123 }
2171 }
2124 }
2172 }
2125 }
2173 }
2126 }
2174 }
2127
2175
2128 // Files
2176 // Files
2129 .edit-file-title {
2177 .edit-file-title {
2130 font-size: 16px;
2178 font-size: 16px;
2131
2179
2132 .title-heading {
2180 .title-heading {
2133 padding: 2px;
2181 padding: 2px;
2134 }
2182 }
2135 }
2183 }
2136
2184
2137 .edit-file-fieldset {
2185 .edit-file-fieldset {
2138 margin: @sidebarpadding 0;
2186 margin: @sidebarpadding 0;
2139
2187
2140 .fieldset {
2188 .fieldset {
2141 .left-label {
2189 .left-label {
2142 width: 13%;
2190 width: 13%;
2143 }
2191 }
2144 .right-content {
2192 .right-content {
2145 width: 87%;
2193 width: 87%;
2146 max-width: 100%;
2194 max-width: 100%;
2147 }
2195 }
2148 .filename-label {
2196 .filename-label {
2149 margin-top: 13px;
2197 margin-top: 13px;
2150 }
2198 }
2151 .commit-message-label {
2199 .commit-message-label {
2152 margin-top: 4px;
2200 margin-top: 4px;
2153 }
2201 }
2154 .file-upload-input {
2202 .file-upload-input {
2155 input {
2203 input {
2156 display: none;
2204 display: none;
2157 }
2205 }
2158 margin-top: 10px;
2206 margin-top: 10px;
2159 }
2207 }
2160 .file-upload-label {
2208 .file-upload-label {
2161 margin-top: 10px;
2209 margin-top: 10px;
2162 }
2210 }
2163 p {
2211 p {
2164 margin-top: 5px;
2212 margin-top: 5px;
2165 }
2213 }
2166
2214
2167 }
2215 }
2168 .custom-path-link {
2216 .custom-path-link {
2169 margin-left: 5px;
2217 margin-left: 5px;
2170 }
2218 }
2171 #commit {
2219 #commit {
2172 resize: vertical;
2220 resize: vertical;
2173 }
2221 }
2174 }
2222 }
2175
2223
2176 .delete-file-preview {
2224 .delete-file-preview {
2177 max-height: 250px;
2225 max-height: 250px;
2178 }
2226 }
2179
2227
2180 .new-file,
2228 .new-file,
2181 #filter_activate,
2229 #filter_activate,
2182 #filter_deactivate {
2230 #filter_deactivate {
2183 float: right;
2231 float: right;
2184 margin: 0 0 0 10px;
2232 margin: 0 0 0 10px;
2185 }
2233 }
2186
2234
2187 .file-upload-transaction-wrapper {
2235 .file-upload-transaction-wrapper {
2188 margin-top: 57px;
2236 margin-top: 57px;
2189 clear: both;
2237 clear: both;
2190 }
2238 }
2191
2239
2192 .file-upload-transaction-wrapper .error {
2240 .file-upload-transaction-wrapper .error {
2193 color: @color5;
2241 color: @color5;
2194 }
2242 }
2195
2243
2196 .file-upload-transaction {
2244 .file-upload-transaction {
2197 min-height: 200px;
2245 min-height: 200px;
2198 padding: 54px;
2246 padding: 54px;
2199 border: 1px solid @grey5;
2247 border: 1px solid @grey5;
2200 text-align: center;
2248 text-align: center;
2201 clear: both;
2249 clear: both;
2202 }
2250 }
2203
2251
2204 .file-upload-transaction i {
2252 .file-upload-transaction i {
2205 font-size: 48px
2253 font-size: 48px
2206 }
2254 }
2207
2255
2208 h3.files_location{
2256 h3.files_location{
2209 line-height: 2.4em;
2257 line-height: 2.4em;
2210 }
2258 }
2211
2259
2212 .browser-nav {
2260 .browser-nav {
2213 width: 100%;
2261 width: 100%;
2214 display: table;
2262 display: table;
2215 margin-bottom: 20px;
2263 margin-bottom: 20px;
2216
2264
2217 .info_box {
2265 .info_box {
2218 float: left;
2266 float: left;
2219 display: inline-table;
2267 display: inline-table;
2220 height: 2.5em;
2268 height: 2.5em;
2221
2269
2222 .browser-cur-rev, .info_box_elem {
2270 .browser-cur-rev, .info_box_elem {
2223 display: table-cell;
2271 display: table-cell;
2224 vertical-align: middle;
2272 vertical-align: middle;
2225 }
2273 }
2226
2274
2227 .drop-menu {
2275 .drop-menu {
2228 margin: 0 10px;
2276 margin: 0 10px;
2229 }
2277 }
2230
2278
2231 .info_box_elem {
2279 .info_box_elem {
2232 border-top: @border-thickness solid @grey5;
2280 border-top: @border-thickness solid @grey5;
2233 border-bottom: @border-thickness solid @grey5;
2281 border-bottom: @border-thickness solid @grey5;
2234 box-shadow: @button-shadow;
2282 box-shadow: @button-shadow;
2235
2283
2236 #at_rev, a {
2284 #at_rev, a {
2237 padding: 0.6em 0.4em;
2285 padding: 0.6em 0.4em;
2238 margin: 0;
2286 margin: 0;
2239 .box-shadow(none);
2287 .box-shadow(none);
2240 border: 0;
2288 border: 0;
2241 height: 12px;
2289 height: 12px;
2242 color: @grey2;
2290 color: @grey2;
2243 }
2291 }
2244
2292
2245 input#at_rev {
2293 input#at_rev {
2246 max-width: 50px;
2294 max-width: 50px;
2247 text-align: center;
2295 text-align: center;
2248 }
2296 }
2249
2297
2250 &.previous {
2298 &.previous {
2251 border: @border-thickness solid @grey5;
2299 border: @border-thickness solid @grey5;
2252 border-top-left-radius: @border-radius;
2300 border-top-left-radius: @border-radius;
2253 border-bottom-left-radius: @border-radius;
2301 border-bottom-left-radius: @border-radius;
2254
2302
2255 &:hover {
2303 &:hover {
2256 border-color: @grey4;
2304 border-color: @grey4;
2257 }
2305 }
2258
2306
2259 .disabled {
2307 .disabled {
2260 color: @grey5;
2308 color: @grey5;
2261 cursor: not-allowed;
2309 cursor: not-allowed;
2262 opacity: 0.5;
2310 opacity: 0.5;
2263 }
2311 }
2264 }
2312 }
2265
2313
2266 &.next {
2314 &.next {
2267 border: @border-thickness solid @grey5;
2315 border: @border-thickness solid @grey5;
2268 border-top-right-radius: @border-radius;
2316 border-top-right-radius: @border-radius;
2269 border-bottom-right-radius: @border-radius;
2317 border-bottom-right-radius: @border-radius;
2270
2318
2271 &:hover {
2319 &:hover {
2272 border-color: @grey4;
2320 border-color: @grey4;
2273 }
2321 }
2274
2322
2275 .disabled {
2323 .disabled {
2276 color: @grey5;
2324 color: @grey5;
2277 cursor: not-allowed;
2325 cursor: not-allowed;
2278 opacity: 0.5;
2326 opacity: 0.5;
2279 }
2327 }
2280 }
2328 }
2281 }
2329 }
2282
2330
2283 .browser-cur-rev {
2331 .browser-cur-rev {
2284
2332
2285 span{
2333 span{
2286 margin: 0;
2334 margin: 0;
2287 color: @rcblue;
2335 color: @rcblue;
2288 height: 12px;
2336 height: 12px;
2289 display: inline-block;
2337 display: inline-block;
2290 padding: 0.7em 1em ;
2338 padding: 0.7em 1em ;
2291 border: @border-thickness solid @rcblue;
2339 border: @border-thickness solid @rcblue;
2292 margin-right: @padding;
2340 margin-right: @padding;
2293 }
2341 }
2294 }
2342 }
2295
2343
2296 }
2344 }
2297
2345
2298 .select-index-number {
2346 .select-index-number {
2299 margin: 0 0 0 20px;
2347 margin: 0 0 0 20px;
2300 color: @grey3;
2348 color: @grey3;
2301 }
2349 }
2302
2350
2303 .search_activate {
2351 .search_activate {
2304 display: table-cell;
2352 display: table-cell;
2305 vertical-align: middle;
2353 vertical-align: middle;
2306
2354
2307 input, label{
2355 input, label{
2308 margin: 0;
2356 margin: 0;
2309 padding: 0;
2357 padding: 0;
2310 }
2358 }
2311
2359
2312 input{
2360 input{
2313 margin-left: @textmargin;
2361 margin-left: @textmargin;
2314 }
2362 }
2315
2363
2316 }
2364 }
2317 }
2365 }
2318
2366
2319 .browser-cur-rev{
2367 .browser-cur-rev{
2320 margin-bottom: @textmargin;
2368 margin-bottom: @textmargin;
2321 }
2369 }
2322
2370
2323 #node_filter_box_loading{
2371 #node_filter_box_loading{
2324 .info_text;
2372 .info_text;
2325 }
2373 }
2326
2374
2327 .browser-search {
2375 .browser-search {
2328 margin: -25px 0px 5px 0px;
2376 margin: -25px 0px 5px 0px;
2329 }
2377 }
2330
2378
2331 .files-quick-filter {
2379 .files-quick-filter {
2332 float: right;
2380 float: right;
2333 width: 180px;
2381 width: 180px;
2334 position: relative;
2382 position: relative;
2335 }
2383 }
2336
2384
2337 .files-filter-box {
2385 .files-filter-box {
2338 display: flex;
2386 display: flex;
2339 padding: 0px;
2387 padding: 0px;
2340 border-radius: 3px;
2388 border-radius: 3px;
2341 margin-bottom: 0;
2389 margin-bottom: 0;
2342
2390
2343 a {
2391 a {
2344 border: none !important;
2392 border: none !important;
2345 }
2393 }
2346
2394
2347 li {
2395 li {
2348 list-style-type: none
2396 list-style-type: none
2349 }
2397 }
2350 }
2398 }
2351
2399
2352 .files-filter-box-path {
2400 .files-filter-box-path {
2353 line-height: 33px;
2401 line-height: 33px;
2354 padding: 0;
2402 padding: 0;
2355 width: 20px;
2403 width: 20px;
2356 position: absolute;
2404 position: absolute;
2357 z-index: 11;
2405 z-index: 11;
2358 left: 5px;
2406 left: 5px;
2359 }
2407 }
2360
2408
2361 .files-filter-box-input {
2409 .files-filter-box-input {
2362 margin-right: 0;
2410 margin-right: 0;
2363
2411
2364 input {
2412 input {
2365 border: 1px solid @white;
2413 border: 1px solid @white;
2366 padding-left: 25px;
2414 padding-left: 25px;
2367 width: 145px;
2415 width: 145px;
2368
2416
2369 &:hover {
2417 &:hover {
2370 border-color: @grey6;
2418 border-color: @grey6;
2371 }
2419 }
2372
2420
2373 &:focus {
2421 &:focus {
2374 border-color: @grey5;
2422 border-color: @grey5;
2375 }
2423 }
2376 }
2424 }
2377 }
2425 }
2378
2426
2379 .browser-result{
2427 .browser-result{
2380 td a{
2428 td a{
2381 margin-left: 0.5em;
2429 margin-left: 0.5em;
2382 display: inline-block;
2430 display: inline-block;
2383
2431
2384 em {
2432 em {
2385 font-weight: @text-bold-weight;
2433 font-weight: @text-bold-weight;
2386 font-family: @text-bold;
2434 font-family: @text-bold;
2387 }
2435 }
2388 }
2436 }
2389 }
2437 }
2390
2438
2391 .browser-highlight{
2439 .browser-highlight{
2392 background-color: @grey5-alpha;
2440 background-color: @grey5-alpha;
2393 }
2441 }
2394
2442
2395
2443
2396 .edit-file-fieldset #location,
2444 .edit-file-fieldset #location,
2397 .edit-file-fieldset #filename {
2445 .edit-file-fieldset #filename {
2398 display: flex;
2446 display: flex;
2399 width: -moz-available; /* WebKit-based browsers will ignore this. */
2447 width: -moz-available; /* WebKit-based browsers will ignore this. */
2400 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2448 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2401 width: fill-available;
2449 width: fill-available;
2402 border: 0;
2450 border: 0;
2403 }
2451 }
2404
2452
2405 .path-items {
2453 .path-items {
2406 display: flex;
2454 display: flex;
2407 padding: 0;
2455 padding: 0;
2408 border: 1px solid #eeeeee;
2456 border: 1px solid #eeeeee;
2409 width: 100%;
2457 width: 100%;
2410 float: left;
2458 float: left;
2411
2459
2412 .breadcrumb-path {
2460 .breadcrumb-path {
2413 line-height: 30px;
2461 line-height: 30px;
2414 padding: 0 4px;
2462 padding: 0 4px;
2415 white-space: nowrap;
2463 white-space: nowrap;
2416 }
2464 }
2417
2465
2418 .location-path {
2466 .location-path {
2419 width: -moz-available; /* WebKit-based browsers will ignore this. */
2467 width: -moz-available; /* WebKit-based browsers will ignore this. */
2420 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2468 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2421 width: fill-available;
2469 width: fill-available;
2422
2470
2423 .file-name-input {
2471 .file-name-input {
2424 padding: 0.5em 0;
2472 padding: 0.5em 0;
2425 }
2473 }
2426
2474
2427 }
2475 }
2428
2476
2429 ul {
2477 ul {
2430 display: flex;
2478 display: flex;
2431 margin: 0;
2479 margin: 0;
2432 padding: 0;
2480 padding: 0;
2433 width: 100%;
2481 width: 100%;
2434 }
2482 }
2435
2483
2436 li {
2484 li {
2437 list-style-type: none;
2485 list-style-type: none;
2438 }
2486 }
2439
2487
2440 }
2488 }
2441
2489
2442 .editor-items {
2490 .editor-items {
2443 height: 40px;
2491 height: 40px;
2444 margin: 10px 0 -17px 10px;
2492 margin: 10px 0 -17px 10px;
2445
2493
2446 .editor-action {
2494 .editor-action {
2447 cursor: pointer;
2495 cursor: pointer;
2448 }
2496 }
2449
2497
2450 .editor-action.active {
2498 .editor-action.active {
2451 border-bottom: 2px solid #5C5C5C;
2499 border-bottom: 2px solid #5C5C5C;
2452 }
2500 }
2453
2501
2454 li {
2502 li {
2455 list-style-type: none;
2503 list-style-type: none;
2456 }
2504 }
2457 }
2505 }
2458
2506
2459 .edit-file-fieldset .message textarea {
2507 .edit-file-fieldset .message textarea {
2460 border: 1px solid #eeeeee;
2508 border: 1px solid #eeeeee;
2461 }
2509 }
2462
2510
2463 #files_data .codeblock {
2511 #files_data .codeblock {
2464 background-color: #F5F5F5;
2512 background-color: #F5F5F5;
2465 }
2513 }
2466
2514
2467 #editor_preview {
2515 #editor_preview {
2468 background: white;
2516 background: white;
2469 }
2517 }
2470
2518
2471 .show-editor {
2519 .show-editor {
2472 padding: 10px;
2520 padding: 10px;
2473 background-color: white;
2521 background-color: white;
2474
2522
2475 }
2523 }
2476
2524
2477 .show-preview {
2525 .show-preview {
2478 padding: 10px;
2526 padding: 10px;
2479 background-color: white;
2527 background-color: white;
2480 border-left: 1px solid #eeeeee;
2528 border-left: 1px solid #eeeeee;
2481 }
2529 }
2482 // quick filter
2530 // quick filter
2483 .grid-quick-filter {
2531 .grid-quick-filter {
2484 float: right;
2532 float: right;
2485 position: relative;
2533 position: relative;
2486 }
2534 }
2487
2535
2488 .grid-filter-box {
2536 .grid-filter-box {
2489 display: flex;
2537 display: flex;
2490 padding: 0px;
2538 padding: 0px;
2491 border-radius: 3px;
2539 border-radius: 3px;
2492 margin-bottom: 0;
2540 margin-bottom: 0;
2493
2541
2494 a {
2542 a {
2495 border: none !important;
2543 border: none !important;
2496 }
2544 }
2497
2545
2498 li {
2546 li {
2499 list-style-type: none
2547 list-style-type: none
2500 }
2548 }
2501 }
2549 }
2502
2550
2503 .grid-filter-box-icon {
2551 .grid-filter-box-icon {
2504 line-height: 33px;
2552 line-height: 33px;
2505 padding: 0;
2553 padding: 0;
2506 width: 20px;
2554 width: 20px;
2507 position: absolute;
2555 position: absolute;
2508 z-index: 11;
2556 z-index: 11;
2509 left: 5px;
2557 left: 5px;
2510 }
2558 }
2511
2559
2512 .grid-filter-box-input {
2560 .grid-filter-box-input {
2513 margin-right: 0;
2561 margin-right: 0;
2514
2562
2515 input {
2563 input {
2516 border: 1px solid @white;
2564 border: 1px solid @white;
2517 padding-left: 25px;
2565 padding-left: 25px;
2518 width: 145px;
2566 width: 145px;
2519
2567
2520 &:hover {
2568 &:hover {
2521 border-color: @grey6;
2569 border-color: @grey6;
2522 }
2570 }
2523
2571
2524 &:focus {
2572 &:focus {
2525 border-color: @grey5;
2573 border-color: @grey5;
2526 }
2574 }
2527 }
2575 }
2528 }
2576 }
2529
2577
2530
2578
2531
2579
2532 // Search
2580 // Search
2533
2581
2534 .search-form{
2582 .search-form{
2535 #q {
2583 #q {
2536 width: @search-form-width;
2584 width: @search-form-width;
2537 }
2585 }
2538 .fields{
2586 .fields{
2539 margin: 0 0 @space;
2587 margin: 0 0 @space;
2540 }
2588 }
2541
2589
2542 label{
2590 label{
2543 display: inline-block;
2591 display: inline-block;
2544 margin-right: @textmargin;
2592 margin-right: @textmargin;
2545 padding-top: 0.25em;
2593 padding-top: 0.25em;
2546 }
2594 }
2547
2595
2548
2596
2549 .results{
2597 .results{
2550 clear: both;
2598 clear: both;
2551 margin: 0 0 @padding;
2599 margin: 0 0 @padding;
2552 }
2600 }
2553
2601
2554 .search-tags {
2602 .search-tags {
2555 padding: 5px 0;
2603 padding: 5px 0;
2556 }
2604 }
2557 }
2605 }
2558
2606
2559 div.search-feedback-items {
2607 div.search-feedback-items {
2560 display: inline-block;
2608 display: inline-block;
2561 }
2609 }
2562
2610
2563 div.search-code-body {
2611 div.search-code-body {
2564 background-color: #ffffff; padding: 5px 0 5px 10px;
2612 background-color: #ffffff; padding: 5px 0 5px 10px;
2565 pre {
2613 pre {
2566 .match { background-color: #faffa6;}
2614 .match { background-color: #faffa6;}
2567 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2615 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2568 }
2616 }
2569 }
2617 }
2570
2618
2571 .expand_commit.search {
2619 .expand_commit.search {
2572 .show_more.open {
2620 .show_more.open {
2573 height: auto;
2621 height: auto;
2574 max-height: none;
2622 max-height: none;
2575 }
2623 }
2576 }
2624 }
2577
2625
2578 .search-results {
2626 .search-results {
2579
2627
2580 h2 {
2628 h2 {
2581 margin-bottom: 0;
2629 margin-bottom: 0;
2582 }
2630 }
2583 .codeblock {
2631 .codeblock {
2584 border: none;
2632 border: none;
2585 background: transparent;
2633 background: transparent;
2586 }
2634 }
2587
2635
2588 .codeblock-header {
2636 .codeblock-header {
2589 border: none;
2637 border: none;
2590 background: transparent;
2638 background: transparent;
2591 }
2639 }
2592
2640
2593 .code-body {
2641 .code-body {
2594 border: @border-thickness solid @grey6;
2642 border: @border-thickness solid @grey6;
2595 .border-radius(@border-radius);
2643 .border-radius(@border-radius);
2596 }
2644 }
2597
2645
2598 .td-commit {
2646 .td-commit {
2599 &:extend(pre);
2647 &:extend(pre);
2600 border-bottom: @border-thickness solid @border-default-color;
2648 border-bottom: @border-thickness solid @border-default-color;
2601 }
2649 }
2602
2650
2603 .message {
2651 .message {
2604 height: auto;
2652 height: auto;
2605 max-width: 350px;
2653 max-width: 350px;
2606 white-space: normal;
2654 white-space: normal;
2607 text-overflow: initial;
2655 text-overflow: initial;
2608 overflow: visible;
2656 overflow: visible;
2609
2657
2610 .match { background-color: #faffa6;}
2658 .match { background-color: #faffa6;}
2611 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2659 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2612 }
2660 }
2613
2661
2614 .path {
2662 .path {
2615 border-bottom: none !important;
2663 border-bottom: none !important;
2616 border-left: 1px solid @grey6 !important;
2664 border-left: 1px solid @grey6 !important;
2617 border-right: 1px solid @grey6 !important;
2665 border-right: 1px solid @grey6 !important;
2618 }
2666 }
2619 }
2667 }
2620
2668
2621 table.rctable td.td-search-results div {
2669 table.rctable td.td-search-results div {
2622 max-width: 100%;
2670 max-width: 100%;
2623 }
2671 }
2624
2672
2625 #tip-box, .tip-box{
2673 #tip-box, .tip-box{
2626 padding: @menupadding/2;
2674 padding: @menupadding/2;
2627 display: block;
2675 display: block;
2628 border: @border-thickness solid @border-highlight-color;
2676 border: @border-thickness solid @border-highlight-color;
2629 .border-radius(@border-radius);
2677 .border-radius(@border-radius);
2630 background-color: white;
2678 background-color: white;
2631 z-index: 99;
2679 z-index: 99;
2632 white-space: pre-wrap;
2680 white-space: pre-wrap;
2633 }
2681 }
2634
2682
2635 #linktt {
2683 #linktt {
2636 width: 79px;
2684 width: 79px;
2637 }
2685 }
2638
2686
2639 #help_kb .modal-content{
2687 #help_kb .modal-content{
2640 max-width: 750px;
2688 max-width: 750px;
2641 margin: 10% auto;
2689 margin: 10% auto;
2642
2690
2643 table{
2691 table{
2644 td,th{
2692 td,th{
2645 border-bottom: none;
2693 border-bottom: none;
2646 line-height: 2.5em;
2694 line-height: 2.5em;
2647 }
2695 }
2648 th{
2696 th{
2649 padding-bottom: @textmargin/2;
2697 padding-bottom: @textmargin/2;
2650 }
2698 }
2651 td.keys{
2699 td.keys{
2652 text-align: center;
2700 text-align: center;
2653 }
2701 }
2654 }
2702 }
2655
2703
2656 .block-left{
2704 .block-left{
2657 width: 45%;
2705 width: 45%;
2658 margin-right: 5%;
2706 margin-right: 5%;
2659 }
2707 }
2660 .modal-footer{
2708 .modal-footer{
2661 clear: both;
2709 clear: both;
2662 }
2710 }
2663 .key.tag{
2711 .key.tag{
2664 padding: 0.5em;
2712 padding: 0.5em;
2665 background-color: @rcblue;
2713 background-color: @rcblue;
2666 color: white;
2714 color: white;
2667 border-color: @rcblue;
2715 border-color: @rcblue;
2668 .box-shadow(none);
2716 .box-shadow(none);
2669 }
2717 }
2670 }
2718 }
2671
2719
2672
2720
2673
2721
2674 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2722 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2675
2723
2676 @import 'statistics-graph';
2724 @import 'statistics-graph';
2677 @import 'tables';
2725 @import 'tables';
2678 @import 'forms';
2726 @import 'forms';
2679 @import 'diff';
2727 @import 'diff';
2680 @import 'summary';
2728 @import 'summary';
2681 @import 'navigation';
2729 @import 'navigation';
2682
2730
2683 //--- SHOW/HIDE SECTIONS --//
2731 //--- SHOW/HIDE SECTIONS --//
2684
2732
2685 .btn-collapse {
2733 .btn-collapse {
2686 float: right;
2734 float: right;
2687 text-align: right;
2735 text-align: right;
2688 font-family: @text-light;
2736 font-family: @text-light;
2689 font-size: @basefontsize;
2737 font-size: @basefontsize;
2690 cursor: pointer;
2738 cursor: pointer;
2691 border: none;
2739 border: none;
2692 color: @rcblue;
2740 color: @rcblue;
2693 }
2741 }
2694
2742
2695 table.rctable,
2743 table.rctable,
2696 table.dataTable {
2744 table.dataTable {
2697 .btn-collapse {
2745 .btn-collapse {
2698 float: right;
2746 float: right;
2699 text-align: right;
2747 text-align: right;
2700 }
2748 }
2701 }
2749 }
2702
2750
2703 table.rctable {
2751 table.rctable {
2704 &.permissions {
2752 &.permissions {
2705
2753
2706 th.td-owner {
2754 th.td-owner {
2707 padding: 0;
2755 padding: 0;
2708 }
2756 }
2709
2757
2710 th {
2758 th {
2711 font-weight: normal;
2759 font-weight: normal;
2712 padding: 0 5px;
2760 padding: 0 5px;
2713 }
2761 }
2714
2762
2715 }
2763 }
2716 }
2764 }
2717
2765
2718
2766
2719 // TODO: johbo: Fix for IE10, this avoids that we see a border
2767 // TODO: johbo: Fix for IE10, this avoids that we see a border
2720 // and padding around checkboxes and radio boxes. Move to the right place,
2768 // and padding around checkboxes and radio boxes. Move to the right place,
2721 // or better: Remove this once we did the form refactoring.
2769 // or better: Remove this once we did the form refactoring.
2722 input[type=checkbox],
2770 input[type=checkbox],
2723 input[type=radio] {
2771 input[type=radio] {
2724 padding: 0;
2772 padding: 0;
2725 border: none;
2773 border: none;
2726 }
2774 }
2727
2775
2728 .toggle-ajax-spinner{
2776 .toggle-ajax-spinner{
2729 height: 16px;
2777 height: 16px;
2730 width: 16px;
2778 width: 16px;
2731 }
2779 }
2732
2780
2733
2781
2734 .markup-form .clearfix {
2782 .markup-form .clearfix {
2735 .border-radius(@border-radius);
2783 .border-radius(@border-radius);
2736 margin: 0px;
2784 margin: 0px;
2737 }
2785 }
2738
2786
2739 .markup-form-area {
2787 .markup-form-area {
2740 padding: 8px 12px;
2788 padding: 8px 12px;
2741 border: 1px solid @grey4;
2789 border: 1px solid @grey4;
2742 .border-radius(@border-radius);
2790 .border-radius(@border-radius);
2743 }
2791 }
2744
2792
2745 .markup-form-area-header .nav-links {
2793 .markup-form-area-header .nav-links {
2746 display: flex;
2794 display: flex;
2747 flex-flow: row wrap;
2795 flex-flow: row wrap;
2748 -webkit-flex-flow: row wrap;
2796 -webkit-flex-flow: row wrap;
2749 width: 100%;
2797 width: 100%;
2750 }
2798 }
2751
2799
2752 .markup-form-area-footer {
2800 .markup-form-area-footer {
2753 display: flex;
2801 display: flex;
2754 }
2802 }
2755
2803
2756 .markup-form-area-footer .toolbar {
2804 .markup-form-area-footer .toolbar {
2757
2805
2758 }
2806 }
2759
2807
2760 // markup Form
2808 // markup Form
2761 div.markup-form {
2809 div.markup-form {
2762 margin-top: 20px;
2810 margin-top: 20px;
2763 }
2811 }
2764
2812
2765 .markup-form strong {
2813 .markup-form strong {
2766 display: block;
2814 display: block;
2767 margin-bottom: 15px;
2815 margin-bottom: 15px;
2768 }
2816 }
2769
2817
2770 .markup-form textarea {
2818 .markup-form textarea {
2771 width: 100%;
2819 width: 100%;
2772 height: 100px;
2820 height: 100px;
2773 font-family: @text-monospace;
2821 font-family: @text-monospace;
2774 }
2822 }
2775
2823
2776 form.markup-form {
2824 form.markup-form {
2777 margin-top: 10px;
2825 margin-top: 10px;
2778 margin-left: 10px;
2826 margin-left: 10px;
2779 }
2827 }
2780
2828
2781 .markup-form .comment-block-ta,
2829 .markup-form .comment-block-ta,
2782 .markup-form .preview-box {
2830 .markup-form .preview-box {
2783 .border-radius(@border-radius);
2831 .border-radius(@border-radius);
2784 .box-sizing(border-box);
2832 .box-sizing(border-box);
2785 background-color: white;
2833 background-color: white;
2786 }
2834 }
2787
2835
2788 .markup-form .preview-box.unloaded {
2836 .markup-form .preview-box.unloaded {
2789 height: 50px;
2837 height: 50px;
2790 text-align: center;
2838 text-align: center;
2791 padding: 20px;
2839 padding: 20px;
2792 background-color: white;
2840 background-color: white;
2793 }
2841 }
2794
2842
2795
2843
2796 .dropzone-wrapper {
2844 .dropzone-wrapper {
2797 border: 1px solid @grey5;
2845 border: 1px solid @grey5;
2798 padding: 20px;
2846 padding: 20px;
2799 }
2847 }
2800
2848
2801 .dropzone,
2849 .dropzone,
2802 .dropzone-pure {
2850 .dropzone-pure {
2803 border: 2px dashed @grey5;
2851 border: 2px dashed @grey5;
2804 border-radius: 5px;
2852 border-radius: 5px;
2805 background: white;
2853 background: white;
2806 min-height: 200px;
2854 min-height: 200px;
2807 padding: 54px;
2855 padding: 54px;
2808
2856
2809 .dz-message {
2857 .dz-message {
2810 font-weight: 700;
2858 font-weight: 700;
2811 text-align: center;
2859 text-align: center;
2812 margin: 2em 0;
2860 margin: 2em 0;
2813 }
2861 }
2814
2862
2815 }
2863 }
2816
2864
2817 .dz-preview {
2865 .dz-preview {
2818 margin: 10px 0 !important;
2866 margin: 10px 0 !important;
2819 position: relative;
2867 position: relative;
2820 vertical-align: top;
2868 vertical-align: top;
2821 padding: 10px;
2869 padding: 10px;
2822 border-bottom: 1px solid @grey5;
2870 border-bottom: 1px solid @grey5;
2823 }
2871 }
2824
2872
2825 .dz-filename {
2873 .dz-filename {
2826 font-weight: 700;
2874 font-weight: 700;
2827 float: left;
2875 float: left;
2828 }
2876 }
2829
2877
2830 .dz-sending {
2878 .dz-sending {
2831 float: right;
2879 float: right;
2832 }
2880 }
2833
2881
2834 .dz-response {
2882 .dz-response {
2835 clear: both
2883 clear: both
2836 }
2884 }
2837
2885
2838 .dz-filename-size {
2886 .dz-filename-size {
2839 float: right
2887 float: right
2840 }
2888 }
2841
2889
2842 .dz-error-message {
2890 .dz-error-message {
2843 color: @alert2;
2891 color: @alert2;
2844 padding-top: 10px;
2892 padding-top: 10px;
2845 clear: both;
2893 clear: both;
2846 }
2894 }
2847
2895
2848
2896
2849 .user-hovercard {
2897 .user-hovercard {
2850 padding: 5px;
2898 padding: 5px;
2851 }
2899 }
2852
2900
2853 .user-hovercard-icon {
2901 .user-hovercard-icon {
2854 display: inline;
2902 display: inline;
2855 padding: 0;
2903 padding: 0;
2856 box-sizing: content-box;
2904 box-sizing: content-box;
2857 border-radius: 50%;
2905 border-radius: 50%;
2858 float: left;
2906 float: left;
2859 }
2907 }
2860
2908
2861 .user-hovercard-name {
2909 .user-hovercard-name {
2862 float: right;
2910 float: right;
2863 vertical-align: top;
2911 vertical-align: top;
2864 padding-left: 10px;
2912 padding-left: 10px;
2865 min-width: 150px;
2913 min-width: 150px;
2866 }
2914 }
2867
2915
2868 .user-hovercard-bio {
2916 .user-hovercard-bio {
2869 clear: both;
2917 clear: both;
2870 padding-top: 10px;
2918 padding-top: 10px;
2871 }
2919 }
2872
2920
2873 .user-hovercard-header {
2921 .user-hovercard-header {
2874 clear: both;
2922 clear: both;
2875 min-height: 10px;
2923 min-height: 10px;
2876 }
2924 }
2877
2925
2878 .user-hovercard-footer {
2926 .user-hovercard-footer {
2879 clear: both;
2927 clear: both;
2880 min-height: 10px;
2928 min-height: 10px;
2881 }
2929 }
2882
2930
2883 .user-group-hovercard {
2931 .user-group-hovercard {
2884 padding: 5px;
2932 padding: 5px;
2885 }
2933 }
2886
2934
2887 .user-group-hovercard-icon {
2935 .user-group-hovercard-icon {
2888 display: inline;
2936 display: inline;
2889 padding: 0;
2937 padding: 0;
2890 box-sizing: content-box;
2938 box-sizing: content-box;
2891 border-radius: 50%;
2939 border-radius: 50%;
2892 float: left;
2940 float: left;
2893 }
2941 }
2894
2942
2895 .user-group-hovercard-name {
2943 .user-group-hovercard-name {
2896 float: left;
2944 float: left;
2897 vertical-align: top;
2945 vertical-align: top;
2898 padding-left: 10px;
2946 padding-left: 10px;
2899 min-width: 150px;
2947 min-width: 150px;
2900 }
2948 }
2901
2949
2902 .user-group-hovercard-icon i {
2950 .user-group-hovercard-icon i {
2903 border: 1px solid @grey4;
2951 border: 1px solid @grey4;
2904 border-radius: 4px;
2952 border-radius: 4px;
2905 }
2953 }
2906
2954
2907 .user-group-hovercard-bio {
2955 .user-group-hovercard-bio {
2908 clear: both;
2956 clear: both;
2909 padding-top: 10px;
2957 padding-top: 10px;
2910 line-height: 1.0em;
2958 line-height: 1.0em;
2911 }
2959 }
2912
2960
2913 .user-group-hovercard-header {
2961 .user-group-hovercard-header {
2914 clear: both;
2962 clear: both;
2915 min-height: 10px;
2963 min-height: 10px;
2916 }
2964 }
2917
2965
2918 .user-group-hovercard-footer {
2966 .user-group-hovercard-footer {
2919 clear: both;
2967 clear: both;
2920 min-height: 10px;
2968 min-height: 10px;
2921 }
2969 }
2922
2970
2923 .pr-hovercard-header {
2971 .pr-hovercard-header {
2924 clear: both;
2972 clear: both;
2925 display: block;
2973 display: block;
2926 line-height: 20px;
2974 line-height: 20px;
2927 }
2975 }
2928
2976
2929 .pr-hovercard-user {
2977 .pr-hovercard-user {
2930 display: flex;
2978 display: flex;
2931 align-items: center;
2979 align-items: center;
2932 padding-left: 5px;
2980 padding-left: 5px;
2933 }
2981 }
2934
2982
2935 .pr-hovercard-title {
2983 .pr-hovercard-title {
2936 padding-top: 5px;
2984 padding-top: 5px;
2937 } No newline at end of file
2985 }
@@ -1,608 +1,623 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#pr_submit').attr('disabled', 'disabled');
44 $('#pr_submit').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#pr_submit').removeAttr('disabled');
47 $('#pr_submit').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0) {
140 if (data.rules.voting < 0) {
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All individual reviewers must vote.'))
143 _gettext('All individual reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158
158
159 if (data.rules.voting_groups !== undefined) {
159 if (data.rules.voting_groups !== undefined) {
160 $.each(data.rules.voting_groups, function(index, rule_data) {
160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 self.$rulesList.append(
161 self.$rulesList.append(
162 self.addRule(rule_data.text)
162 self.addRule(rule_data.text)
163 )
163 )
164 });
164 });
165 }
165 }
166
166
167 if (data.rules.use_code_authors_for_review) {
167 if (data.rules.use_code_authors_for_review) {
168 self.$rulesList.append(
168 self.$rulesList.append(
169 self.addRule(
169 self.addRule(
170 _gettext('Reviewers picked from source code changes.'))
170 _gettext('Reviewers picked from source code changes.'))
171 )
171 )
172 }
172 }
173 if (data.rules.forbid_adding_reviewers) {
173 if (data.rules.forbid_adding_reviewers) {
174 $('#add_reviewer_input').remove();
174 $('#add_reviewer_input').remove();
175 self.$rulesList.append(
175 self.$rulesList.append(
176 self.addRule(
176 self.addRule(
177 _gettext('Adding new reviewers is forbidden.'))
177 _gettext('Adding new reviewers is forbidden.'))
178 )
178 )
179 }
179 }
180 if (data.rules.forbid_author_to_review) {
180 if (data.rules.forbid_author_to_review) {
181 self.forbidReviewUsers.push(data.rules_data.pr_author);
181 self.forbidReviewUsers.push(data.rules_data.pr_author);
182 self.$rulesList.append(
182 self.$rulesList.append(
183 self.addRule(
183 self.addRule(
184 _gettext('Author is not allowed to be a reviewer.'))
184 _gettext('Author is not allowed to be a reviewer.'))
185 )
185 )
186 }
186 }
187 if (data.rules.forbid_commit_author_to_review) {
187 if (data.rules.forbid_commit_author_to_review) {
188
188
189 if (data.rules_data.forbidden_users) {
189 if (data.rules_data.forbidden_users) {
190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
191 self.forbidReviewUsers.push(member_data)
191 self.forbidReviewUsers.push(member_data)
192 });
192 });
193
193
194 }
194 }
195
195
196 self.$rulesList.append(
196 self.$rulesList.append(
197 self.addRule(
197 self.addRule(
198 _gettext('Commit Authors are not allowed to be a reviewer.'))
198 _gettext('Commit Authors are not allowed to be a reviewer.'))
199 )
199 )
200 }
200 }
201
201
202 return self.forbidReviewUsers
202 return self.forbidReviewUsers
203 };
203 };
204
204
205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
206
206
207 if (self.currentRequest) {
207 if (self.currentRequest) {
208 // make sure we cleanup old running requests before triggering this
208 // make sure we cleanup old running requests before triggering this
209 // again
209 // again
210 self.currentRequest.abort();
210 self.currentRequest.abort();
211 }
211 }
212
212
213 $('.calculate-reviewers').show();
213 $('.calculate-reviewers').show();
214 // reset reviewer members
214 // reset reviewer members
215 self.$reviewMembers.empty();
215 self.$reviewMembers.empty();
216
216
217 prButtonLock(true, null, 'reviewers');
217 prButtonLock(true, null, 'reviewers');
218 $('#user').hide(); // hide user autocomplete before load
218 $('#user').hide(); // hide user autocomplete before load
219
219
220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
221 // don't load defaults in case we're missing some refs...
221 // don't load defaults in case we're missing some refs...
222 $('.calculate-reviewers').hide();
222 $('.calculate-reviewers').hide();
223 return
223 return
224 }
224 }
225
225
226 var url = pyroutes.url('repo_default_reviewers_data',
226 var url = pyroutes.url('repo_default_reviewers_data',
227 {
227 {
228 'repo_name': templateContext.repo_name,
228 'repo_name': templateContext.repo_name,
229 'source_repo': sourceRepo,
229 'source_repo': sourceRepo,
230 'source_ref': sourceRef[2],
230 'source_ref': sourceRef[2],
231 'target_repo': targetRepo,
231 'target_repo': targetRepo,
232 'target_ref': targetRef[2]
232 'target_ref': targetRef[2]
233 });
233 });
234
234
235 self.currentRequest = $.get(url)
235 self.currentRequest = $.get(url)
236 .done(function(data) {
236 .done(function(data) {
237 self.currentRequest = null;
237 self.currentRequest = null;
238
238
239 // review rules
239 // review rules
240 self.loadReviewRules(data);
240 self.loadReviewRules(data);
241
241
242 for (var i = 0; i < data.reviewers.length; i++) {
242 for (var i = 0; i < data.reviewers.length; i++) {
243 var reviewer = data.reviewers[i];
243 var reviewer = data.reviewers[i];
244 self.addReviewMember(
244 self.addReviewMember(
245 reviewer, reviewer.reasons, reviewer.mandatory);
245 reviewer, reviewer.reasons, reviewer.mandatory);
246 }
246 }
247 $('.calculate-reviewers').hide();
247 $('.calculate-reviewers').hide();
248 prButtonLock(false, null, 'reviewers');
248 prButtonLock(false, null, 'reviewers');
249 $('#user').show(); // show user autocomplete after load
249 $('#user').show(); // show user autocomplete after load
250 });
250 });
251 };
251 };
252
252
253 // check those, refactor
253 // check those, refactor
254 this.removeReviewMember = function(reviewer_id, mark_delete) {
254 this.removeReviewMember = function(reviewer_id, mark_delete) {
255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
256
256
257 if(typeof(mark_delete) === undefined){
257 if(typeof(mark_delete) === undefined){
258 mark_delete = false;
258 mark_delete = false;
259 }
259 }
260
260
261 if(mark_delete === true){
261 if(mark_delete === true){
262 if (reviewer){
262 if (reviewer){
263 // now delete the input
263 // now delete the input
264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
265 // mark as to-delete
265 // mark as to-delete
266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
267 obj.addClass('to-delete');
267 obj.addClass('to-delete');
268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
269 }
269 }
270 }
270 }
271 else{
271 else{
272 $('#reviewer_{0}'.format(reviewer_id)).remove();
272 $('#reviewer_{0}'.format(reviewer_id)).remove();
273 }
273 }
274 };
274 };
275 this.reviewMemberEntry = function() {
275 this.reviewMemberEntry = function() {
276
276
277 };
277 };
278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
279 var members = self.$reviewMembers.get(0);
279 var members = self.$reviewMembers.get(0);
280 var id = reviewer_obj.user_id;
280 var id = reviewer_obj.user_id;
281 var username = reviewer_obj.username;
281 var username = reviewer_obj.username;
282
282
283 var reasons = reasons || [];
283 var reasons = reasons || [];
284 var mandatory = mandatory || false;
284 var mandatory = mandatory || false;
285
285
286 // register IDS to check if we don't have this ID already in
286 // register IDS to check if we don't have this ID already in
287 var currentIds = [];
287 var currentIds = [];
288 var _els = self.$reviewMembers.find('li').toArray();
288 var _els = self.$reviewMembers.find('li').toArray();
289 for (el in _els){
289 for (el in _els){
290 currentIds.push(_els[el].id)
290 currentIds.push(_els[el].id)
291 }
291 }
292
292
293 var userAllowedReview = function(userId) {
293 var userAllowedReview = function(userId) {
294 var allowed = true;
294 var allowed = true;
295 $.each(self.forbidReviewUsers, function(index, member_data) {
295 $.each(self.forbidReviewUsers, function(index, member_data) {
296 if (parseInt(userId) === member_data['user_id']) {
296 if (parseInt(userId) === member_data['user_id']) {
297 allowed = false;
297 allowed = false;
298 return false // breaks the loop
298 return false // breaks the loop
299 }
299 }
300 });
300 });
301 return allowed
301 return allowed
302 };
302 };
303
303
304 var userAllowed = userAllowedReview(id);
304 var userAllowed = userAllowedReview(id);
305 if (!userAllowed){
305 if (!userAllowed){
306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
307 } else {
307 } else {
308 // only add if it's not there
308 // only add if it's not there
309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
310
310
311 if (alreadyReviewer) {
311 if (alreadyReviewer) {
312 alert(_gettext('User `{0}` already in reviewers').format(username));
312 alert(_gettext('User `{0}` already in reviewers').format(username));
313 } else {
313 } else {
314 members.innerHTML += renderTemplate('reviewMemberEntry', {
314 members.innerHTML += renderTemplate('reviewMemberEntry', {
315 'member': reviewer_obj,
315 'member': reviewer_obj,
316 'mandatory': mandatory,
316 'mandatory': mandatory,
317 'allowed_to_update': true,
317 'allowed_to_update': true,
318 'review_status': 'not_reviewed',
318 'review_status': 'not_reviewed',
319 'review_status_label': _gettext('Not Reviewed'),
319 'review_status_label': _gettext('Not Reviewed'),
320 'reasons': reasons,
320 'reasons': reasons,
321 'create': true
321 'create': true
322 });
322 });
323 tooltipActivate();
323 tooltipActivate();
324 }
324 }
325 }
325 }
326
326
327 };
327 };
328
328
329 this.updateReviewers = function(repo_name, pull_request_id){
329 this.updateReviewers = function(repo_name, pull_request_id){
330 var postData = $('#reviewers input').serialize();
330 var postData = $('#reviewers input').serialize();
331 _updatePullRequest(repo_name, pull_request_id, postData);
331 _updatePullRequest(repo_name, pull_request_id, postData);
332 };
332 };
333
333
334 };
334 };
335
335
336
336
337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
338 var url = pyroutes.url(
338 var url = pyroutes.url(
339 'pullrequest_update',
339 'pullrequest_update',
340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
341 if (typeof postData === 'string' ) {
341 if (typeof postData === 'string' ) {
342 postData += '&csrf_token=' + CSRF_TOKEN;
342 postData += '&csrf_token=' + CSRF_TOKEN;
343 } else {
343 } else {
344 postData.csrf_token = CSRF_TOKEN;
344 postData.csrf_token = CSRF_TOKEN;
345 }
345 }
346
346
347 var success = function(o) {
347 var success = function(o) {
348 var redirectUrl = o['redirect_url'];
348 var redirectUrl = o['redirect_url'];
349 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
349 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
350 window.location = redirectUrl;
350 window.location = redirectUrl;
351 } else {
351 } else {
352 window.location.reload();
352 window.location.reload();
353 }
353 }
354 };
354 };
355
355
356 ajaxPOST(url, postData, success);
356 ajaxPOST(url, postData, success);
357 };
357 };
358
358
359 /**
359 /**
360 * PULL REQUEST update commits
360 * PULL REQUEST update commits
361 */
361 */
362 var updateCommits = function(repo_name, pull_request_id, force) {
362 var updateCommits = function(repo_name, pull_request_id, force) {
363 var postData = {
363 var postData = {
364 'update_commits': true
364 'update_commits': true
365 };
365 };
366 if (force !== undefined && force === true) {
366 if (force !== undefined && force === true) {
367 postData['force_refresh'] = true
367 postData['force_refresh'] = true
368 }
368 }
369 _updatePullRequest(repo_name, pull_request_id, postData);
369 _updatePullRequest(repo_name, pull_request_id, postData);
370 };
370 };
371
371
372
372
373 /**
373 /**
374 * PULL REQUEST edit info
374 * PULL REQUEST edit info
375 */
375 */
376 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
376 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
377 var url = pyroutes.url(
377 var url = pyroutes.url(
378 'pullrequest_update',
378 'pullrequest_update',
379 {"repo_name": repo_name, "pull_request_id": pull_request_id});
379 {"repo_name": repo_name, "pull_request_id": pull_request_id});
380
380
381 var postData = {
381 var postData = {
382 'title': title,
382 'title': title,
383 'description': description,
383 'description': description,
384 'description_renderer': renderer,
384 'description_renderer': renderer,
385 'edit_pull_request': true,
385 'edit_pull_request': true,
386 'csrf_token': CSRF_TOKEN
386 'csrf_token': CSRF_TOKEN
387 };
387 };
388 var success = function(o) {
388 var success = function(o) {
389 window.location.reload();
389 window.location.reload();
390 };
390 };
391 ajaxPOST(url, postData, success);
391 ajaxPOST(url, postData, success);
392 };
392 };
393
393
394
394
395 /**
395 /**
396 * Reviewer autocomplete
396 * Reviewer autocomplete
397 */
397 */
398 var ReviewerAutoComplete = function(inputId) {
398 var ReviewerAutoComplete = function(inputId) {
399 $(inputId).autocomplete({
399 $(inputId).autocomplete({
400 serviceUrl: pyroutes.url('user_autocomplete_data'),
400 serviceUrl: pyroutes.url('user_autocomplete_data'),
401 minChars:2,
401 minChars:2,
402 maxHeight:400,
402 maxHeight:400,
403 deferRequestBy: 300, //miliseconds
403 deferRequestBy: 300, //miliseconds
404 showNoSuggestionNotice: true,
404 showNoSuggestionNotice: true,
405 tabDisabled: true,
405 tabDisabled: true,
406 autoSelectFirst: true,
406 autoSelectFirst: true,
407 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
407 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
408 formatResult: autocompleteFormatResult,
408 formatResult: autocompleteFormatResult,
409 lookupFilter: autocompleteFilterResult,
409 lookupFilter: autocompleteFilterResult,
410 onSelect: function(element, data) {
410 onSelect: function(element, data) {
411 var mandatory = false;
411 var mandatory = false;
412 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
412 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
413
413
414 // add whole user groups
414 // add whole user groups
415 if (data.value_type == 'user_group') {
415 if (data.value_type == 'user_group') {
416 reasons.push(_gettext('member of "{0}"').format(data.value_display));
416 reasons.push(_gettext('member of "{0}"').format(data.value_display));
417
417
418 $.each(data.members, function(index, member_data) {
418 $.each(data.members, function(index, member_data) {
419 var reviewer = member_data;
419 var reviewer = member_data;
420 reviewer['user_id'] = member_data['id'];
420 reviewer['user_id'] = member_data['id'];
421 reviewer['gravatar_link'] = member_data['icon_link'];
421 reviewer['gravatar_link'] = member_data['icon_link'];
422 reviewer['user_link'] = member_data['profile_link'];
422 reviewer['user_link'] = member_data['profile_link'];
423 reviewer['rules'] = [];
423 reviewer['rules'] = [];
424 reviewersController.addReviewMember(reviewer, reasons, mandatory);
424 reviewersController.addReviewMember(reviewer, reasons, mandatory);
425 })
425 })
426 }
426 }
427 // add single user
427 // add single user
428 else {
428 else {
429 var reviewer = data;
429 var reviewer = data;
430 reviewer['user_id'] = data['id'];
430 reviewer['user_id'] = data['id'];
431 reviewer['gravatar_link'] = data['icon_link'];
431 reviewer['gravatar_link'] = data['icon_link'];
432 reviewer['user_link'] = data['profile_link'];
432 reviewer['user_link'] = data['profile_link'];
433 reviewer['rules'] = [];
433 reviewer['rules'] = [];
434 reviewersController.addReviewMember(reviewer, reasons, mandatory);
434 reviewersController.addReviewMember(reviewer, reasons, mandatory);
435 }
435 }
436
436
437 $(inputId).val('');
437 $(inputId).val('');
438 }
438 }
439 });
439 });
440 };
440 };
441
441
442
442
443 VersionController = function () {
443 VersionController = function () {
444 var self = this;
444 var self = this;
445 this.$verSource = $('input[name=ver_source]');
445 this.$verSource = $('input[name=ver_source]');
446 this.$verTarget = $('input[name=ver_target]');
446 this.$verTarget = $('input[name=ver_target]');
447 this.$showVersionDiff = $('#show-version-diff');
447 this.$showVersionDiff = $('#show-version-diff');
448
448
449 this.adjustRadioSelectors = function (curNode) {
449 this.adjustRadioSelectors = function (curNode) {
450 var getVal = function (item) {
450 var getVal = function (item) {
451 if (item == 'latest') {
451 if (item == 'latest') {
452 return Number.MAX_SAFE_INTEGER
452 return Number.MAX_SAFE_INTEGER
453 }
453 }
454 else {
454 else {
455 return parseInt(item)
455 return parseInt(item)
456 }
456 }
457 };
457 };
458
458
459 var curVal = getVal($(curNode).val());
459 var curVal = getVal($(curNode).val());
460 var cleared = false;
460 var cleared = false;
461
461
462 $.each(self.$verSource, function (index, value) {
462 $.each(self.$verSource, function (index, value) {
463 var elVal = getVal($(value).val());
463 var elVal = getVal($(value).val());
464
464
465 if (elVal > curVal) {
465 if (elVal > curVal) {
466 if ($(value).is(':checked')) {
466 if ($(value).is(':checked')) {
467 cleared = true;
467 cleared = true;
468 }
468 }
469 $(value).attr('disabled', 'disabled');
469 $(value).attr('disabled', 'disabled');
470 $(value).removeAttr('checked');
470 $(value).removeAttr('checked');
471 $(value).css({'opacity': 0.1});
471 $(value).css({'opacity': 0.1});
472 }
472 }
473 else {
473 else {
474 $(value).css({'opacity': 1});
474 $(value).css({'opacity': 1});
475 $(value).removeAttr('disabled');
475 $(value).removeAttr('disabled');
476 }
476 }
477 });
477 });
478
478
479 if (cleared) {
479 if (cleared) {
480 // if we unchecked an active, set the next one to same loc.
480 // if we unchecked an active, set the next one to same loc.
481 $(this.$verSource).filter('[value={0}]'.format(
481 $(this.$verSource).filter('[value={0}]'.format(
482 curVal)).attr('checked', 'checked');
482 curVal)).attr('checked', 'checked');
483 }
483 }
484
484
485 self.setLockAction(false,
485 self.setLockAction(false,
486 $(curNode).data('verPos'),
486 $(curNode).data('verPos'),
487 $(this.$verSource).filter(':checked').data('verPos')
487 $(this.$verSource).filter(':checked').data('verPos')
488 );
488 );
489 };
489 };
490
490
491
491
492 this.attachVersionListener = function () {
492 this.attachVersionListener = function () {
493 self.$verTarget.change(function (e) {
493 self.$verTarget.change(function (e) {
494 self.adjustRadioSelectors(this)
494 self.adjustRadioSelectors(this)
495 });
495 });
496 self.$verSource.change(function (e) {
496 self.$verSource.change(function (e) {
497 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
497 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
498 });
498 });
499 };
499 };
500
500
501 this.init = function () {
501 this.init = function () {
502
502
503 var curNode = self.$verTarget.filter(':checked');
503 var curNode = self.$verTarget.filter(':checked');
504 self.adjustRadioSelectors(curNode);
504 self.adjustRadioSelectors(curNode);
505 self.setLockAction(true);
505 self.setLockAction(true);
506 self.attachVersionListener();
506 self.attachVersionListener();
507
507
508 };
508 };
509
509
510 this.setLockAction = function (state, selectedVersion, otherVersion) {
510 this.setLockAction = function (state, selectedVersion, otherVersion) {
511 var $showVersionDiff = this.$showVersionDiff;
511 var $showVersionDiff = this.$showVersionDiff;
512
512
513 if (state) {
513 if (state) {
514 $showVersionDiff.attr('disabled', 'disabled');
514 $showVersionDiff.attr('disabled', 'disabled');
515 $showVersionDiff.addClass('disabled');
515 $showVersionDiff.addClass('disabled');
516 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
516 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
517 }
517 }
518 else {
518 else {
519 $showVersionDiff.removeAttr('disabled');
519 $showVersionDiff.removeAttr('disabled');
520 $showVersionDiff.removeClass('disabled');
520 $showVersionDiff.removeClass('disabled');
521
521
522 if (selectedVersion == otherVersion) {
522 if (selectedVersion == otherVersion) {
523 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
523 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
524 } else {
524 } else {
525 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
525 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
526 }
526 }
527 }
527 }
528
528
529 };
529 };
530
530
531 this.showVersionDiff = function () {
531 this.showVersionDiff = function () {
532 var target = self.$verTarget.filter(':checked');
532 var target = self.$verTarget.filter(':checked');
533 var source = self.$verSource.filter(':checked');
533 var source = self.$verSource.filter(':checked');
534
534
535 if (target.val() && source.val()) {
535 if (target.val() && source.val()) {
536 var params = {
536 var params = {
537 'pull_request_id': templateContext.pull_request_data.pull_request_id,
537 'pull_request_id': templateContext.pull_request_data.pull_request_id,
538 'repo_name': templateContext.repo_name,
538 'repo_name': templateContext.repo_name,
539 'version': target.val(),
539 'version': target.val(),
540 'from_version': source.val()
540 'from_version': source.val()
541 };
541 };
542 window.location = pyroutes.url('pullrequest_show', params)
542 window.location = pyroutes.url('pullrequest_show', params)
543 }
543 }
544
544
545 return false;
545 return false;
546 };
546 };
547
547
548 this.toggleVersionView = function (elem) {
548 this.toggleVersionView = function (elem) {
549
549
550 if (this.$showVersionDiff.is(':visible')) {
550 if (this.$showVersionDiff.is(':visible')) {
551 $('.version-pr').hide();
551 $('.version-pr').hide();
552 this.$showVersionDiff.hide();
552 this.$showVersionDiff.hide();
553 $(elem).html($(elem).data('toggleOn'))
553 $(elem).html($(elem).data('toggleOn'))
554 } else {
554 } else {
555 $('.version-pr').show();
555 $('.version-pr').show();
556 this.$showVersionDiff.show();
556 this.$showVersionDiff.show();
557 $(elem).html($(elem).data('toggleOff'))
557 $(elem).html($(elem).data('toggleOff'))
558 }
558 }
559
559
560 return false
560 return false
561 };
562
563 this.toggleElement = function (elem, target) {
564 var $elem = $(elem);
565 var $target = $(target);
566
567 if ($target.is(':visible')) {
568 $target.hide();
569 $elem.html($elem.data('toggleOn'))
570 } else {
571 $target.show();
572 $elem.html($elem.data('toggleOff'))
573 }
574
575 return false
561 }
576 }
562
577
563 };
578 };
564
579
565
580
566 UpdatePrController = function () {
581 UpdatePrController = function () {
567 var self = this;
582 var self = this;
568 this.$updateCommits = $('#update_commits');
583 this.$updateCommits = $('#update_commits');
569 this.$updateCommitsSwitcher = $('#update_commits_switcher');
584 this.$updateCommitsSwitcher = $('#update_commits_switcher');
570
585
571 this.lockUpdateButton = function (label) {
586 this.lockUpdateButton = function (label) {
572 self.$updateCommits.attr('disabled', 'disabled');
587 self.$updateCommits.attr('disabled', 'disabled');
573 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
588 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
574
589
575 self.$updateCommits.addClass('disabled');
590 self.$updateCommits.addClass('disabled');
576 self.$updateCommitsSwitcher.addClass('disabled');
591 self.$updateCommitsSwitcher.addClass('disabled');
577
592
578 self.$updateCommits.removeClass('btn-primary');
593 self.$updateCommits.removeClass('btn-primary');
579 self.$updateCommitsSwitcher.removeClass('btn-primary');
594 self.$updateCommitsSwitcher.removeClass('btn-primary');
580
595
581 self.$updateCommits.text(_gettext(label));
596 self.$updateCommits.text(_gettext(label));
582 };
597 };
583
598
584 this.isUpdateLocked = function () {
599 this.isUpdateLocked = function () {
585 return self.$updateCommits.attr('disabled') !== undefined;
600 return self.$updateCommits.attr('disabled') !== undefined;
586 };
601 };
587
602
588 this.updateCommits = function (curNode) {
603 this.updateCommits = function (curNode) {
589 if (self.isUpdateLocked()) {
604 if (self.isUpdateLocked()) {
590 return
605 return
591 }
606 }
592 self.lockUpdateButton(_gettext('Updating...'));
607 self.lockUpdateButton(_gettext('Updating...'));
593 updateCommits(
608 updateCommits(
594 templateContext.repo_name,
609 templateContext.repo_name,
595 templateContext.pull_request_data.pull_request_id);
610 templateContext.pull_request_data.pull_request_id);
596 };
611 };
597
612
598 this.forceUpdateCommits = function () {
613 this.forceUpdateCommits = function () {
599 if (self.isUpdateLocked()) {
614 if (self.isUpdateLocked()) {
600 return
615 return
601 }
616 }
602 self.lockUpdateButton(_gettext('Force updating...'));
617 self.lockUpdateButton(_gettext('Force updating...'));
603 var force = true;
618 var force = true;
604 updateCommits(
619 updateCommits(
605 templateContext.repo_name,
620 templateContext.repo_name,
606 templateContext.pull_request_data.pull_request_id, force);
621 templateContext.pull_request_data.pull_request_id, force);
607 };
622 };
608 }; No newline at end of file
623 };
@@ -1,819 +1,842 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 %if c.rhodecode_name:
7 %if c.rhodecode_name:
8 &middot; ${h.branding(c.rhodecode_name)}
8 &middot; ${h.branding(c.rhodecode_name)}
9 %endif
9 %endif
10 </%def>
10 </%def>
11
11
12 <%def name="breadcrumbs_links()">
12 <%def name="breadcrumbs_links()">
13 <%
14 pr_title = c.pull_request.title
15 if c.pull_request.is_closed():
16 pr_title = '[{}] {}'.format(_('Closed'), pr_title)
17 %>
18
13
19 <div id="pr-title">
14 <div id="pr-title">
20 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${pr_title}">
15 % if c.pull_request.is_closed():
16 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
17 % endif
18 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
21 </div>
19 </div>
22 <div id="pr-title-edit" class="input" style="display: none;">
20 <div id="pr-title-edit" class="input" style="display: none;">
23 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
21 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
24 </div>
22 </div>
25 </%def>
23 </%def>
26
24
27 <%def name="menu_bar_nav()">
25 <%def name="menu_bar_nav()">
28 ${self.menu_items(active='repositories')}
26 ${self.menu_items(active='repositories')}
29 </%def>
27 </%def>
30
28
31 <%def name="menu_bar_subnav()">
29 <%def name="menu_bar_subnav()">
32 ${self.repo_menu(active='showpullrequest')}
30 ${self.repo_menu(active='showpullrequest')}
33 </%def>
31 </%def>
34
32
35 <%def name="main()">
33 <%def name="main()">
36
34
37 <script type="text/javascript">
35 <script type="text/javascript">
38 // TODO: marcink switch this to pyroutes
36 // TODO: marcink switch this to pyroutes
39 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
40 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
41 </script>
39 </script>
40
42 <div class="box">
41 <div class="box">
43
42
44 ${self.breadcrumbs()}
43 ${self.breadcrumbs()}
45
44
46 <div class="box pr-summary">
45 <div class="box pr-summary">
47
46
48 <div class="summary-details block-left">
47 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
49 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
50 <div class="pull-left">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
52 ${_('Created on')}
53 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
54 <span class="pr-details-title-author-pref">${_('by')}</span>
55 </div>
56
57 <div class="pull-left">
58 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
59 </div>
60
52 %if c.allowed_to_update:
61 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
62 <div id="delete_pullrequest" class="pull-right action_button ${('' if c.allowed_to_delete else 'disabled' )}" >
54 % if c.allowed_to_delete:
63 % if c.allowed_to_delete:
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
64 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
65 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
66 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
67 ${h.end_form()}
59 % else:
68 % else:
60 ${_('Delete')}
69 ${_('Delete')}
61 % endif
70 % endif
62 </div>
71 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
72 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
73 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;">${_('Cancel')}</div>
74 <div id="edit_pull_request" class="pull-right action_button pr-save" style="display: none;">${_('Save Changes')}</div>
65 %endif
75 %endif
66 </div>
76 </div>
67
77
78 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
79 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
80 </div>
81
82 <div id="pr-desc-edit" class="input textarea" style="display: none;">
83 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
84 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
85 </div>
86
68 <div id="summary" class="fields pr-details-content">
87 <div id="summary" class="fields pr-details-content">
88
89 ## review
69 <div class="field">
90 <div class="field">
70 <div class="label-summary">
91 <div class="label-pr-detail">
71 <label>${_('Source')}:</label>
92 <label>${_('Review status')}:</label>
93 </div>
94 <div class="input">
95 %if c.pull_request_review_status:
96 <div class="tag status-tag-${c.pull_request_review_status}">
97 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
98 <span class="changeset-status-lbl">
99 %if c.pull_request.is_closed():
100 ${_('Closed')},
101 %endif
102
103 ${h.commit_status_lbl(c.pull_request_review_status)}
104
105 </span>
106 </div>
107 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
108 %endif
109 </div>
110 </div>
111
112 ## source
113 <div class="field">
114 <div class="label-pr-detail">
115 <label>${_('Commit flow')}:</label>
72 </div>
116 </div>
73 <div class="input">
117 <div class="input">
74 <div class="pr-origininfo">
118 <div class="pr-commit-flow">
75 ## branch link is only valid if it is a branch
119 ## Source
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
120 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
121 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
79 %else:
122 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
123 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
124 %endif
125 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
126 &rarr;
127 ## Target
128 %if c.pull_request.target_ref_parts.type == 'branch':
129 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
130 %else:
131 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
81 %endif
132 %endif
82 </span>
133
83 <span class="clone-url">
134 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
135
85 </span>
136 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
86 <br/>
137 <i class="icon-angle-down">more details</i>
138 </a>
139
140 </div>
141
142 <div class="source-details" style="display: none">
143
144 <ul>
145
146 ## common ancestor
147 <li>
148 ${_('Common ancestor')}:
87 % if c.ancestor_commit:
149 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
150 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
151 % else:
152 ${_('not available')}
90 % endif
153 % endif
91 </div>
154 </li>
155
156 ## pull url
157 <li>
92 %if h.is_hg(c.pull_request.source_repo):
158 %if h.is_hg(c.pull_request.source_repo):
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
159 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
94 %elif h.is_git(c.pull_request.source_repo):
160 %elif h.is_git(c.pull_request.source_repo):
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
161 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
96 %endif
162 %endif
97
163
98 <div class="">
164 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
101 </div>
166 </li>
102
167
103 </div>
168 ## Shadow repo
104 </div>
169 <li>
105 <div class="field">
106 <div class="label-summary">
107 <label>${_('Target')}:</label>
108 </div>
109 <div class="input">
110 <div class="pr-targetinfo">
111 ## branch link is only valid if it is a branch
112 <span class="tag">
113 %if c.pull_request.target_ref_parts.type == 'branch':
114 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
115 %else:
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 %endif
118 </span>
119 <span class="clone-url">
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
121 </span>
122 </div>
123 </div>
124 </div>
125
126 ## Link to the shadow repository.
127 <div class="field">
128 <div class="label-summary">
129 <label>${_('Merge')}:</label>
130 </div>
131 <div class="input">
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
170 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 %if h.is_hg(c.pull_request.target_repo):
171 %if h.is_hg(c.pull_request.target_repo):
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
172 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
135 %elif h.is_git(c.pull_request.target_repo):
173 %elif h.is_git(c.pull_request.target_repo):
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
174 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
137 %endif
175 %endif
138 <div class="">
176
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
177 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
140 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
178 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
141 </div>
179
142 % else:
180 % else:
143 <div class="">
181 <div class="">
144 ${_('Shadow repository data not available')}.
182 ${_('Shadow repository data not available')}.
145 </div>
183 </div>
146 % endif
184 % endif
147 </div>
185 </li>
186
187 </ul>
188
148 </div>
189 </div>
149
190
150 <div class="field">
151 <div class="label-summary">
152 <label>${_('Review')}:</label>
153 </div>
191 </div>
154 <div class="input">
192
155 %if c.pull_request_review_status:
156 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
157 <span class="changeset-status-lbl">
158 %if c.pull_request.is_closed():
159 ${_('Closed')},
160 %endif
161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 </span>
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
164 %endif
165 </div>
166 </div>
167 <div class="field">
168 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
169 <label>${_('Description')}:</label>
170 </div>
171 <div id="pr-desc" class="input">
172 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
173 </div>
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
175 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
176 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
177 </div>
178 </div>
193 </div>
179
194
195 ## versions
180 <div class="field">
196 <div class="field">
181 <div class="label-summary">
197 <div class="label-pr-detail">
182 <label>${_('Versions')}:</label>
198 <label>${_('Versions')}:</label>
183 </div>
199 </div>
184
200
185 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
201 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
186 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
202 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
187
203
188 <div class="pr-versions">
204 <div class="pr-versions">
189 % if c.show_version_changes:
205 % if c.show_version_changes:
190 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
206 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
191 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
207 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
192 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
208 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
193 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
209 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
194 data-toggle-off="${_('Hide all versions of this pull request')}">
210 data-toggle-on="${_('show versions')}."
195 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
211 data-toggle-off="${_('hide versions')}.">
212 ${_('show versions')}.
196 </a>
213 </a>
197 <table>
214 <table>
198 ## SHOW ALL VERSIONS OF PR
215 ## SHOW ALL VERSIONS OF PR
199 <% ver_pr = None %>
216 <% ver_pr = None %>
200
217
201 % for data in reversed(list(enumerate(c.versions, 1))):
218 % for data in reversed(list(enumerate(c.versions, 1))):
202 <% ver_pos = data[0] %>
219 <% ver_pos = data[0] %>
203 <% ver = data[1] %>
220 <% ver = data[1] %>
204 <% ver_pr = ver.pull_request_version_id %>
221 <% ver_pr = ver.pull_request_version_id %>
205 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
222 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
206
223
207 <tr class="version-pr" style="display: ${display_row}">
224 <tr class="version-pr" style="display: ${display_row}">
208 <td>
225 <td>
209 <code>
226 <code>
210 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
227 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
211 </code>
228 </code>
212 </td>
229 </td>
213 <td>
230 <td>
214 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
231 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
215 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
232 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
216 </td>
233 </td>
217 <td>
234 <td>
218 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
235 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
219 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
236 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
220
237
221 </td>
238 </td>
222 <td>
239 <td>
223 % if c.at_version_num != ver_pr:
240 % if c.at_version_num != ver_pr:
224 <i class="icon-comment"></i>
241 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
225 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
242 <code>
226 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
243 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
227 </code>
244 </code>
228 % endif
245 % endif
229 </td>
246 </td>
230 <td>
247 <td>
231 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
248 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
232 </td>
249 </td>
233 <td>
250 <td>
234 ${h.age_component(ver.updated_on, time_is_local=True)}
251 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
235 </td>
252 </td>
236 </tr>
253 </tr>
237 % endfor
254 % endfor
238
255
239 <tr>
256 <tr>
240 <td colspan="6">
257 <td colspan="6">
241 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
258 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
242 data-label-text-locked="${_('select versions to show changes')}"
259 data-label-text-locked="${_('select versions to show changes')}"
243 data-label-text-diff="${_('show changes between versions')}"
260 data-label-text-diff="${_('show changes between versions')}"
244 data-label-text-show="${_('show pull request for this version')}"
261 data-label-text-show="${_('show pull request for this version')}"
245 >
262 >
246 ${_('select versions to show changes')}
263 ${_('select versions to show changes')}
247 </button>
264 </button>
248 </td>
265 </td>
249 </tr>
266 </tr>
250 </table>
267 </table>
251 % else:
268 % else:
252 <div class="input">
269 <div class="input">
253 ${_('Pull request versions not available')}.
270 ${_('Pull request versions not available')}.
254 </div>
271 </div>
255 % endif
272 % endif
256 </div>
273 </div>
257 </div>
274 </div>
258
275
259 <div id="pr-save" class="field" style="display: none;">
260 <div class="label-summary"></div>
261 <div class="input">
262 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
263 </div>
276 </div>
264 </div>
277
265 </div>
266 </div>
267 <div>
268 ## AUTHOR
269 <div class="reviewers-title block-right">
270 <div class="pr-details-title">
271 ${_('Author of this pull request')}
272 </div>
273 </div>
274 <div class="block-right pr-details-content reviewers">
275 <ul class="group_members">
276 <li>
277 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
278 </li>
279 </ul>
280 </div>
278 </div>
281
279
282 ## REVIEW RULES
280 ## REVIEW RULES
283 <div id="review_rules" style="display: none" class="reviewers-title block-right">
281 <div id="review_rules" style="display: none" class="reviewers-title block-right">
284 <div class="pr-details-title">
282 <div class="pr-details-title">
285 ${_('Reviewer rules')}
283 ${_('Reviewer rules')}
286 %if c.allowed_to_update:
284 %if c.allowed_to_update:
287 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
285 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
288 %endif
286 %endif
289 </div>
287 </div>
290 <div class="pr-reviewer-rules">
288 <div class="pr-reviewer-rules">
291 ## review rules will be appended here, by default reviewers logic
289 ## review rules will be appended here, by default reviewers logic
292 </div>
290 </div>
293 <input id="review_data" type="hidden" name="review_data" value="">
291 <input id="review_data" type="hidden" name="review_data" value="">
294 </div>
292 </div>
295
293
296 ## REVIEWERS
294 ## REVIEWERS
297 <div class="reviewers-title block-right">
295 <div class="reviewers-title block-right">
298 <div class="pr-details-title">
296 <div class="pr-details-title">
299 ${_('Pull request reviewers')}
297 ${_('Pull request reviewers')}
300 %if c.allowed_to_update:
298 %if c.allowed_to_update:
301 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
299 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
302 %endif
300 %endif
303 </div>
301 </div>
304 </div>
302 </div>
305 <div id="reviewers" class="block-right pr-details-content reviewers">
303 <div id="reviewers" class="block-right pr-details-content reviewers">
306
304
307 ## members redering block
305 ## members redering block
308 <input type="hidden" name="__start__" value="review_members:sequence">
306 <input type="hidden" name="__start__" value="review_members:sequence">
309 <ul id="review_members" class="group_members">
307 <ul id="review_members" class="group_members">
310
308
311 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
309 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
312 <script>
310 <script>
313 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
311 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
314 var status = "${(status[0][1].status if status else 'not_reviewed')}";
312 var status = "${(status[0][1].status if status else 'not_reviewed')}";
315 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
313 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
316 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
314 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
317
315
318 var entry = renderTemplate('reviewMemberEntry', {
316 var entry = renderTemplate('reviewMemberEntry', {
319 'member': member,
317 'member': member,
320 'mandatory': member.mandatory,
318 'mandatory': member.mandatory,
321 'reasons': member.reasons,
319 'reasons': member.reasons,
322 'allowed_to_update': allowed_to_update,
320 'allowed_to_update': allowed_to_update,
323 'review_status': status,
321 'review_status': status,
324 'review_status_label': status_lbl,
322 'review_status_label': status_lbl,
325 'user_group': member.user_group,
323 'user_group': member.user_group,
326 'create': false
324 'create': false
327 });
325 });
328 $('#review_members').append(entry)
326 $('#review_members').append(entry)
329 </script>
327 </script>
330
328
331 % endfor
329 % endfor
332
330
333 </ul>
331 </ul>
334
332
335 <input type="hidden" name="__end__" value="review_members:sequence">
333 <input type="hidden" name="__end__" value="review_members:sequence">
336 ## end members redering block
334 ## end members redering block
337
335
338 %if not c.pull_request.is_closed():
336 %if not c.pull_request.is_closed():
339 <div id="add_reviewer" class="ac" style="display: none;">
337 <div id="add_reviewer" class="ac" style="display: none;">
340 %if c.allowed_to_update:
338 %if c.allowed_to_update:
341 % if not c.forbid_adding_reviewers:
339 % if not c.forbid_adding_reviewers:
342 <div id="add_reviewer_input" class="reviewer_ac">
340 <div id="add_reviewer_input" class="reviewer_ac">
343 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
341 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
344 <div id="reviewers_container"></div>
342 <div id="reviewers_container"></div>
345 </div>
343 </div>
346 % endif
344 % endif
347 <div class="pull-right">
345 <div class="pull-right">
348 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
346 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
349 </div>
347 </div>
350 %endif
348 %endif
351 </div>
349 </div>
352 %endif
350 %endif
353 </div>
351 </div>
354 </div>
352
353 ## ## TODOs will be listed here
354 ## <div class="reviewers-title block-right">
355 ## <div class="pr-details-title">
356 ## ${_('TODOs')}
357 ## </div>
358 ## </div>
359 ## <div class="block-right pr-details-content reviewers">
360 ## <ul class="group_members">
361 ## <li>
362 ## XXXX
363 ## </li>
364 ## </ul>
365 ## </div>
366 ## </div>
367
355 </div>
368 </div>
356
369
357 <div class="box">
370 <div class="box">
358
371
359 % if c.state_progressing:
372 % if c.state_progressing:
373
360 <h2 style="text-align: center">
374 <h2 style="text-align: center">
361 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
375 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
362 </h2>
376 </h2>
363
377
364 % else:
378 % else:
365
379
366 ## Diffs rendered here
380 ## Diffs rendered here
367 <div class="table" >
381 <div class="table" >
368 <div id="changeset_compare_view_content">
382 <div id="changeset_compare_view_content">
369 ##CS
383 ##CS
370 % if c.missing_requirements:
384 % if c.missing_requirements:
371 <div class="box">
385 <div class="box">
372 <div class="alert alert-warning">
386 <div class="alert alert-warning">
373 <div>
387 <div>
374 <strong>${_('Missing requirements:')}</strong>
388 <strong>${_('Missing requirements:')}</strong>
375 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
389 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
376 </div>
390 </div>
377 </div>
391 </div>
378 </div>
392 </div>
379 % elif c.missing_commits:
393 % elif c.missing_commits:
380 <div class="box">
394 <div class="box">
381 <div class="alert alert-warning">
395 <div class="alert alert-warning">
382 <div>
396 <div>
383 <strong>${_('Missing commits')}:</strong>
397 <strong>${_('Missing commits')}:</strong>
384 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
398 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
385 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
399 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
386 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
400 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
387 </div>
401 </div>
388 </div>
402 </div>
389 </div>
403 </div>
390 % endif
404 % endif
391
405
392 <div class="compare_view_commits_title">
406 <div class="compare_view_commits_title">
393 % if not c.compare_mode:
407 % if not c.compare_mode:
394
408
395 % if c.at_version_pos:
409 % if c.at_version_pos:
396 <h4>
410 <h4>
397 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
411 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
398 </h4>
412 </h4>
399 % endif
413 % endif
400
414
401 <div class="pull-left">
415 <div class="pull-left">
402 <div class="btn-group">
416 <div class="btn-group">
403 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
417 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
404 % if c.collapse_all_commits:
418 % if c.collapse_all_commits:
405 <i class="icon-plus-squared-alt icon-no-margin"></i>
419 <i class="icon-plus-squared-alt icon-no-margin"></i>
406 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
420 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
407 % else:
421 % else:
408 <i class="icon-minus-squared-alt icon-no-margin"></i>
422 <i class="icon-minus-squared-alt icon-no-margin"></i>
409 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
423 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
410 % endif
424 % endif
411 </a>
425 </a>
412 </div>
426 </div>
413 </div>
427 </div>
414
428
415 <div class="pull-right">
429 <div class="pull-right">
416 % if c.allowed_to_update and not c.pull_request.is_closed():
430 % if c.allowed_to_update and not c.pull_request.is_closed():
417
431
418 <div class="btn-group btn-group-actions">
432 <div class="btn-group btn-group-actions">
419 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
433 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
420 ${_('Update commits')}
434 ${_('Update commits')}
421 </a>
435 </a>
422
436
423 <a id="update_commits_switcher" class="btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button">
437 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
424 <i class="icon-down"></i>
438 <i class="icon-down"></i>
425 </a>
439 </a>
426
440
427 <div class="btn-action-switcher-container" id="update-commits-switcher">
441 <div class="btn-action-switcher-container" id="update-commits-switcher">
428 <ul class="btn-action-switcher" role="menu">
442 <ul class="btn-action-switcher" role="menu">
429 <li>
443 <li>
430 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
444 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
431 ${_('Force update commits')}
445 ${_('Force update commits')}
432 </a>
446 </a>
433 <div class="action-help-block">
447 <div class="action-help-block">
434 ${_('Update commits and force refresh this pull request.')}
448 ${_('Update commits and force refresh this pull request.')}
435 </div>
449 </div>
436 </li>
450 </li>
437 </ul>
451 </ul>
438 </div>
452 </div>
439 </div>
453 </div>
440
454
441 % else:
455 % else:
442 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
456 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
443 % endif
457 % endif
444
458
445 </div>
459 </div>
446 % endif
460 % endif
447 </div>
461 </div>
448
462
449 % if not c.missing_commits:
463 % if not c.missing_commits:
450 % if c.compare_mode:
464 % if c.compare_mode:
451 % if c.at_version:
465 % if c.at_version:
452 <h4>
466 <h4>
453 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
467 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
454 </h4>
468 </h4>
455
469
456 <div class="subtitle-compare">
470 <div class="subtitle-compare">
457 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
471 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
458 </div>
472 </div>
459
473
460 <div class="container">
474 <div class="container">
461 <table class="rctable compare_view_commits">
475 <table class="rctable compare_view_commits">
462 <tr>
476 <tr>
463 <th></th>
477 <th></th>
464 <th>${_('Time')}</th>
478 <th>${_('Time')}</th>
465 <th>${_('Author')}</th>
479 <th>${_('Author')}</th>
466 <th>${_('Commit')}</th>
480 <th>${_('Commit')}</th>
467 <th></th>
481 <th></th>
468 <th>${_('Description')}</th>
482 <th>${_('Description')}</th>
469 </tr>
483 </tr>
470
484
471 % for c_type, commit in c.commit_changes:
485 % for c_type, commit in c.commit_changes:
472 % if c_type in ['a', 'r']:
486 % if c_type in ['a', 'r']:
473 <%
487 <%
474 if c_type == 'a':
488 if c_type == 'a':
475 cc_title = _('Commit added in displayed changes')
489 cc_title = _('Commit added in displayed changes')
476 elif c_type == 'r':
490 elif c_type == 'r':
477 cc_title = _('Commit removed in displayed changes')
491 cc_title = _('Commit removed in displayed changes')
478 else:
492 else:
479 cc_title = ''
493 cc_title = ''
480 %>
494 %>
481 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
495 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
482 <td>
496 <td>
483 <div class="commit-change-indicator color-${c_type}-border">
497 <div class="commit-change-indicator color-${c_type}-border">
484 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
498 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
485 ${c_type.upper()}
499 ${c_type.upper()}
486 </div>
500 </div>
487 </div>
501 </div>
488 </td>
502 </td>
489 <td class="td-time">
503 <td class="td-time">
490 ${h.age_component(commit.date)}
504 ${h.age_component(commit.date)}
491 </td>
505 </td>
492 <td class="td-user">
506 <td class="td-user">
493 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
507 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
494 </td>
508 </td>
495 <td class="td-hash">
509 <td class="td-hash">
496 <code>
510 <code>
497 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
511 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
498 r${commit.idx}:${h.short_id(commit.raw_id)}
512 r${commit.idx}:${h.short_id(commit.raw_id)}
499 </a>
513 </a>
500 ${h.hidden('revisions', commit.raw_id)}
514 ${h.hidden('revisions', commit.raw_id)}
501 </code>
515 </code>
502 </td>
516 </td>
503 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
517 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
504 <i class="icon-expand-linked"></i>
518 <i class="icon-expand-linked"></i>
505 </td>
519 </td>
506 <td class="mid td-description">
520 <td class="mid td-description">
507 <div class="log-container truncate-wrap">
521 <div class="log-container truncate-wrap">
508 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
522 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
509 </div>
523 </div>
510 </td>
524 </td>
511 </tr>
525 </tr>
512 % endif
526 % endif
513 % endfor
527 % endfor
514 </table>
528 </table>
515 </div>
529 </div>
516
530
517 % endif
531 % endif
518
532
519 % else:
533 % else:
520 <%include file="/compare/compare_commits.mako" />
534 <%include file="/compare/compare_commits.mako" />
521 % endif
535 % endif
522
536
523 <div class="cs_files">
537 <div class="cs_files">
524 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
538 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
525 % if c.at_version:
539 % if c.at_version:
526 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
540 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
527 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
541 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
528 % else:
542 % else:
529 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
543 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
530 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
544 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
531 % endif
545 % endif
532
546
533 <%
547 <%
534 pr_menu_data = {
548 pr_menu_data = {
535 'outdated_comm_count_ver': outdated_comm_count_ver
549 'outdated_comm_count_ver': outdated_comm_count_ver
536 }
550 }
537 %>
551 %>
538
552
539 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
553 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
540
554
541 % if c.range_diff_on:
555 % if c.range_diff_on:
542 % for commit in c.commit_ranges:
556 % for commit in c.commit_ranges:
543 ${cbdiffs.render_diffset(
557 ${cbdiffs.render_diffset(
544 c.changes[commit.raw_id],
558 c.changes[commit.raw_id],
545 commit=commit, use_comments=True,
559 commit=commit, use_comments=True,
546 collapse_when_files_over=5,
560 collapse_when_files_over=5,
547 disable_new_comments=True,
561 disable_new_comments=True,
548 deleted_files_comments=c.deleted_files_comments,
562 deleted_files_comments=c.deleted_files_comments,
549 inline_comments=c.inline_comments,
563 inline_comments=c.inline_comments,
550 pull_request_menu=pr_menu_data)}
564 pull_request_menu=pr_menu_data)}
551 % endfor
565 % endfor
552 % else:
566 % else:
553 ${cbdiffs.render_diffset(
567 ${cbdiffs.render_diffset(
554 c.diffset, use_comments=True,
568 c.diffset, use_comments=True,
555 collapse_when_files_over=30,
569 collapse_when_files_over=30,
556 disable_new_comments=not c.allowed_to_comment,
570 disable_new_comments=not c.allowed_to_comment,
557 deleted_files_comments=c.deleted_files_comments,
571 deleted_files_comments=c.deleted_files_comments,
558 inline_comments=c.inline_comments,
572 inline_comments=c.inline_comments,
559 pull_request_menu=pr_menu_data)}
573 pull_request_menu=pr_menu_data)}
560 % endif
574 % endif
561
575
562 </div>
576 </div>
563 % else:
577 % else:
564 ## skipping commits we need to clear the view for missing commits
578 ## skipping commits we need to clear the view for missing commits
565 <div style="clear:both;"></div>
579 <div style="clear:both;"></div>
566 % endif
580 % endif
567
581
568 </div>
582 </div>
569 </div>
583 </div>
570
584
571 ## template for inline comment form
585 ## template for inline comment form
572 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
586 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
573
587
574 ## comments heading with count
588 ## comments heading with count
575 <div class="comments-heading">
589 <div class="comments-heading">
576 <i class="icon-comment"></i>
590 <i class="icon-comment"></i>
577 ${_('Comments')} ${len(c.comments)}
591 ${_('Comments')} ${len(c.comments)}
578 </div>
592 </div>
579
593
580 ## render general comments
594 ## render general comments
581 <div id="comment-tr-show">
595 <div id="comment-tr-show">
582 % if general_outdated_comm_count_ver:
596 % if general_outdated_comm_count_ver:
583 <div class="info-box">
597 <div class="info-box">
584 % if general_outdated_comm_count_ver == 1:
598 % if general_outdated_comm_count_ver == 1:
585 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
586 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
587 % else:
601 % else:
588 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
589 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
590 % endif
604 % endif
591 </div>
605 </div>
592 % endif
606 % endif
593 </div>
607 </div>
594
608
595 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
609 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
596
610
597 % if not c.pull_request.is_closed():
611 % if not c.pull_request.is_closed():
598 ## main comment form and it status
612 ## main comment form and it status
599 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
613 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
600 pull_request_id=c.pull_request.pull_request_id),
614 pull_request_id=c.pull_request.pull_request_id),
601 c.pull_request_review_status,
615 c.pull_request_review_status,
602 is_pull_request=True, change_status=c.allowed_to_change_status)}
616 is_pull_request=True, change_status=c.allowed_to_change_status)}
603
617
604 ## merge status, and merge action
618 ## merge status, and merge action
605 <div class="pull-request-merge">
619 <div class="pull-request-merge">
606 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
620 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
607 </div>
621 </div>
608
622
609 %endif
623 %endif
610
624
611 % endif
625 % endif
612 </div>
626 </div>
613
627
614 <script type="text/javascript">
628 <script type="text/javascript">
615
629
616 versionController = new VersionController();
630 versionController = new VersionController();
617 versionController.init();
631 versionController.init();
618
632
619 reviewersController = new ReviewersController();
633 reviewersController = new ReviewersController();
620 commitsController = new CommitsController();
634 commitsController = new CommitsController();
621
635
622 updateController = new UpdatePrController();
636 updateController = new UpdatePrController();
623
637
624 $(function(){
638 $(function () {
625
639
626 // custom code mirror
640 // custom code mirror
627 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
641 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
628
642
629 var PRDetails = {
643 var PRDetails = {
630 editButton: $('#open_edit_pullrequest'),
644 editButton: $('#open_edit_pullrequest'),
631 closeButton: $('#close_edit_pullrequest'),
645 closeButton: $('#close_edit_pullrequest'),
632 deleteButton: $('#delete_pullrequest'),
646 deleteButton: $('#delete_pullrequest'),
633 viewFields: $('#pr-desc, #pr-title'),
647 viewFields: $('#pr-desc, #pr-title'),
634 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
648 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
635
649
636 init: function() {
650 init: function () {
637 var that = this;
651 var that = this;
638 this.editButton.on('click', function(e) { that.edit(); });
652 this.editButton.on('click', function (e) {
639 this.closeButton.on('click', function(e) { that.view(); });
653 that.edit();
654 });
655 this.closeButton.on('click', function (e) {
656 that.view();
657 });
640 },
658 },
641
659
642 edit: function(event) {
660 edit: function (event) {
643 this.viewFields.hide();
661 this.viewFields.hide();
644 this.editButton.hide();
662 this.editButton.hide();
645 this.deleteButton.hide();
663 this.deleteButton.hide();
646 this.closeButton.show();
664 this.closeButton.show();
647 this.editFields.show();
665 this.editFields.show();
648 codeMirrorInstance.refresh();
666 codeMirrorInstance.refresh();
649 },
667 },
650
668
651 view: function(event) {
669 view: function (event) {
652 this.editButton.show();
670 this.editButton.show();
653 this.deleteButton.show();
671 this.deleteButton.show();
654 this.editFields.hide();
672 this.editFields.hide();
655 this.closeButton.hide();
673 this.closeButton.hide();
656 this.viewFields.show();
674 this.viewFields.show();
657 }
675 }
658 };
676 };
659
677
660 var ReviewersPanel = {
678 var ReviewersPanel = {
661 editButton: $('#open_edit_reviewers'),
679 editButton: $('#open_edit_reviewers'),
662 closeButton: $('#close_edit_reviewers'),
680 closeButton: $('#close_edit_reviewers'),
663 addButton: $('#add_reviewer'),
681 addButton: $('#add_reviewer'),
664 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
682 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
665
683
666 init: function() {
684 init: function () {
667 var self = this;
685 var self = this;
668 this.editButton.on('click', function(e) { self.edit(); });
686 this.editButton.on('click', function (e) {
669 this.closeButton.on('click', function(e) { self.close(); });
687 self.edit();
688 });
689 this.closeButton.on('click', function (e) {
690 self.close();
691 });
670 },
692 },
671
693
672 edit: function(event) {
694 edit: function (event) {
673 this.editButton.hide();
695 this.editButton.hide();
674 this.closeButton.show();
696 this.closeButton.show();
675 this.addButton.show();
697 this.addButton.show();
676 this.removeButtons.css('visibility', 'visible');
698 this.removeButtons.css('visibility', 'visible');
677 // review rules
699 // review rules
678 reviewersController.loadReviewRules(
700 reviewersController.loadReviewRules(
679 ${c.pull_request.reviewer_data_json | n});
701 ${c.pull_request.reviewer_data_json | n});
680 },
702 },
681
703
682 close: function(event) {
704 close: function (event) {
683 this.editButton.show();
705 this.editButton.show();
684 this.closeButton.hide();
706 this.closeButton.hide();
685 this.addButton.hide();
707 this.addButton.hide();
686 this.removeButtons.css('visibility', 'hidden');
708 this.removeButtons.css('visibility', 'hidden');
687 // hide review rules
709 // hide review rules
688 reviewersController.hideReviewRules()
710 reviewersController.hideReviewRules()
689 }
711 }
690 };
712 };
691
713
692 PRDetails.init();
714 PRDetails.init();
693 ReviewersPanel.init();
715 ReviewersPanel.init();
694
716
695 showOutdated = function(self){
717 showOutdated = function (self) {
696 $('.comment-inline.comment-outdated').show();
718 $('.comment-inline.comment-outdated').show();
697 $('.filediff-outdated').show();
719 $('.filediff-outdated').show();
698 $('.showOutdatedComments').hide();
720 $('.showOutdatedComments').hide();
699 $('.hideOutdatedComments').show();
721 $('.hideOutdatedComments').show();
700 };
722 };
701
723
702 hideOutdated = function(self){
724 hideOutdated = function (self) {
703 $('.comment-inline.comment-outdated').hide();
725 $('.comment-inline.comment-outdated').hide();
704 $('.filediff-outdated').hide();
726 $('.filediff-outdated').hide();
705 $('.hideOutdatedComments').hide();
727 $('.hideOutdatedComments').hide();
706 $('.showOutdatedComments').show();
728 $('.showOutdatedComments').show();
707 };
729 };
708
730
709 refreshMergeChecks = function(){
731 refreshMergeChecks = function () {
710 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
732 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
711 $('.pull-request-merge').css('opacity', 0.3);
733 $('.pull-request-merge').css('opacity', 0.3);
712 $('.action-buttons-extra').css('opacity', 0.3);
734 $('.action-buttons-extra').css('opacity', 0.3);
713
735
714 $('.pull-request-merge').load(
736 $('.pull-request-merge').load(
715 loadUrl, function() {
737 loadUrl, function () {
716 $('.pull-request-merge').css('opacity', 1);
738 $('.pull-request-merge').css('opacity', 1);
717
739
718 $('.action-buttons-extra').css('opacity', 1);
740 $('.action-buttons-extra').css('opacity', 1);
719 }
741 }
720 );
742 );
721 };
743 };
722
744
723 closePullRequest = function (status) {
745 closePullRequest = function (status) {
724 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
746 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
725 return false;
747 return false;
726 }
748 }
727 // inject closing flag
749 // inject closing flag
728 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
750 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
729 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
751 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
730 $(generalCommentForm.submitForm).submit();
752 $(generalCommentForm.submitForm).submit();
731 };
753 };
732
754
733 $('#show-outdated-comments').on('click', function(e){
755 $('#show-outdated-comments').on('click', function (e) {
734 var button = $(this);
756 var button = $(this);
735 var outdated = $('.comment-outdated');
757 var outdated = $('.comment-outdated');
736
758
737 if (button.html() === "(Show)") {
759 if (button.html() === "(Show)") {
738 button.html("(Hide)");
760 button.html("(Hide)");
739 outdated.show();
761 outdated.show();
740 } else {
762 } else {
741 button.html("(Show)");
763 button.html("(Show)");
742 outdated.hide();
764 outdated.hide();
743 }
765 }
744 });
766 });
745
767
746 $('.show-inline-comments').on('change', function(e){
768 $('.show-inline-comments').on('change', function (e) {
747 var show = 'none';
769 var show = 'none';
748 var target = e.currentTarget;
770 var target = e.currentTarget;
749 if(target.checked){
771 if (target.checked) {
750 show = ''
772 show = ''
751 }
773 }
752 var boxid = $(target).attr('id_for');
774 var boxid = $(target).attr('id_for');
753 var comments = $('#{0} .inline-comments'.format(boxid));
775 var comments = $('#{0} .inline-comments'.format(boxid));
754 var fn_display = function(idx){
776 var fn_display = function (idx) {
755 $(this).css('display', show);
777 $(this).css('display', show);
756 };
778 };
757 $(comments).each(fn_display);
779 $(comments).each(fn_display);
758 var btns = $('#{0} .inline-comments-button'.format(boxid));
780 var btns = $('#{0} .inline-comments-button'.format(boxid));
759 $(btns).each(fn_display);
781 $(btns).each(fn_display);
760 });
782 });
761
783
762 $('#merge_pull_request_form').submit(function() {
784 $('#merge_pull_request_form').submit(function () {
763 if (!$('#merge_pull_request').attr('disabled')) {
785 if (!$('#merge_pull_request').attr('disabled')) {
764 $('#merge_pull_request').attr('disabled', 'disabled');
786 $('#merge_pull_request').attr('disabled', 'disabled');
765 }
787 }
766 return true;
788 return true;
767 });
789 });
768
790
769 $('#edit_pull_request').on('click', function(e){
791 $('#edit_pull_request').on('click', function (e) {
770 var title = $('#pr-title-input').val();
792 var title = $('#pr-title-input').val();
771 var description = codeMirrorInstance.getValue();
793 var description = codeMirrorInstance.getValue();
772 var renderer = $('#pr-renderer-input').val();
794 var renderer = $('#pr-renderer-input').val();
773 editPullRequest(
795 editPullRequest(
774 "${c.repo_name}", "${c.pull_request.pull_request_id}",
796 "${c.repo_name}", "${c.pull_request.pull_request_id}",
775 title, description, renderer);
797 title, description, renderer);
776 });
798 });
777
799
778 $('#update_pull_request').on('click', function(e){
800 $('#update_pull_request').on('click', function (e) {
779 $(this).attr('disabled', 'disabled');
801 $(this).attr('disabled', 'disabled');
780 $(this).addClass('disabled');
802 $(this).addClass('disabled');
781 $(this).html(_gettext('Saving...'));
803 $(this).html(_gettext('Saving...'));
782 reviewersController.updateReviewers(
804 reviewersController.updateReviewers(
783 "${c.repo_name}", "${c.pull_request.pull_request_id}");
805 "${c.repo_name}", "${c.pull_request.pull_request_id}");
784 });
806 });
785
807
786
808
787 // fixing issue with caches on firefox
809 // fixing issue with caches on firefox
788 $('#update_commits').removeAttr("disabled");
810 $('#update_commits').removeAttr("disabled");
789
811
790 $('.show-inline-comments').on('click', function(e){
812 $('.show-inline-comments').on('click', function (e) {
791 var boxid = $(this).attr('data-comment-id');
813 var boxid = $(this).attr('data-comment-id');
792 var button = $(this);
814 var button = $(this);
793
815
794 if(button.hasClass("comments-visible")) {
816 if (button.hasClass("comments-visible")) {
795 $('#{0} .inline-comments'.format(boxid)).each(function(index){
817 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
796 $(this).hide();
818 $(this).hide();
797 });
819 });
798 button.removeClass("comments-visible");
820 button.removeClass("comments-visible");
799 } else {
821 } else {
800 $('#{0} .inline-comments'.format(boxid)).each(function(index){
822 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
801 $(this).show();
823 $(this).show();
802 });
824 });
803 button.addClass("comments-visible");
825 button.addClass("comments-visible");
804 }
826 }
805 });
827 });
806
828
807 // register submit callback on commentForm form to track TODOs
829 // register submit callback on commentForm form to track TODOs
808 window.commentFormGlobalSubmitSuccessCallback = function(){
830 window.commentFormGlobalSubmitSuccessCallback = function () {
809 refreshMergeChecks();
831 refreshMergeChecks();
810 };
832 };
811
833
812 ReviewerAutoComplete('#user');
834 ReviewerAutoComplete('#user');
813
835
814 })
836 })
815
837
816 </script>
838 </script>
839
817 </div>
840 </div>
818
841
819 </%def>
842 </%def>
General Comments 0
You need to be logged in to leave comments. Login now