##// END OF EJS Templates
commits/ux: use similar as in files expand/collapse toggle.
marcink -
r4126:9f1311d3 default
parent child Browse files
Show More
@@ -1,265 +1,291 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 import pytest
21 import pytest
22
22
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs import nodes
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.utils import commit_change
27 from rhodecode.tests.utils import commit_change
28
28
29 fixture = Fixture()
29 fixture = Fixture()
30
30
31
31
32 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
33 import urllib
33 import urllib
34
34
35 base_url = {
35 base_url = {
36 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare_select': '/{repo_name}/compare',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
38 }[name].format(**kwargs)
39
39
40 if params:
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 return base_url
42 return base_url
43
43
44
44
45 @pytest.mark.usefixtures("autologin_user", "app")
45 @pytest.mark.usefixtures("autologin_user", "app")
46 class TestSideBySideDiff(object):
46 class TestSideBySideDiff(object):
47
47
48 def test_diff_sidebyside_single_commit(self, app, backend):
48 def test_diff_sidebyside_single_commit(self, app, backend):
49 commit_id_range = {
49 commit_id_range = {
50 'hg': {
50 'hg': {
51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
53 'changes': (21, 943, 288),
53 'changes': (21, 943, 288),
54 },
54 },
55 'git': {
55 'git': {
56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
58 'changes': (20, 941, 286),
58 'changes': (20, 941, 286),
59 },
59 },
60
60
61 'svn': {
61 'svn': {
62 'commits': ['336',
62 'commits': ['336',
63 '337'],
63 '337'],
64 'changes': (21, 943, 288),
64 'changes': (21, 943, 288),
65 },
65 },
66 }
66 }
67
67
68 commit_info = commit_id_range[backend.alias]
68 commit_info = commit_id_range[backend.alias]
69 commit2, commit1 = commit_info['commits']
69 commit2, commit1 = commit_info['commits']
70 file_changes = commit_info['changes']
70 file_changes = commit_info['changes']
71
71
72 response = self.app.get(route_path(
72 response = self.app.get(route_path(
73 'repo_compare',
73 'repo_compare',
74 repo_name=backend.repo_name,
74 repo_name=backend.repo_name,
75 source_ref_type='rev',
75 source_ref_type='rev',
76 source_ref=commit2,
76 source_ref=commit2,
77 target_repo=backend.repo_name,
77 target_repo=backend.repo_name,
78 target_ref_type='rev',
78 target_ref_type='rev',
79 target_ref=commit1,
79 target_ref=commit1,
80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
81 ))
81 ))
82
82
83 compare_page = ComparePage(response)
83 compare_page = ComparePage(response)
84 compare_page.contains_change_summary(*file_changes)
84 compare_page.contains_change_summary(*file_changes)
85 response.mustcontain('Expand 1 commit')
85 response.mustcontain('Collapse 1 commit')
86
86
87 def test_diff_sidebyside_two_commits(self, app, backend):
87 def test_diff_sidebyside_two_commits(self, app, backend):
88 commit_id_range = {
88 commit_id_range = {
89 'hg': {
89 'hg': {
90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
92 'changes': (32, 1165, 308),
92 'changes': (32, 1165, 308),
93 },
93 },
94 'git': {
94 'git': {
95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
97 'changes': (31, 1163, 306),
97 'changes': (31, 1163, 306),
98 },
98 },
99
99
100 'svn': {
100 'svn': {
101 'commits': ['335',
101 'commits': ['335',
102 '337'],
102 '337'],
103 'changes': (32, 1179, 310),
103 'changes': (32, 1179, 310),
104 },
104 },
105 }
105 }
106
106
107 commit_info = commit_id_range[backend.alias]
107 commit_info = commit_id_range[backend.alias]
108 commit2, commit1 = commit_info['commits']
108 commit2, commit1 = commit_info['commits']
109 file_changes = commit_info['changes']
109 file_changes = commit_info['changes']
110
110
111 response = self.app.get(route_path(
111 response = self.app.get(route_path(
112 'repo_compare',
112 'repo_compare',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 source_ref_type='rev',
114 source_ref_type='rev',
115 source_ref=commit2,
115 source_ref=commit2,
116 target_repo=backend.repo_name,
116 target_repo=backend.repo_name,
117 target_ref_type='rev',
117 target_ref_type='rev',
118 target_ref=commit1,
118 target_ref=commit1,
119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
120 ))
120 ))
121
121
122 compare_page = ComparePage(response)
122 compare_page = ComparePage(response)
123 compare_page.contains_change_summary(*file_changes)
123 compare_page.contains_change_summary(*file_changes)
124
124
125 response.mustcontain('Expand 2 commits')
125 response.mustcontain('Collapse 2 commits')
126
127 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
128 commit_id_range = {
129
130 'svn': {
131 'commits': ['330',
132 '337'],
133
134 },
135 }
136
137 commit_info = commit_id_range['svn']
138 commit2, commit1 = commit_info['commits']
139
140 response = self.app.get(route_path(
141 'repo_compare',
142 repo_name=backend_svn.repo_name,
143 source_ref_type='rev',
144 source_ref=commit2,
145 target_repo=backend_svn.repo_name,
146 target_ref_type='rev',
147 target_ref=commit1,
148 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
149 ))
150
151 response.mustcontain('Expand 7 commits')
126
152
127 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
153 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
128 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
154 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
129 f_path = 'test_sidebyside_file.py'
155 f_path = 'test_sidebyside_file.py'
130 commit1_content = 'content-25d7e49c18b159446c\n'
156 commit1_content = 'content-25d7e49c18b159446c\n'
131 commit2_content = 'content-603d6c72c46d953420\n'
157 commit2_content = 'content-603d6c72c46d953420\n'
132 repo = backend.create_repo()
158 repo = backend.create_repo()
133
159
134 commit1 = commit_change(
160 commit1 = commit_change(
135 repo.repo_name, filename=f_path, content=commit1_content,
161 repo.repo_name, filename=f_path, content=commit1_content,
136 message='A', vcs_type=backend.alias, parent=None, newfile=True)
162 message='A', vcs_type=backend.alias, parent=None, newfile=True)
137
163
138 commit2 = commit_change(
164 commit2 = commit_change(
139 repo.repo_name, filename=f_path, content=commit2_content,
165 repo.repo_name, filename=f_path, content=commit2_content,
140 message='B, child of A', vcs_type=backend.alias, parent=commit1)
166 message='B, child of A', vcs_type=backend.alias, parent=commit1)
141
167
142 response = self.app.get(route_path(
168 response = self.app.get(route_path(
143 'repo_compare',
169 'repo_compare',
144 repo_name=repo.repo_name,
170 repo_name=repo.repo_name,
145 source_ref_type='rev',
171 source_ref_type='rev',
146 source_ref=EmptyCommit().raw_id,
172 source_ref=EmptyCommit().raw_id,
147 target_ref_type='rev',
173 target_ref_type='rev',
148 target_ref=commit2.raw_id,
174 target_ref=commit2.raw_id,
149 params=dict(diffmode='sidebyside')
175 params=dict(diffmode='sidebyside')
150 ))
176 ))
151
177
152 response.mustcontain('Expand 2 commits')
178 response.mustcontain('Collapse 2 commits')
153 response.mustcontain('123 file changed')
179 response.mustcontain('123 file changed')
154
180
155 response.mustcontain(
181 response.mustcontain(
156 'r%s:%s...r%s:%s' % (
182 'r%s:%s...r%s:%s' % (
157 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
183 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
158
184
159 response.mustcontain(f_path)
185 response.mustcontain(f_path)
160
186
161 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
187 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
162 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
188 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
163 f_path = 'test_sidebyside_file.py'
189 f_path = 'test_sidebyside_file.py'
164 commit1_content = 'content-25d7e49c18b159446c\n'
190 commit1_content = 'content-25d7e49c18b159446c\n'
165 commit2_content = 'content-603d6c72c46d953420\n'
191 commit2_content = 'content-603d6c72c46d953420\n'
166 repo = backend.create_repo()
192 repo = backend.create_repo()
167
193
168 commit1 = commit_change(
194 commit1 = commit_change(
169 repo.repo_name, filename=f_path, content=commit1_content,
195 repo.repo_name, filename=f_path, content=commit1_content,
170 message='A', vcs_type=backend.alias, parent=None, newfile=True)
196 message='A', vcs_type=backend.alias, parent=None, newfile=True)
171
197
172 commit2 = commit_change(
198 commit2 = commit_change(
173 repo.repo_name, filename=f_path, content=commit2_content,
199 repo.repo_name, filename=f_path, content=commit2_content,
174 message='B, child of A', vcs_type=backend.alias, parent=commit1)
200 message='B, child of A', vcs_type=backend.alias, parent=commit1)
175
201
176 response = self.app.get(route_path(
202 response = self.app.get(route_path(
177 'repo_compare',
203 'repo_compare',
178 repo_name=repo.repo_name,
204 repo_name=repo.repo_name,
179 source_ref_type='rev',
205 source_ref_type='rev',
180 source_ref=EmptyCommit().raw_id,
206 source_ref=EmptyCommit().raw_id,
181 target_ref_type='rev',
207 target_ref_type='rev',
182 target_ref=commit2.raw_id,
208 target_ref=commit2.raw_id,
183 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
209 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
184 ))
210 ))
185
211
186 response.mustcontain('Expand 2 commits')
212 response.mustcontain('Collapse 2 commits')
187 response.mustcontain('1 file changed')
213 response.mustcontain('1 file changed')
188
214
189 response.mustcontain(
215 response.mustcontain(
190 'r%s:%s...r%s:%s' % (
216 'r%s:%s...r%s:%s' % (
191 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
217 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
192
218
193 response.mustcontain(f_path)
219 response.mustcontain(f_path)
194
220
195 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
221 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
196 commits = [
222 commits = [
197 {'message': 'First commit'},
223 {'message': 'First commit'},
198 {'message': 'Second commit'},
224 {'message': 'Second commit'},
199 {'message': 'Commit with binary',
225 {'message': 'Commit with binary',
200 'added': [nodes.FileNode('file.empty', content='')]},
226 'added': [nodes.FileNode('file.empty', content='')]},
201 ]
227 ]
202 f_path = 'file.empty'
228 f_path = 'file.empty'
203 repo = backend.create_repo(commits=commits)
229 repo = backend.create_repo(commits=commits)
204 commit1 = repo.get_commit(commit_idx=0)
230 commit1 = repo.get_commit(commit_idx=0)
205 commit2 = repo.get_commit(commit_idx=1)
231 commit2 = repo.get_commit(commit_idx=1)
206 commit3 = repo.get_commit(commit_idx=2)
232 commit3 = repo.get_commit(commit_idx=2)
207
233
208 response = self.app.get(route_path(
234 response = self.app.get(route_path(
209 'repo_compare',
235 'repo_compare',
210 repo_name=repo.repo_name,
236 repo_name=repo.repo_name,
211 source_ref_type='rev',
237 source_ref_type='rev',
212 source_ref=commit1.raw_id,
238 source_ref=commit1.raw_id,
213 target_ref_type='rev',
239 target_ref_type='rev',
214 target_ref=commit3.raw_id,
240 target_ref=commit3.raw_id,
215 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
241 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
216 ))
242 ))
217
243
218 response.mustcontain('Expand 2 commits')
244 response.mustcontain('Collapse 2 commits')
219 response.mustcontain('1 file changed')
245 response.mustcontain('1 file changed')
220
246
221 response.mustcontain(
247 response.mustcontain(
222 'r%s:%s...r%s:%s' % (
248 'r%s:%s...r%s:%s' % (
223 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
249 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
224
250
225 response.mustcontain(f_path)
251 response.mustcontain(f_path)
226
252
227 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
253 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
228 commit_id_range = {
254 commit_id_range = {
229 'hg': {
255 'hg': {
230 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
256 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
231 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
257 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
232 'changes': (1, 3, 3)
258 'changes': (1, 3, 3)
233 },
259 },
234 'git': {
260 'git': {
235 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
261 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
236 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
262 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
237 'changes': (1, 3, 3)
263 'changes': (1, 3, 3)
238 },
264 },
239
265
240 'svn': {
266 'svn': {
241 'commits': ['335',
267 'commits': ['335',
242 '337'],
268 '337'],
243 'changes': (1, 3, 3)
269 'changes': (1, 3, 3)
244 },
270 },
245 }
271 }
246 f_path = 'docs/conf.py'
272 f_path = 'docs/conf.py'
247
273
248 commit_info = commit_id_range[backend.alias]
274 commit_info = commit_id_range[backend.alias]
249 commit2, commit1 = commit_info['commits']
275 commit2, commit1 = commit_info['commits']
250 file_changes = commit_info['changes']
276 file_changes = commit_info['changes']
251
277
252 response = self.app.get(route_path(
278 response = self.app.get(route_path(
253 'repo_compare',
279 'repo_compare',
254 repo_name=backend.repo_name,
280 repo_name=backend.repo_name,
255 source_ref_type='rev',
281 source_ref_type='rev',
256 source_ref=commit2,
282 source_ref=commit2,
257 target_ref_type='rev',
283 target_ref_type='rev',
258 target_ref=commit1,
284 target_ref=commit1,
259 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
285 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
260 ))
286 ))
261
287
262 response.mustcontain('Expand 2 commits')
288 response.mustcontain('Collapse 2 commits')
263
289
264 compare_page = ComparePage(response)
290 compare_page = ComparePage(response)
265 compare_page.contains_change_summary(*file_changes)
291 compare_page.contains_change_summary(*file_changes)
@@ -1,1070 +1,1070 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 import os
21 import os
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.compat import OrderedDict
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
32
32
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
36 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
37
37
38 fixture = Fixture()
38 fixture = Fixture()
39
39
40
40
41 def get_node_history(backend_type):
41 def get_node_history(backend_type):
42 return {
42 return {
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 }[backend_type]
46 }[backend_type]
47
47
48
48
49 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
50 import urllib
50 import urllib
51
51
52 base_url = {
52 base_url = {
53 'repo_summary': '/{repo_name}',
53 'repo_summary': '/{repo_name}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:default_commit': '/{repo_name}/files',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 }[name].format(**kwargs)
76 }[name].format(**kwargs)
77
77
78 if params:
78 if params:
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
80 return base_url
80 return base_url
81
81
82
82
83 def assert_files_in_response(response, files, params):
83 def assert_files_in_response(response, files, params):
84 template = (
84 template = (
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 _assert_items_in_response(response, files, template, params)
86 _assert_items_in_response(response, files, template, params)
87
87
88
88
89 def assert_dirs_in_response(response, dirs, params):
89 def assert_dirs_in_response(response, dirs, params):
90 template = (
90 template = (
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 _assert_items_in_response(response, dirs, template, params)
92 _assert_items_in_response(response, dirs, template, params)
93
93
94
94
95 def _assert_items_in_response(response, items, template, params):
95 def _assert_items_in_response(response, items, template, params):
96 for item in items:
96 for item in items:
97 item_params = {'name': item}
97 item_params = {'name': item}
98 item_params.update(params)
98 item_params.update(params)
99 response.mustcontain(template % item_params)
99 response.mustcontain(template % item_params)
100
100
101
101
102 def assert_timeago_in_response(response, items, params):
102 def assert_timeago_in_response(response, items, params):
103 for item in items:
103 for item in items:
104 response.mustcontain(h.age_component(params['date']))
104 response.mustcontain(h.age_component(params['date']))
105
105
106
106
107 @pytest.mark.usefixtures("app")
107 @pytest.mark.usefixtures("app")
108 class TestFilesViews(object):
108 class TestFilesViews(object):
109
109
110 def test_show_files(self, backend):
110 def test_show_files(self, backend):
111 response = self.app.get(
111 response = self.app.get(
112 route_path('repo_files',
112 route_path('repo_files',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
115 commit = backend.repo.get_commit()
115 commit = backend.repo.get_commit()
116
116
117 params = {
117 params = {
118 'repo_name': backend.repo_name,
118 'repo_name': backend.repo_name,
119 'commit_id': commit.raw_id,
119 'commit_id': commit.raw_id,
120 'date': commit.date
120 'date': commit.date
121 }
121 }
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 files = [
123 files = [
124 '.gitignore',
124 '.gitignore',
125 '.hgignore',
125 '.hgignore',
126 '.hgtags',
126 '.hgtags',
127 # TODO: missing in Git
127 # TODO: missing in Git
128 # '.travis.yml',
128 # '.travis.yml',
129 'MANIFEST.in',
129 'MANIFEST.in',
130 'README.rst',
130 'README.rst',
131 # TODO: File is missing in svn repository
131 # TODO: File is missing in svn repository
132 # 'run_test_and_report.sh',
132 # 'run_test_and_report.sh',
133 'setup.cfg',
133 'setup.cfg',
134 'setup.py',
134 'setup.py',
135 'test_and_report.sh',
135 'test_and_report.sh',
136 'tox.ini',
136 'tox.ini',
137 ]
137 ]
138 assert_files_in_response(response, files, params)
138 assert_files_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
140
140
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 repo = backend_hg['subrepos']
142 repo = backend_hg['subrepos']
143 response = self.app.get(
143 response = self.app.get(
144 route_path('repo_files',
144 route_path('repo_files',
145 repo_name=repo.repo_name,
145 repo_name=repo.repo_name,
146 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 assert_response.contains_one_link(
148 assert_response.contains_one_link(
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150
150
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 self, backend_hg):
152 self, backend_hg):
153 repo = backend_hg['subrepos']
153 repo = backend_hg['subrepos']
154 response = self.app.get(
154 response = self.app.get(
155 route_path('repo_files',
155 route_path('repo_files',
156 repo_name=repo.repo_name,
156 repo_name=repo.repo_name,
157 commit_id='tip', f_path='/'))
157 commit_id='tip', f_path='/'))
158 assert_response = response.assert_response()
158 assert_response = response.assert_response()
159 assert_response.contains_one_link(
159 assert_response.contains_one_link(
160 'subpaths-path @ 000000000000',
160 'subpaths-path @ 000000000000',
161 'http://sub-base.example.com/subpaths-path')
161 'http://sub-base.example.com/subpaths-path')
162
162
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 def test_files_menu(self, backend):
164 def test_files_menu(self, backend):
165 new_branch = "temp_branch_name"
165 new_branch = "temp_branch_name"
166 commits = [
166 commits = [
167 {'message': 'a'},
167 {'message': 'a'},
168 {'message': 'b', 'branch': new_branch}
168 {'message': 'b', 'branch': new_branch}
169 ]
169 ]
170 backend.create_repo(commits)
170 backend.create_repo(commits)
171 backend.repo.landing_rev = "branch:%s" % new_branch
171 backend.repo.landing_rev = "branch:%s" % new_branch
172 Session().commit()
172 Session().commit()
173
173
174 # get response based on tip and not new commit
174 # get response based on tip and not new commit
175 response = self.app.get(
175 response = self.app.get(
176 route_path('repo_files',
176 route_path('repo_files',
177 repo_name=backend.repo_name,
177 repo_name=backend.repo_name,
178 commit_id='tip', f_path='/'))
178 commit_id='tip', f_path='/'))
179
179
180 # make sure Files menu url is not tip but new commit
180 # make sure Files menu url is not tip but new commit
181 landing_rev = backend.repo.landing_rev[1]
181 landing_rev = backend.repo.landing_rev[1]
182 files_url = route_path('repo_files:default_path',
182 files_url = route_path('repo_files:default_path',
183 repo_name=backend.repo_name,
183 repo_name=backend.repo_name,
184 commit_id=landing_rev)
184 commit_id=landing_rev)
185
185
186 assert landing_rev != 'tip'
186 assert landing_rev != 'tip'
187 response.mustcontain(
187 response.mustcontain(
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189
189
190 def test_show_files_commit(self, backend):
190 def test_show_files_commit(self, backend):
191 commit = backend.repo.get_commit(commit_idx=32)
191 commit = backend.repo.get_commit(commit_idx=32)
192
192
193 response = self.app.get(
193 response = self.app.get(
194 route_path('repo_files',
194 route_path('repo_files',
195 repo_name=backend.repo_name,
195 repo_name=backend.repo_name,
196 commit_id=commit.raw_id, f_path='/'))
196 commit_id=commit.raw_id, f_path='/'))
197
197
198 dirs = ['docs', 'tests']
198 dirs = ['docs', 'tests']
199 files = ['README.rst']
199 files = ['README.rst']
200 params = {
200 params = {
201 'repo_name': backend.repo_name,
201 'repo_name': backend.repo_name,
202 'commit_id': commit.raw_id,
202 'commit_id': commit.raw_id,
203 }
203 }
204 assert_dirs_in_response(response, dirs, params)
204 assert_dirs_in_response(response, dirs, params)
205 assert_files_in_response(response, files, params)
205 assert_files_in_response(response, files, params)
206
206
207 def test_show_files_different_branch(self, backend):
207 def test_show_files_different_branch(self, backend):
208 branches = dict(
208 branches = dict(
209 hg=(150, ['git']),
209 hg=(150, ['git']),
210 # TODO: Git test repository does not contain other branches
210 # TODO: Git test repository does not contain other branches
211 git=(633, ['master']),
211 git=(633, ['master']),
212 # TODO: Branch support in Subversion
212 # TODO: Branch support in Subversion
213 svn=(150, [])
213 svn=(150, [])
214 )
214 )
215 idx, branches = branches[backend.alias]
215 idx, branches = branches[backend.alias]
216 commit = backend.repo.get_commit(commit_idx=idx)
216 commit = backend.repo.get_commit(commit_idx=idx)
217 response = self.app.get(
217 response = self.app.get(
218 route_path('repo_files',
218 route_path('repo_files',
219 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
220 commit_id=commit.raw_id, f_path='/'))
220 commit_id=commit.raw_id, f_path='/'))
221
221
222 assert_response = response.assert_response()
222 assert_response = response.assert_response()
223 for branch in branches:
223 for branch in branches:
224 assert_response.element_contains('.tags .branchtag', branch)
224 assert_response.element_contains('.tags .branchtag', branch)
225
225
226 def test_show_files_paging(self, backend):
226 def test_show_files_paging(self, backend):
227 repo = backend.repo
227 repo = backend.repo
228 indexes = [73, 92, 109, 1, 0]
228 indexes = [73, 92, 109, 1, 0]
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 for rev in indexes]
230 for rev in indexes]
231
231
232 for idx in idx_map:
232 for idx in idx_map:
233 response = self.app.get(
233 response = self.app.get(
234 route_path('repo_files',
234 route_path('repo_files',
235 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
236 commit_id=idx[1], f_path='/'))
236 commit_id=idx[1], f_path='/'))
237
237
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239
239
240 def test_file_source(self, backend):
240 def test_file_source(self, backend):
241 commit = backend.repo.get_commit(commit_idx=167)
241 commit = backend.repo.get_commit(commit_idx=167)
242 response = self.app.get(
242 response = self.app.get(
243 route_path('repo_files',
243 route_path('repo_files',
244 repo_name=backend.repo_name,
244 repo_name=backend.repo_name,
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246
246
247 msgbox = """<div class="commit">%s</div>"""
247 msgbox = """<div class="commit">%s</div>"""
248 response.mustcontain(msgbox % (commit.message, ))
248 response.mustcontain(msgbox % (commit.message, ))
249
249
250 assert_response = response.assert_response()
250 assert_response = response.assert_response()
251 if commit.branch:
251 if commit.branch:
252 assert_response.element_contains(
252 assert_response.element_contains(
253 '.tags.tags-main .branchtag', commit.branch)
253 '.tags.tags-main .branchtag', commit.branch)
254 if commit.tags:
254 if commit.tags:
255 for tag in commit.tags:
255 for tag in commit.tags:
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257
257
258 def test_file_source_annotated(self, backend):
258 def test_file_source_annotated(self, backend):
259 response = self.app.get(
259 response = self.app.get(
260 route_path('repo_files:annotated',
260 route_path('repo_files:annotated',
261 repo_name=backend.repo_name,
261 repo_name=backend.repo_name,
262 commit_id='tip', f_path='vcs/nodes.py'))
262 commit_id='tip', f_path='vcs/nodes.py'))
263 expected_commits = {
263 expected_commits = {
264 'hg': 'r356',
264 'hg': 'r356',
265 'git': 'r345',
265 'git': 'r345',
266 'svn': 'r208',
266 'svn': 'r208',
267 }
267 }
268 response.mustcontain(expected_commits[backend.alias])
268 response.mustcontain(expected_commits[backend.alias])
269
269
270 def test_file_source_authors(self, backend):
270 def test_file_source_authors(self, backend):
271 response = self.app.get(
271 response = self.app.get(
272 route_path('repo_file_authors',
272 route_path('repo_file_authors',
273 repo_name=backend.repo_name,
273 repo_name=backend.repo_name,
274 commit_id='tip', f_path='vcs/nodes.py'))
274 commit_id='tip', f_path='vcs/nodes.py'))
275 expected_authors = {
275 expected_authors = {
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 'svn': ('marcin', 'lukasz'),
278 'svn': ('marcin', 'lukasz'),
279 }
279 }
280
280
281 for author in expected_authors[backend.alias]:
281 for author in expected_authors[backend.alias]:
282 response.mustcontain(author)
282 response.mustcontain(author)
283
283
284 def test_file_source_authors_with_annotation(self, backend):
284 def test_file_source_authors_with_annotation(self, backend):
285 response = self.app.get(
285 response = self.app.get(
286 route_path('repo_file_authors',
286 route_path('repo_file_authors',
287 repo_name=backend.repo_name,
287 repo_name=backend.repo_name,
288 commit_id='tip', f_path='vcs/nodes.py',
288 commit_id='tip', f_path='vcs/nodes.py',
289 params=dict(annotate=1)))
289 params=dict(annotate=1)))
290 expected_authors = {
290 expected_authors = {
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 'svn': ('marcin', 'lukasz'),
293 'svn': ('marcin', 'lukasz'),
294 }
294 }
295
295
296 for author in expected_authors[backend.alias]:
296 for author in expected_authors[backend.alias]:
297 response.mustcontain(author)
297 response.mustcontain(author)
298
298
299 def test_file_source_history(self, backend, xhr_header):
299 def test_file_source_history(self, backend, xhr_header):
300 response = self.app.get(
300 response = self.app.get(
301 route_path('repo_file_history',
301 route_path('repo_file_history',
302 repo_name=backend.repo_name,
302 repo_name=backend.repo_name,
303 commit_id='tip', f_path='vcs/nodes.py'),
303 commit_id='tip', f_path='vcs/nodes.py'),
304 extra_environ=xhr_header)
304 extra_environ=xhr_header)
305 assert get_node_history(backend.alias) == json.loads(response.body)
305 assert get_node_history(backend.alias) == json.loads(response.body)
306
306
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 simple_repo = backend_svn['svn-simple-layout']
308 simple_repo = backend_svn['svn-simple-layout']
309 response = self.app.get(
309 response = self.app.get(
310 route_path('repo_file_history',
310 route_path('repo_file_history',
311 repo_name=simple_repo.repo_name,
311 repo_name=simple_repo.repo_name,
312 commit_id='tip', f_path='trunk/example.py'),
312 commit_id='tip', f_path='trunk/example.py'),
313 extra_environ=xhr_header)
313 extra_environ=xhr_header)
314
314
315 expected_data = json.loads(
315 expected_data = json.loads(
316 fixture.load_resource('svn_node_history_branches.json'))
316 fixture.load_resource('svn_node_history_branches.json'))
317
317
318 assert expected_data == response.json
318 assert expected_data == response.json
319
319
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 response = self.app.get(
321 response = self.app.get(
322 route_path('repo_file_history',
322 route_path('repo_file_history',
323 repo_name=backend.repo_name,
323 repo_name=backend.repo_name,
324 commit_id='tip', f_path='vcs/nodes.py',
324 commit_id='tip', f_path='vcs/nodes.py',
325 params=dict(annotate=1)),
325 params=dict(annotate=1)),
326
326
327 extra_environ=xhr_header)
327 extra_environ=xhr_header)
328 assert get_node_history(backend.alias) == json.loads(response.body)
328 assert get_node_history(backend.alias) == json.loads(response.body)
329
329
330 def test_tree_search_top_level(self, backend, xhr_header):
330 def test_tree_search_top_level(self, backend, xhr_header):
331 commit = backend.repo.get_commit(commit_idx=173)
331 commit = backend.repo.get_commit(commit_idx=173)
332 response = self.app.get(
332 response = self.app.get(
333 route_path('repo_files_nodelist',
333 route_path('repo_files_nodelist',
334 repo_name=backend.repo_name,
334 repo_name=backend.repo_name,
335 commit_id=commit.raw_id, f_path='/'),
335 commit_id=commit.raw_id, f_path='/'),
336 extra_environ=xhr_header)
336 extra_environ=xhr_header)
337 assert 'nodes' in response.json
337 assert 'nodes' in response.json
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339
339
340 def test_tree_search_missing_xhr(self, backend):
340 def test_tree_search_missing_xhr(self, backend):
341 self.app.get(
341 self.app.get(
342 route_path('repo_files_nodelist',
342 route_path('repo_files_nodelist',
343 repo_name=backend.repo_name,
343 repo_name=backend.repo_name,
344 commit_id='tip', f_path='/'),
344 commit_id='tip', f_path='/'),
345 status=404)
345 status=404)
346
346
347 def test_tree_search_at_path(self, backend, xhr_header):
347 def test_tree_search_at_path(self, backend, xhr_header):
348 commit = backend.repo.get_commit(commit_idx=173)
348 commit = backend.repo.get_commit(commit_idx=173)
349 response = self.app.get(
349 response = self.app.get(
350 route_path('repo_files_nodelist',
350 route_path('repo_files_nodelist',
351 repo_name=backend.repo_name,
351 repo_name=backend.repo_name,
352 commit_id=commit.raw_id, f_path='/docs'),
352 commit_id=commit.raw_id, f_path='/docs'),
353 extra_environ=xhr_header)
353 extra_environ=xhr_header)
354 assert 'nodes' in response.json
354 assert 'nodes' in response.json
355 nodes = response.json['nodes']
355 nodes = response.json['nodes']
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358
358
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 commit = backend.repo.get_commit(commit_idx=173)
360 commit = backend.repo.get_commit(commit_idx=173)
361 response = self.app.get(
361 response = self.app.get(
362 route_path('repo_files_nodelist',
362 route_path('repo_files_nodelist',
363 repo_name=backend.repo_name,
363 repo_name=backend.repo_name,
364 commit_id=commit.raw_id, f_path='/docs/api'),
364 commit_id=commit.raw_id, f_path='/docs/api'),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 assert 'nodes' in response.json
366 assert 'nodes' in response.json
367 nodes = response.json['nodes']
367 nodes = response.json['nodes']
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369
369
370 def test_tree_search_at_path_missing_xhr(self, backend):
370 def test_tree_search_at_path_missing_xhr(self, backend):
371 self.app.get(
371 self.app.get(
372 route_path('repo_files_nodelist',
372 route_path('repo_files_nodelist',
373 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
374 commit_id='tip', f_path='/docs'),
374 commit_id='tip', f_path='/docs'),
375 status=404)
375 status=404)
376
376
377 def test_nodetree(self, backend, xhr_header):
377 def test_nodetree(self, backend, xhr_header):
378 commit = backend.repo.get_commit(commit_idx=173)
378 commit = backend.repo.get_commit(commit_idx=173)
379 response = self.app.get(
379 response = self.app.get(
380 route_path('repo_nodetree_full',
380 route_path('repo_nodetree_full',
381 repo_name=backend.repo_name,
381 repo_name=backend.repo_name,
382 commit_id=commit.raw_id, f_path='/'),
382 commit_id=commit.raw_id, f_path='/'),
383 extra_environ=xhr_header)
383 extra_environ=xhr_header)
384
384
385 assert_response = response.assert_response()
385 assert_response = response.assert_response()
386
386
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 elements = assert_response.get_elements('[{}]'.format(attr))
388 elements = assert_response.get_elements('[{}]'.format(attr))
389 assert len(elements) > 1
389 assert len(elements) > 1
390
390
391 for element in elements:
391 for element in elements:
392 assert element.get(attr)
392 assert element.get(attr)
393
393
394 def test_nodetree_if_file(self, backend, xhr_header):
394 def test_nodetree_if_file(self, backend, xhr_header):
395 commit = backend.repo.get_commit(commit_idx=173)
395 commit = backend.repo.get_commit(commit_idx=173)
396 response = self.app.get(
396 response = self.app.get(
397 route_path('repo_nodetree_full',
397 route_path('repo_nodetree_full',
398 repo_name=backend.repo_name,
398 repo_name=backend.repo_name,
399 commit_id=commit.raw_id, f_path='README.rst'),
399 commit_id=commit.raw_id, f_path='README.rst'),
400 extra_environ=xhr_header)
400 extra_environ=xhr_header)
401 assert response.body == ''
401 assert response.body == ''
402
402
403 def test_nodetree_wrong_path(self, backend, xhr_header):
403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 commit = backend.repo.get_commit(commit_idx=173)
404 commit = backend.repo.get_commit(commit_idx=173)
405 response = self.app.get(
405 response = self.app.get(
406 route_path('repo_nodetree_full',
406 route_path('repo_nodetree_full',
407 repo_name=backend.repo_name,
407 repo_name=backend.repo_name,
408 commit_id=commit.raw_id, f_path='/dont-exist'),
408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 extra_environ=xhr_header)
409 extra_environ=xhr_header)
410
410
411 err = 'error: There is no file nor ' \
411 err = 'error: There is no file nor ' \
412 'directory at the given path'
412 'directory at the given path'
413 assert err in response.body
413 assert err in response.body
414
414
415 def test_nodetree_missing_xhr(self, backend):
415 def test_nodetree_missing_xhr(self, backend):
416 self.app.get(
416 self.app.get(
417 route_path('repo_nodetree_full',
417 route_path('repo_nodetree_full',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id='tip', f_path='/'),
419 commit_id='tip', f_path='/'),
420 status=404)
420 status=404)
421
421
422
422
423 @pytest.mark.usefixtures("app", "autologin_user")
423 @pytest.mark.usefixtures("app", "autologin_user")
424 class TestRawFileHandling(object):
424 class TestRawFileHandling(object):
425
425
426 def test_download_file(self, backend):
426 def test_download_file(self, backend):
427 commit = backend.repo.get_commit(commit_idx=173)
427 commit = backend.repo.get_commit(commit_idx=173)
428 response = self.app.get(
428 response = self.app.get(
429 route_path('repo_file_download',
429 route_path('repo_file_download',
430 repo_name=backend.repo_name,
430 repo_name=backend.repo_name,
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432
432
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 assert response.content_type == "text/x-python"
434 assert response.content_type == "text/x-python"
435
435
436 def test_download_file_wrong_cs(self, backend):
436 def test_download_file_wrong_cs(self, backend):
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438
438
439 response = self.app.get(
439 response = self.app.get(
440 route_path('repo_file_download',
440 route_path('repo_file_download',
441 repo_name=backend.repo_name,
441 repo_name=backend.repo_name,
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 status=404)
443 status=404)
444
444
445 msg = """No such commit exists for this repository"""
445 msg = """No such commit exists for this repository"""
446 response.mustcontain(msg)
446 response.mustcontain(msg)
447
447
448 def test_download_file_wrong_f_path(self, backend):
448 def test_download_file_wrong_f_path(self, backend):
449 commit = backend.repo.get_commit(commit_idx=173)
449 commit = backend.repo.get_commit(commit_idx=173)
450 f_path = 'vcs/ERRORnodes.py'
450 f_path = 'vcs/ERRORnodes.py'
451
451
452 response = self.app.get(
452 response = self.app.get(
453 route_path('repo_file_download',
453 route_path('repo_file_download',
454 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
455 commit_id=commit.raw_id, f_path=f_path),
455 commit_id=commit.raw_id, f_path=f_path),
456 status=404)
456 status=404)
457
457
458 msg = (
458 msg = (
459 "There is no file nor directory at the given path: "
459 "There is no file nor directory at the given path: "
460 "`%s` at commit %s" % (f_path, commit.short_id))
460 "`%s` at commit %s" % (f_path, commit.short_id))
461 response.mustcontain(msg)
461 response.mustcontain(msg)
462
462
463 def test_file_raw(self, backend):
463 def test_file_raw(self, backend):
464 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
465 response = self.app.get(
465 response = self.app.get(
466 route_path('repo_file_raw',
466 route_path('repo_file_raw',
467 repo_name=backend.repo_name,
467 repo_name=backend.repo_name,
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469
469
470 assert response.content_type == "text/plain"
470 assert response.content_type == "text/plain"
471
471
472 def test_file_raw_binary(self, backend):
472 def test_file_raw_binary(self, backend):
473 commit = backend.repo.get_commit()
473 commit = backend.repo.get_commit()
474 response = self.app.get(
474 response = self.app.get(
475 route_path('repo_file_raw',
475 route_path('repo_file_raw',
476 repo_name=backend.repo_name,
476 repo_name=backend.repo_name,
477 commit_id=commit.raw_id,
477 commit_id=commit.raw_id,
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479
479
480 assert response.content_disposition == 'inline'
480 assert response.content_disposition == 'inline'
481
481
482 def test_raw_file_wrong_cs(self, backend):
482 def test_raw_file_wrong_cs(self, backend):
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484
484
485 response = self.app.get(
485 response = self.app.get(
486 route_path('repo_file_raw',
486 route_path('repo_file_raw',
487 repo_name=backend.repo_name,
487 repo_name=backend.repo_name,
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 status=404)
489 status=404)
490
490
491 msg = """No such commit exists for this repository"""
491 msg = """No such commit exists for this repository"""
492 response.mustcontain(msg)
492 response.mustcontain(msg)
493
493
494 def test_raw_wrong_f_path(self, backend):
494 def test_raw_wrong_f_path(self, backend):
495 commit = backend.repo.get_commit(commit_idx=173)
495 commit = backend.repo.get_commit(commit_idx=173)
496 f_path = 'vcs/ERRORnodes.py'
496 f_path = 'vcs/ERRORnodes.py'
497 response = self.app.get(
497 response = self.app.get(
498 route_path('repo_file_raw',
498 route_path('repo_file_raw',
499 repo_name=backend.repo_name,
499 repo_name=backend.repo_name,
500 commit_id=commit.raw_id, f_path=f_path),
500 commit_id=commit.raw_id, f_path=f_path),
501 status=404)
501 status=404)
502
502
503 msg = (
503 msg = (
504 "There is no file nor directory at the given path: "
504 "There is no file nor directory at the given path: "
505 "`%s` at commit %s" % (f_path, commit.short_id))
505 "`%s` at commit %s" % (f_path, commit.short_id))
506 response.mustcontain(msg)
506 response.mustcontain(msg)
507
507
508 def test_raw_svg_should_not_be_rendered(self, backend):
508 def test_raw_svg_should_not_be_rendered(self, backend):
509 backend.create_repo()
509 backend.create_repo()
510 backend.ensure_file("xss.svg")
510 backend.ensure_file("xss.svg")
511 response = self.app.get(
511 response = self.app.get(
512 route_path('repo_file_raw',
512 route_path('repo_file_raw',
513 repo_name=backend.repo_name,
513 repo_name=backend.repo_name,
514 commit_id='tip', f_path='xss.svg'),)
514 commit_id='tip', f_path='xss.svg'),)
515 # If the content type is image/svg+xml then it allows to render HTML
515 # If the content type is image/svg+xml then it allows to render HTML
516 # and malicious SVG.
516 # and malicious SVG.
517 assert response.content_type == "text/plain"
517 assert response.content_type == "text/plain"
518
518
519
519
520 @pytest.mark.usefixtures("app")
520 @pytest.mark.usefixtures("app")
521 class TestRepositoryArchival(object):
521 class TestRepositoryArchival(object):
522
522
523 def test_archival(self, backend):
523 def test_archival(self, backend):
524 backend.enable_downloads()
524 backend.enable_downloads()
525 commit = backend.repo.get_commit(commit_idx=173)
525 commit = backend.repo.get_commit(commit_idx=173)
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527
527
528 short = commit.short_id + extension
528 short = commit.short_id + extension
529 fname = commit.raw_id + extension
529 fname = commit.raw_id + extension
530 filename = '%s-%s' % (backend.repo_name, short)
530 filename = '%s-%s' % (backend.repo_name, short)
531 response = self.app.get(
531 response = self.app.get(
532 route_path('repo_archivefile',
532 route_path('repo_archivefile',
533 repo_name=backend.repo_name,
533 repo_name=backend.repo_name,
534 fname=fname))
534 fname=fname))
535
535
536 assert response.status == '200 OK'
536 assert response.status == '200 OK'
537 headers = [
537 headers = [
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 ('Content-Type', '%s' % content_type),
539 ('Content-Type', '%s' % content_type),
540 ]
540 ]
541
541
542 for header in headers:
542 for header in headers:
543 assert header in response.headers.items()
543 assert header in response.headers.items()
544
544
545 @pytest.mark.parametrize('arch_ext',[
545 @pytest.mark.parametrize('arch_ext',[
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
547 def test_archival_wrong_ext(self, backend, arch_ext):
547 def test_archival_wrong_ext(self, backend, arch_ext):
548 backend.enable_downloads()
548 backend.enable_downloads()
549 commit = backend.repo.get_commit(commit_idx=173)
549 commit = backend.repo.get_commit(commit_idx=173)
550
550
551 fname = commit.raw_id + '.' + arch_ext
551 fname = commit.raw_id + '.' + arch_ext
552
552
553 response = self.app.get(
553 response = self.app.get(
554 route_path('repo_archivefile',
554 route_path('repo_archivefile',
555 repo_name=backend.repo_name,
555 repo_name=backend.repo_name,
556 fname=fname))
556 fname=fname))
557 response.mustcontain(
557 response.mustcontain(
558 'Unknown archive type for: `{}`'.format(fname))
558 'Unknown archive type for: `{}`'.format(fname))
559
559
560 @pytest.mark.parametrize('commit_id', [
560 @pytest.mark.parametrize('commit_id', [
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
562 def test_archival_wrong_commit_id(self, backend, commit_id):
562 def test_archival_wrong_commit_id(self, backend, commit_id):
563 backend.enable_downloads()
563 backend.enable_downloads()
564 fname = '%s.zip' % commit_id
564 fname = '%s.zip' % commit_id
565
565
566 response = self.app.get(
566 response = self.app.get(
567 route_path('repo_archivefile',
567 route_path('repo_archivefile',
568 repo_name=backend.repo_name,
568 repo_name=backend.repo_name,
569 fname=fname))
569 fname=fname))
570 response.mustcontain('Unknown commit_id')
570 response.mustcontain('Unknown commit_id')
571
571
572
572
573 @pytest.mark.usefixtures("app")
573 @pytest.mark.usefixtures("app")
574 class TestFilesDiff(object):
574 class TestFilesDiff(object):
575
575
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
577 def test_file_full_diff(self, backend, diff):
577 def test_file_full_diff(self, backend, diff):
578 commit1 = backend.repo.get_commit(commit_idx=-1)
578 commit1 = backend.repo.get_commit(commit_idx=-1)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
580
580
581 response = self.app.get(
581 response = self.app.get(
582 route_path('repo_files_diff',
582 route_path('repo_files_diff',
583 repo_name=backend.repo_name,
583 repo_name=backend.repo_name,
584 f_path='README'),
584 f_path='README'),
585 params={
585 params={
586 'diff1': commit2.raw_id,
586 'diff1': commit2.raw_id,
587 'diff2': commit1.raw_id,
587 'diff2': commit1.raw_id,
588 'fulldiff': '1',
588 'fulldiff': '1',
589 'diff': diff,
589 'diff': diff,
590 })
590 })
591
591
592 if diff == 'diff':
592 if diff == 'diff':
593 # use redirect since this is OLD view redirecting to compare page
593 # use redirect since this is OLD view redirecting to compare page
594 response = response.follow()
594 response = response.follow()
595
595
596 # It's a symlink to README.rst
596 # It's a symlink to README.rst
597 response.mustcontain('README.rst')
597 response.mustcontain('README.rst')
598 response.mustcontain('No newline at end of file')
598 response.mustcontain('No newline at end of file')
599
599
600 def test_file_binary_diff(self, backend):
600 def test_file_binary_diff(self, backend):
601 commits = [
601 commits = [
602 {'message': 'First commit'},
602 {'message': 'First commit'},
603 {'message': 'Commit with binary',
603 {'message': 'Commit with binary',
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
605 ]
605 ]
606 repo = backend.create_repo(commits=commits)
606 repo = backend.create_repo(commits=commits)
607
607
608 response = self.app.get(
608 response = self.app.get(
609 route_path('repo_files_diff',
609 route_path('repo_files_diff',
610 repo_name=backend.repo_name,
610 repo_name=backend.repo_name,
611 f_path='file.bin'),
611 f_path='file.bin'),
612 params={
612 params={
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
615 'fulldiff': '1',
615 'fulldiff': '1',
616 'diff': 'diff',
616 'diff': 'diff',
617 })
617 })
618 # use redirect since this is OLD view redirecting to compare page
618 # use redirect since this is OLD view redirecting to compare page
619 response = response.follow()
619 response = response.follow()
620 response.mustcontain('Expand 1 commit')
620 response.mustcontain('Collapse 1 commit')
621 file_changes = (1, 0, 0)
621 file_changes = (1, 0, 0)
622
622
623 compare_page = ComparePage(response)
623 compare_page = ComparePage(response)
624 compare_page.contains_change_summary(*file_changes)
624 compare_page.contains_change_summary(*file_changes)
625
625
626 if backend.alias == 'svn':
626 if backend.alias == 'svn':
627 response.mustcontain('new file 10644')
627 response.mustcontain('new file 10644')
628 # TODO(marcink): SVN doesn't yet detect binary changes
628 # TODO(marcink): SVN doesn't yet detect binary changes
629 else:
629 else:
630 response.mustcontain('new file 100644')
630 response.mustcontain('new file 100644')
631 response.mustcontain('binary diff hidden')
631 response.mustcontain('binary diff hidden')
632
632
633 def test_diff_2way(self, backend):
633 def test_diff_2way(self, backend):
634 commit1 = backend.repo.get_commit(commit_idx=-1)
634 commit1 = backend.repo.get_commit(commit_idx=-1)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
636 response = self.app.get(
636 response = self.app.get(
637 route_path('repo_files_diff_2way_redirect',
637 route_path('repo_files_diff_2way_redirect',
638 repo_name=backend.repo_name,
638 repo_name=backend.repo_name,
639 f_path='README'),
639 f_path='README'),
640 params={
640 params={
641 'diff1': commit2.raw_id,
641 'diff1': commit2.raw_id,
642 'diff2': commit1.raw_id,
642 'diff2': commit1.raw_id,
643 })
643 })
644 # use redirect since this is OLD view redirecting to compare page
644 # use redirect since this is OLD view redirecting to compare page
645 response = response.follow()
645 response = response.follow()
646
646
647 # It's a symlink to README.rst
647 # It's a symlink to README.rst
648 response.mustcontain('README.rst')
648 response.mustcontain('README.rst')
649 response.mustcontain('No newline at end of file')
649 response.mustcontain('No newline at end of file')
650
650
651 def test_requires_one_commit_id(self, backend, autologin_user):
651 def test_requires_one_commit_id(self, backend, autologin_user):
652 response = self.app.get(
652 response = self.app.get(
653 route_path('repo_files_diff',
653 route_path('repo_files_diff',
654 repo_name=backend.repo_name,
654 repo_name=backend.repo_name,
655 f_path='README.rst'),
655 f_path='README.rst'),
656 status=400)
656 status=400)
657 response.mustcontain(
657 response.mustcontain(
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
659
659
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
661 repo = vcsbackend.repo
661 repo = vcsbackend.repo
662 response = self.app.get(
662 response = self.app.get(
663 route_path('repo_files_diff',
663 route_path('repo_files_diff',
664 repo_name=repo.name,
664 repo_name=repo.name,
665 f_path='does-not-exist-in-any-commit'),
665 f_path='does-not-exist-in-any-commit'),
666 params={
666 params={
667 'diff1': repo[0].raw_id,
667 'diff1': repo[0].raw_id,
668 'diff2': repo[1].raw_id
668 'diff2': repo[1].raw_id
669 })
669 })
670
670
671 response = response.follow()
671 response = response.follow()
672 response.mustcontain('No files')
672 response.mustcontain('No files')
673
673
674 def test_returns_redirect_if_file_not_changed(self, backend):
674 def test_returns_redirect_if_file_not_changed(self, backend):
675 commit = backend.repo.get_commit(commit_idx=-1)
675 commit = backend.repo.get_commit(commit_idx=-1)
676 response = self.app.get(
676 response = self.app.get(
677 route_path('repo_files_diff_2way_redirect',
677 route_path('repo_files_diff_2way_redirect',
678 repo_name=backend.repo_name,
678 repo_name=backend.repo_name,
679 f_path='README'),
679 f_path='README'),
680 params={
680 params={
681 'diff1': commit.raw_id,
681 'diff1': commit.raw_id,
682 'diff2': commit.raw_id,
682 'diff2': commit.raw_id,
683 })
683 })
684
684
685 response = response.follow()
685 response = response.follow()
686 response.mustcontain('No files')
686 response.mustcontain('No files')
687 response.mustcontain('No commits in this compare')
687 response.mustcontain('No commits in this compare')
688
688
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
690 #TODO: check this case
690 #TODO: check this case
691 return
691 return
692
692
693 repo = backend_svn['svn-simple-layout'].scm_instance()
693 repo = backend_svn['svn-simple-layout'].scm_instance()
694 commit_id_1 = '24'
694 commit_id_1 = '24'
695 commit_id_2 = '26'
695 commit_id_2 = '26'
696
696
697 response = self.app.get(
697 response = self.app.get(
698 route_path('repo_files_diff',
698 route_path('repo_files_diff',
699 repo_name=backend_svn.repo_name,
699 repo_name=backend_svn.repo_name,
700 f_path='trunk/example.py'),
700 f_path='trunk/example.py'),
701 params={
701 params={
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
703 'diff2': commit_id_2,
703 'diff2': commit_id_2,
704 })
704 })
705
705
706 response = response.follow()
706 response = response.follow()
707 response.mustcontain(
707 response.mustcontain(
708 # diff contains this
708 # diff contains this
709 "Will print out a useful message on invocation.")
709 "Will print out a useful message on invocation.")
710
710
711 # Note: Expecting that we indicate the user what's being compared
711 # Note: Expecting that we indicate the user what's being compared
712 response.mustcontain("trunk/example.py")
712 response.mustcontain("trunk/example.py")
713 response.mustcontain("tags/v0.2/example.py")
713 response.mustcontain("tags/v0.2/example.py")
714
714
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
716 #TODO: check this case
716 #TODO: check this case
717 return
717 return
718
718
719 repo = backend_svn['svn-simple-layout'].scm_instance()
719 repo = backend_svn['svn-simple-layout'].scm_instance()
720 commit_id = repo[-1].raw_id
720 commit_id = repo[-1].raw_id
721
721
722 response = self.app.get(
722 response = self.app.get(
723 route_path('repo_files_diff',
723 route_path('repo_files_diff',
724 repo_name=backend_svn.repo_name,
724 repo_name=backend_svn.repo_name,
725 f_path='trunk/example.py'),
725 f_path='trunk/example.py'),
726 params={
726 params={
727 'diff1': 'branches/argparse/example.py@' + commit_id,
727 'diff1': 'branches/argparse/example.py@' + commit_id,
728 'diff2': commit_id,
728 'diff2': commit_id,
729 },
729 },
730 status=302)
730 status=302)
731 response = response.follow()
731 response = response.follow()
732 assert response.headers['Location'].endswith(
732 assert response.headers['Location'].endswith(
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
734
734
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
736 #TODO: check this case
736 #TODO: check this case
737 return
737 return
738
738
739 repo = backend_svn['svn-simple-layout'].scm_instance()
739 repo = backend_svn['svn-simple-layout'].scm_instance()
740 commit_id = repo[-1].raw_id
740 commit_id = repo[-1].raw_id
741 response = self.app.get(
741 response = self.app.get(
742 route_path('repo_files_diff',
742 route_path('repo_files_diff',
743 repo_name=backend_svn.repo_name,
743 repo_name=backend_svn.repo_name,
744 f_path='trunk/example.py'),
744 f_path='trunk/example.py'),
745 params={
745 params={
746 'diff1': 'branches/argparse/example.py@' + commit_id,
746 'diff1': 'branches/argparse/example.py@' + commit_id,
747 'diff2': commit_id,
747 'diff2': commit_id,
748 'show_rev': 'Show at Revision',
748 'show_rev': 'Show at Revision',
749 'annotate': 'true',
749 'annotate': 'true',
750 },
750 },
751 status=302)
751 status=302)
752 response = response.follow()
752 response = response.follow()
753 assert response.headers['Location'].endswith(
753 assert response.headers['Location'].endswith(
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
755
755
756
756
757 @pytest.mark.usefixtures("app", "autologin_user")
757 @pytest.mark.usefixtures("app", "autologin_user")
758 class TestModifyFilesWithWebInterface(object):
758 class TestModifyFilesWithWebInterface(object):
759
759
760 def test_add_file_view(self, backend):
760 def test_add_file_view(self, backend):
761 self.app.get(
761 self.app.get(
762 route_path('repo_files_add_file',
762 route_path('repo_files_add_file',
763 repo_name=backend.repo_name,
763 repo_name=backend.repo_name,
764 commit_id='tip', f_path='/')
764 commit_id='tip', f_path='/')
765 )
765 )
766
766
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
769 backend.create_repo()
769 backend.create_repo()
770 filename = 'init.py'
770 filename = 'init.py'
771 response = self.app.post(
771 response = self.app.post(
772 route_path('repo_files_create_file',
772 route_path('repo_files_create_file',
773 repo_name=backend.repo_name,
773 repo_name=backend.repo_name,
774 commit_id='tip', f_path='/'),
774 commit_id='tip', f_path='/'),
775 params={
775 params={
776 'content': "",
776 'content': "",
777 'filename': filename,
777 'filename': filename,
778 'csrf_token': csrf_token,
778 'csrf_token': csrf_token,
779 },
779 },
780 status=302)
780 status=302)
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
782 assert_session_flash(response, expected_msg)
782 assert_session_flash(response, expected_msg)
783
783
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
785 commit_id = backend.repo.get_commit().raw_id
785 commit_id = backend.repo.get_commit().raw_id
786 response = self.app.post(
786 response = self.app.post(
787 route_path('repo_files_create_file',
787 route_path('repo_files_create_file',
788 repo_name=backend.repo_name,
788 repo_name=backend.repo_name,
789 commit_id=commit_id, f_path='/'),
789 commit_id=commit_id, f_path='/'),
790 params={
790 params={
791 'content': "foo",
791 'content': "foo",
792 'csrf_token': csrf_token,
792 'csrf_token': csrf_token,
793 },
793 },
794 status=302)
794 status=302)
795
795
796 assert_session_flash(response, 'No filename specified')
796 assert_session_flash(response, 'No filename specified')
797
797
798 def test_add_file_into_repo_errors_and_no_commits(
798 def test_add_file_into_repo_errors_and_no_commits(
799 self, backend, csrf_token):
799 self, backend, csrf_token):
800 repo = backend.create_repo()
800 repo = backend.create_repo()
801 # Create a file with no filename, it will display an error but
801 # Create a file with no filename, it will display an error but
802 # the repo has no commits yet
802 # the repo has no commits yet
803 response = self.app.post(
803 response = self.app.post(
804 route_path('repo_files_create_file',
804 route_path('repo_files_create_file',
805 repo_name=repo.repo_name,
805 repo_name=repo.repo_name,
806 commit_id='tip', f_path='/'),
806 commit_id='tip', f_path='/'),
807 params={
807 params={
808 'content': "foo",
808 'content': "foo",
809 'csrf_token': csrf_token,
809 'csrf_token': csrf_token,
810 },
810 },
811 status=302)
811 status=302)
812
812
813 assert_session_flash(response, 'No filename specified')
813 assert_session_flash(response, 'No filename specified')
814
814
815 # Not allowed, redirect to the summary
815 # Not allowed, redirect to the summary
816 redirected = response.follow()
816 redirected = response.follow()
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
818
818
819 # As there are no commits, displays the summary page with the error of
819 # As there are no commits, displays the summary page with the error of
820 # creating a file with no filename
820 # creating a file with no filename
821
821
822 assert redirected.request.path == summary_url
822 assert redirected.request.path == summary_url
823
823
824 @pytest.mark.parametrize("filename, clean_filename", [
824 @pytest.mark.parametrize("filename, clean_filename", [
825 ('/abs/foo', 'abs/foo'),
825 ('/abs/foo', 'abs/foo'),
826 ('../rel/foo', 'rel/foo'),
826 ('../rel/foo', 'rel/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
828 ])
828 ])
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
830 repo = backend.create_repo()
830 repo = backend.create_repo()
831 commit_id = repo.get_commit().raw_id
831 commit_id = repo.get_commit().raw_id
832
832
833 response = self.app.post(
833 response = self.app.post(
834 route_path('repo_files_create_file',
834 route_path('repo_files_create_file',
835 repo_name=repo.repo_name,
835 repo_name=repo.repo_name,
836 commit_id=commit_id, f_path='/'),
836 commit_id=commit_id, f_path='/'),
837 params={
837 params={
838 'content': "foo",
838 'content': "foo",
839 'filename': filename,
839 'filename': filename,
840 'csrf_token': csrf_token,
840 'csrf_token': csrf_token,
841 },
841 },
842 status=302)
842 status=302)
843
843
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
845 assert_session_flash(response, expected_msg)
845 assert_session_flash(response, expected_msg)
846
846
847 @pytest.mark.parametrize("cnt, filename, content", [
847 @pytest.mark.parametrize("cnt, filename, content", [
848 (1, 'foo.txt', "Content"),
848 (1, 'foo.txt', "Content"),
849 (2, 'dir/foo.rst', "Content"),
849 (2, 'dir/foo.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
852 ])
852 ])
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
854 repo = backend.create_repo()
854 repo = backend.create_repo()
855 commit_id = repo.get_commit().raw_id
855 commit_id = repo.get_commit().raw_id
856 response = self.app.post(
856 response = self.app.post(
857 route_path('repo_files_create_file',
857 route_path('repo_files_create_file',
858 repo_name=repo.repo_name,
858 repo_name=repo.repo_name,
859 commit_id=commit_id, f_path='/'),
859 commit_id=commit_id, f_path='/'),
860 params={
860 params={
861 'content': content,
861 'content': content,
862 'filename': filename,
862 'filename': filename,
863 'csrf_token': csrf_token,
863 'csrf_token': csrf_token,
864 },
864 },
865 status=302)
865 status=302)
866
866
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
868 assert_session_flash(response, expected_msg)
868 assert_session_flash(response, expected_msg)
869
869
870 def test_edit_file_view(self, backend):
870 def test_edit_file_view(self, backend):
871 response = self.app.get(
871 response = self.app.get(
872 route_path('repo_files_edit_file',
872 route_path('repo_files_edit_file',
873 repo_name=backend.repo_name,
873 repo_name=backend.repo_name,
874 commit_id=backend.default_head_id,
874 commit_id=backend.default_head_id,
875 f_path='vcs/nodes.py'),
875 f_path='vcs/nodes.py'),
876 status=200)
876 status=200)
877 response.mustcontain("Module holding everything related to vcs nodes.")
877 response.mustcontain("Module holding everything related to vcs nodes.")
878
878
879 def test_edit_file_view_not_on_branch(self, backend):
879 def test_edit_file_view_not_on_branch(self, backend):
880 repo = backend.create_repo()
880 repo = backend.create_repo()
881 backend.ensure_file("vcs/nodes.py")
881 backend.ensure_file("vcs/nodes.py")
882
882
883 response = self.app.get(
883 response = self.app.get(
884 route_path('repo_files_edit_file',
884 route_path('repo_files_edit_file',
885 repo_name=repo.repo_name,
885 repo_name=repo.repo_name,
886 commit_id='tip',
886 commit_id='tip',
887 f_path='vcs/nodes.py'),
887 f_path='vcs/nodes.py'),
888 status=302)
888 status=302)
889 assert_session_flash(
889 assert_session_flash(
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
891
891
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
893 repo = backend.create_repo()
893 repo = backend.create_repo()
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
895
895
896 response = self.app.post(
896 response = self.app.post(
897 route_path('repo_files_update_file',
897 route_path('repo_files_update_file',
898 repo_name=repo.repo_name,
898 repo_name=repo.repo_name,
899 commit_id=backend.default_head_id,
899 commit_id=backend.default_head_id,
900 f_path='vcs/nodes.py'),
900 f_path='vcs/nodes.py'),
901 params={
901 params={
902 'content': "print 'hello world'",
902 'content': "print 'hello world'",
903 'message': 'I committed',
903 'message': 'I committed',
904 'filename': "vcs/nodes.py",
904 'filename': "vcs/nodes.py",
905 'csrf_token': csrf_token,
905 'csrf_token': csrf_token,
906 },
906 },
907 status=302)
907 status=302)
908 assert_session_flash(
908 assert_session_flash(
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
910 tip = repo.get_commit(commit_idx=-1)
910 tip = repo.get_commit(commit_idx=-1)
911 assert tip.message == 'I committed'
911 assert tip.message == 'I committed'
912
912
913 def test_edit_file_view_commit_changes_default_message(self, backend,
913 def test_edit_file_view_commit_changes_default_message(self, backend,
914 csrf_token):
914 csrf_token):
915 repo = backend.create_repo()
915 repo = backend.create_repo()
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917
917
918 commit_id = (
918 commit_id = (
919 backend.default_branch_name or
919 backend.default_branch_name or
920 backend.repo.scm_instance().commit_ids[-1])
920 backend.repo.scm_instance().commit_ids[-1])
921
921
922 response = self.app.post(
922 response = self.app.post(
923 route_path('repo_files_update_file',
923 route_path('repo_files_update_file',
924 repo_name=repo.repo_name,
924 repo_name=repo.repo_name,
925 commit_id=commit_id,
925 commit_id=commit_id,
926 f_path='vcs/nodes.py'),
926 f_path='vcs/nodes.py'),
927 params={
927 params={
928 'content': "print 'hello world'",
928 'content': "print 'hello world'",
929 'message': '',
929 'message': '',
930 'filename': "vcs/nodes.py",
930 'filename': "vcs/nodes.py",
931 'csrf_token': csrf_token,
931 'csrf_token': csrf_token,
932 },
932 },
933 status=302)
933 status=302)
934 assert_session_flash(
934 assert_session_flash(
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
936 tip = repo.get_commit(commit_idx=-1)
936 tip = repo.get_commit(commit_idx=-1)
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
938
938
939 def test_delete_file_view(self, backend):
939 def test_delete_file_view(self, backend):
940 self.app.get(
940 self.app.get(
941 route_path('repo_files_remove_file',
941 route_path('repo_files_remove_file',
942 repo_name=backend.repo_name,
942 repo_name=backend.repo_name,
943 commit_id=backend.default_head_id,
943 commit_id=backend.default_head_id,
944 f_path='vcs/nodes.py'),
944 f_path='vcs/nodes.py'),
945 status=200)
945 status=200)
946
946
947 def test_delete_file_view_not_on_branch(self, backend):
947 def test_delete_file_view_not_on_branch(self, backend):
948 repo = backend.create_repo()
948 repo = backend.create_repo()
949 backend.ensure_file('vcs/nodes.py')
949 backend.ensure_file('vcs/nodes.py')
950
950
951 response = self.app.get(
951 response = self.app.get(
952 route_path('repo_files_remove_file',
952 route_path('repo_files_remove_file',
953 repo_name=repo.repo_name,
953 repo_name=repo.repo_name,
954 commit_id='tip',
954 commit_id='tip',
955 f_path='vcs/nodes.py'),
955 f_path='vcs/nodes.py'),
956 status=302)
956 status=302)
957 assert_session_flash(
957 assert_session_flash(
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
959
959
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
961 repo = backend.create_repo()
961 repo = backend.create_repo()
962 backend.ensure_file("vcs/nodes.py")
962 backend.ensure_file("vcs/nodes.py")
963
963
964 response = self.app.post(
964 response = self.app.post(
965 route_path('repo_files_delete_file',
965 route_path('repo_files_delete_file',
966 repo_name=repo.repo_name,
966 repo_name=repo.repo_name,
967 commit_id=backend.default_head_id,
967 commit_id=backend.default_head_id,
968 f_path='vcs/nodes.py'),
968 f_path='vcs/nodes.py'),
969 params={
969 params={
970 'message': 'i commited',
970 'message': 'i commited',
971 'csrf_token': csrf_token,
971 'csrf_token': csrf_token,
972 },
972 },
973 status=302)
973 status=302)
974 assert_session_flash(
974 assert_session_flash(
975 response, 'Successfully deleted file `vcs/nodes.py`')
975 response, 'Successfully deleted file `vcs/nodes.py`')
976
976
977
977
978 @pytest.mark.usefixtures("app")
978 @pytest.mark.usefixtures("app")
979 class TestFilesViewOtherCases(object):
979 class TestFilesViewOtherCases(object):
980
980
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
982 self, backend_stub, autologin_regular_user, user_regular,
982 self, backend_stub, autologin_regular_user, user_regular,
983 user_util):
983 user_util):
984
984
985 repo = backend_stub.create_repo()
985 repo = backend_stub.create_repo()
986 user_util.grant_user_permission_to_repo(
986 user_util.grant_user_permission_to_repo(
987 repo, user_regular, 'repository.write')
987 repo, user_regular, 'repository.write')
988 response = self.app.get(
988 response = self.app.get(
989 route_path('repo_files',
989 route_path('repo_files',
990 repo_name=repo.repo_name,
990 repo_name=repo.repo_name,
991 commit_id='tip', f_path='/'))
991 commit_id='tip', f_path='/'))
992
992
993 repo_file_add_url = route_path(
993 repo_file_add_url = route_path(
994 'repo_files_add_file',
994 'repo_files_add_file',
995 repo_name=repo.repo_name,
995 repo_name=repo.repo_name,
996 commit_id=0, f_path='')
996 commit_id=0, f_path='')
997
997
998 assert_session_flash(
998 assert_session_flash(
999 response,
999 response,
1000 'There are no files yet. <a class="alert-link" '
1000 'There are no files yet. <a class="alert-link" '
1001 'href="{}">Click here to add a new file.</a>'
1001 'href="{}">Click here to add a new file.</a>'
1002 .format(repo_file_add_url))
1002 .format(repo_file_add_url))
1003
1003
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1005 self, backend_stub, autologin_regular_user):
1005 self, backend_stub, autologin_regular_user):
1006 repo = backend_stub.create_repo()
1006 repo = backend_stub.create_repo()
1007 # init session for anon user
1007 # init session for anon user
1008 route_path('repo_summary', repo_name=repo.repo_name)
1008 route_path('repo_summary', repo_name=repo.repo_name)
1009
1009
1010 repo_file_add_url = route_path(
1010 repo_file_add_url = route_path(
1011 'repo_files_add_file',
1011 'repo_files_add_file',
1012 repo_name=repo.repo_name,
1012 repo_name=repo.repo_name,
1013 commit_id=0, f_path='')
1013 commit_id=0, f_path='')
1014
1014
1015 response = self.app.get(
1015 response = self.app.get(
1016 route_path('repo_files',
1016 route_path('repo_files',
1017 repo_name=repo.repo_name,
1017 repo_name=repo.repo_name,
1018 commit_id='tip', f_path='/'))
1018 commit_id='tip', f_path='/'))
1019
1019
1020 assert_session_flash(response, no_=repo_file_add_url)
1020 assert_session_flash(response, no_=repo_file_add_url)
1021
1021
1022 @pytest.mark.parametrize('file_node', [
1022 @pytest.mark.parametrize('file_node', [
1023 'archive/file.zip',
1023 'archive/file.zip',
1024 'diff/my-file.txt',
1024 'diff/my-file.txt',
1025 'render.py',
1025 'render.py',
1026 'render',
1026 'render',
1027 'remove_file',
1027 'remove_file',
1028 'remove_file/to-delete.txt',
1028 'remove_file/to-delete.txt',
1029 ])
1029 ])
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1031 backend.create_repo()
1031 backend.create_repo()
1032 backend.ensure_file(file_node)
1032 backend.ensure_file(file_node)
1033
1033
1034 self.app.get(
1034 self.app.get(
1035 route_path('repo_files',
1035 route_path('repo_files',
1036 repo_name=backend.repo_name,
1036 repo_name=backend.repo_name,
1037 commit_id='tip', f_path=file_node),
1037 commit_id='tip', f_path=file_node),
1038 status=200)
1038 status=200)
1039
1039
1040
1040
1041 class TestAdjustFilePathForSvn(object):
1041 class TestAdjustFilePathForSvn(object):
1042 """
1042 """
1043 SVN specific adjustments of node history in RepoFilesView.
1043 SVN specific adjustments of node history in RepoFilesView.
1044 """
1044 """
1045
1045
1046 def test_returns_path_relative_to_matched_reference(self):
1046 def test_returns_path_relative_to_matched_reference(self):
1047 repo = self._repo(branches=['trunk'])
1047 repo = self._repo(branches=['trunk'])
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1049
1049
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1051 repo = self._repo(branches=['trunk'])
1051 repo = self._repo(branches=['trunk'])
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1053
1053
1054 def test_does_not_adjust_partial_directory_names(self):
1054 def test_does_not_adjust_partial_directory_names(self):
1055 repo = self._repo(branches=['trun'])
1055 repo = self._repo(branches=['trun'])
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1057
1057
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1061
1061
1062 def assert_file_adjustment(self, f_path, expected, repo):
1062 def assert_file_adjustment(self, f_path, expected, repo):
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1064 assert result == expected
1064 assert result == expected
1065
1065
1066 def _repo(self, branches=None):
1066 def _repo(self, branches=None):
1067 repo = mock.Mock()
1067 repo = mock.Mock()
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1069 repo.tags = {}
1069 repo.tags = {}
1070 return repo
1070 return repo
@@ -1,119 +1,116 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Commits') % c.repo_name} -
5 ${_('%s Commits') % c.repo_name} -
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
7 ...
7 ...
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()"></%def>
15 <%def name="breadcrumbs_links()"></%def>
16
16
17 <%def name="menu_bar_nav()">
17 <%def name="menu_bar_nav()">
18 ${self.menu_items(active='repositories')}
18 ${self.menu_items(active='repositories')}
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_subnav()">
21 <%def name="menu_bar_subnav()">
22 ${self.repo_menu(active='commits')}
22 ${self.repo_menu(active='commits')}
23 </%def>
23 </%def>
24
24
25 <%def name="main()">
25 <%def name="main()">
26
26
27 <div class="box">
27 <div class="box">
28 <div class="summary changeset">
28 <div class="summary changeset">
29 <div class="summary-detail">
29 <div class="summary-detail">
30 <div class="summary-detail-header">
30 <div class="summary-detail-header">
31 <span class="breadcrumbs files_location">
31 <span class="breadcrumbs files_location">
32 <h4>
32 <h4>
33 ${_('Commit Range')}
33 ${_('Commit Range')}
34 </h4>
34 </h4>
35 </span>
35 </span>
36
36
37 <div class="clear-fix"></div>
37 <div class="clear-fix"></div>
38 </div>
38 </div>
39
39
40 <div class="fieldset">
40 <div class="fieldset">
41 <div class="left-label-summary">
41 <div class="left-label-summary">
42 <p class="spacing">${_('Range')}:</p>
42 <p class="spacing">${_('Range')}:</p>
43 <div class="right-label-summary">
43 <div class="right-label-summary">
44 <div class="code-header" >
44 <div class="code-header" >
45 <div class="compare_header">
45 <div class="compare_header">
46 <code class="fieldset-text-line">
46 <code class="fieldset-text-line">
47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
48 ...
48 ...
49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
51 </code>
51 </code>
52 </div>
52 </div>
53 </div>
53 </div>
54 </div>
54 </div>
55 </div>
55 </div>
56 </div>
56 </div>
57
57
58 <div class="fieldset">
58 <div class="fieldset">
59 <div class="left-label-summary">
59 <div class="left-label-summary">
60 <p class="spacing">${_('Diff Option')}:</p>
60 <p class="spacing">${_('Diff Option')}:</p>
61 <div class="right-label-summary">
61 <div class="right-label-summary">
62 <div class="code-header" >
62 <div class="code-header" >
63 <div class="compare_header">
63 <div class="compare_header">
64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
65 repo_name=c.repo_name,
65 repo_name=c.repo_name,
66 source_ref_type='rev',
66 source_ref_type='rev',
67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
68 target_ref_type='rev',
68 target_ref_type='rev',
69 target_ref=c.commit_ranges[-1].raw_id)}"
69 target_ref=c.commit_ranges[-1].raw_id)}"
70 >
70 >
71 ${_('Show combined diff')}
71 ${_('Show combined diff')}
72 </a>
72 </a>
73 </div>
73 </div>
74 </div>
74 </div>
75 </div>
75 </div>
76 </div>
76 </div>
77 </div>
77 </div>
78
78
79 <div class="clear-fix"></div>
79 <div class="clear-fix"></div>
80 </div> <!-- end summary-detail -->
80 </div> <!-- end summary-detail -->
81 </div> <!-- end summary -->
81 </div> <!-- end summary -->
82
82
83 <div id="changeset_compare_view_content">
83 <div id="changeset_compare_view_content">
84 <div class="pull-left">
84 <div class="pull-left">
85 <div class="btn-group">
85 <div class="btn-group">
86 <a
86 <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)} >
87 class="btn"
87 % if c.collapse_all_commits:
88 href="#"
88 <i class="icon-plus-squared-alt icon-no-margin"></i>
89 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
89 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
90 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
90 % else:
91 </a>
91 <i class="icon-minus-squared-alt icon-no-margin"></i>
92 <a
92 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
93 class="btn"
93 % endif
94 href="#"
95 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
96 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
97 </a>
94 </a>
98 </div>
95 </div>
99 </div>
96 </div>
100 ## Commit range generated below
97 ## Commit range generated below
101 <%include file="../compare/compare_commits.mako"/>
98 <%include file="../compare/compare_commits.mako"/>
102 <div class="cs_files">
99 <div class="cs_files">
103 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
100 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
104 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
101 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
105 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
106
103
107 %for commit in c.commit_ranges:
104 %for commit in c.commit_ranges:
108 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
105 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
109 ${cbdiffs.render_diffset(
106 ${cbdiffs.render_diffset(
110 diffset=c.changes[commit.raw_id],
107 diffset=c.changes[commit.raw_id],
111 collapse_when_files_over=5,
108 collapse_when_files_over=5,
112 commit=commit,
109 commit=commit,
113 )}
110 )}
114 %endfor
111 %endfor
115 </div>
112 </div>
116 </div>
113 </div>
117 </div>
114 </div>
118
115
119 </%def>
116 </%def>
@@ -1,1152 +1,1178 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None,
50 inline_comments=None,
51
51
52 # additional menu for PRs
52 # additional menu for PRs
53 pull_request_menu=None
53 pull_request_menu=None
54
54
55 )">
55 )">
56
56
57 <%
57 <%
58 diffset_container_id = h.md5(diffset.target_ref)
58 diffset_container_id = h.md5(diffset.target_ref)
59 collapse_all = len(diffset.files) > collapse_when_files_over
59 collapse_all = len(diffset.files) > collapse_when_files_over
60 %>
60 %>
61
61
62 %if use_comments:
62 %if use_comments:
63 <div id="cb-comments-inline-container-template" class="js-template">
63 <div id="cb-comments-inline-container-template" class="js-template">
64 ${inline_comments_container([], inline_comments)}
64 ${inline_comments_container([], inline_comments)}
65 </div>
65 </div>
66 <div class="js-template" id="cb-comment-inline-form-template">
66 <div class="js-template" id="cb-comment-inline-form-template">
67 <div class="comment-inline-form ac">
67 <div class="comment-inline-form ac">
68
68
69 %if c.rhodecode_user.username != h.DEFAULT_USER:
69 %if c.rhodecode_user.username != h.DEFAULT_USER:
70 ## render template for inline comments
70 ## render template for inline comments
71 ${commentblock.comment_form(form_type='inline')}
71 ${commentblock.comment_form(form_type='inline')}
72 %else:
72 %else:
73 ${h.form('', class_='inline-form comment-form-login', method='get')}
73 ${h.form('', class_='inline-form comment-form-login', method='get')}
74 <div class="pull-left">
74 <div class="pull-left">
75 <div class="comment-help pull-right">
75 <div class="comment-help pull-right">
76 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
76 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
77 </div>
77 </div>
78 </div>
78 </div>
79 <div class="comment-button pull-right">
79 <div class="comment-button pull-right">
80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 ${_('Cancel')}
81 ${_('Cancel')}
82 </button>
82 </button>
83 </div>
83 </div>
84 <div class="clearfix"></div>
84 <div class="clearfix"></div>
85 ${h.end_form()}
85 ${h.end_form()}
86 %endif
86 %endif
87 </div>
87 </div>
88 </div>
88 </div>
89
89
90 %endif
90 %endif
91
91
92 %if c.user_session_attrs["diffmode"] == 'sideside':
92 %if c.user_session_attrs["diffmode"] == 'sideside':
93 <style>
93 <style>
94 .wrapper {
94 .wrapper {
95 max-width: 1600px !important;
95 max-width: 1600px !important;
96 }
96 }
97 </style>
97 </style>
98 %endif
98 %endif
99
99
100 %if ruler_at_chars:
100 %if ruler_at_chars:
101 <style>
101 <style>
102 .diff table.cb .cb-content:after {
102 .diff table.cb .cb-content:after {
103 content: "";
103 content: "";
104 border-left: 1px solid blue;
104 border-left: 1px solid blue;
105 position: absolute;
105 position: absolute;
106 top: 0;
106 top: 0;
107 height: 18px;
107 height: 18px;
108 opacity: .2;
108 opacity: .2;
109 z-index: 10;
109 z-index: 10;
110 //## +5 to account for diff action (+/-)
110 //## +5 to account for diff action (+/-)
111 left: ${ruler_at_chars + 5}ch;
111 left: ${ruler_at_chars + 5}ch;
112 </style>
112 </style>
113 %endif
113 %endif
114
114
115 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
115 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
116
116
117 <div style="height: 20px; line-height: 20px">
117 <div style="height: 20px; line-height: 20px">
118 ## expand/collapse action
118 ## expand/collapse action
119 <div class="pull-left">
119 <div class="pull-left">
120 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
120 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
121 % if collapse_all:
121 % if collapse_all:
122 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
122 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
123 % else:
123 % else:
124 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
124 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
125 % endif
125 % endif
126 </a>
126 </a>
127
127
128 </div>
128 </div>
129
129
130 ## todos
130 ## todos
131 % if getattr(c, 'at_version', None):
131 % if getattr(c, 'at_version', None):
132 <div class="pull-right">
132 <div class="pull-right">
133 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
133 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
134 ${_('not available in this view')}
134 ${_('not available in this view')}
135 </div>
135 </div>
136 % else:
136 % else:
137 <div class="pull-right">
137 <div class="pull-right">
138 <div class="comments-number" style="padding-left: 10px">
138 <div class="comments-number" style="padding-left: 10px">
139 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
139 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
140 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
140 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
141 % if c.unresolved_comments:
141 % if c.unresolved_comments:
142 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
142 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
143 ${_('{} unresolved').format(len(c.unresolved_comments))}
143 ${_('{} unresolved').format(len(c.unresolved_comments))}
144 </a>
144 </a>
145 % else:
145 % else:
146 ${_('0 unresolved')}
146 ${_('0 unresolved')}
147 % endif
147 % endif
148
148
149 ${_('{} Resolved').format(len(c.resolved_comments))}
149 ${_('{} Resolved').format(len(c.resolved_comments))}
150 % endif
150 % endif
151 </div>
151 </div>
152 </div>
152 </div>
153 % endif
153 % endif
154
154
155 ## comments
155 ## comments
156 <div class="pull-right">
156 <div class="pull-right">
157 <div class="comments-number" style="padding-left: 10px">
157 <div class="comments-number" style="padding-left: 10px">
158 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
158 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
159 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
159 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
160 % if c.comments:
160 % if c.comments:
161 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
161 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
162 % else:
162 % else:
163 ${_('0 General')}
163 ${_('0 General')}
164 % endif
164 % endif
165
165
166 % if c.inline_cnt:
166 % if c.inline_cnt:
167 <a href="#" onclick="return Rhodecode.comments.nextComment();"
167 <a href="#" onclick="return Rhodecode.comments.nextComment();"
168 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
168 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
169 </a>
169 </a>
170 % else:
170 % else:
171 ${_('0 Inline')}
171 ${_('0 Inline')}
172 % endif
172 % endif
173 % endif
173 % endif
174
174
175 % if pull_request_menu:
175 % if pull_request_menu:
176 <%
176 <%
177 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
177 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
178 %>
178 %>
179
179
180 % if outdated_comm_count_ver:
180 % if outdated_comm_count_ver:
181 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
181 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
182 (${_("{} Outdated").format(outdated_comm_count_ver)})
182 (${_("{} Outdated").format(outdated_comm_count_ver)})
183 </a>
183 </a>
184 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
184 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
185 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
185 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
186 % else:
186 % else:
187 (${_("{} Outdated").format(outdated_comm_count_ver)})
187 (${_("{} Outdated").format(outdated_comm_count_ver)})
188 % endif
188 % endif
189
189
190 % endif
190 % endif
191
191
192 </div>
192 </div>
193 </div>
193 </div>
194
194
195 </div>
195 </div>
196
196
197 % if diffset.limited_diff:
197 % if diffset.limited_diff:
198 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
198 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
199 <h2 class="clearinner">
199 <h2 class="clearinner">
200 ${_('The requested changes are too big and content was truncated.')}
200 ${_('The requested changes are too big and content was truncated.')}
201 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
201 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
202 </h2>
202 </h2>
203 </div>
203 </div>
204 ## commit range header for each individual diff
204 ## commit range header for each individual diff
205 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
205 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
206 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
206 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
207 <div class="clearinner">
207 <div class="clearinner">
208 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
208 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
209 </div>
209 </div>
210 </div>
210 </div>
211 % endif
211 % endif
212
212
213 <div id="todo-box">
213 <div id="todo-box">
214 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
214 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
215 % for co in c.unresolved_comments:
215 % for co in c.unresolved_comments:
216 <a class="permalink" href="#comment-${co.comment_id}"
216 <a class="permalink" href="#comment-${co.comment_id}"
217 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
217 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
218 <i class="icon-flag-filled-red"></i>
218 <i class="icon-flag-filled-red"></i>
219 ${co.comment_id}</a>${('' if loop.last else ',')}
219 ${co.comment_id}</a>${('' if loop.last else ',')}
220 % endfor
220 % endfor
221 % endif
221 % endif
222 </div>
222 </div>
223 %if diffset.has_hidden_changes:
223 %if diffset.has_hidden_changes:
224 <p class="empty_data">${_('Some changes may be hidden')}</p>
224 <p class="empty_data">${_('Some changes may be hidden')}</p>
225 %elif not diffset.files:
225 %elif not diffset.files:
226 <p class="empty_data">${_('No files')}</p>
226 <p class="empty_data">${_('No files')}</p>
227 %endif
227 %endif
228
228
229 <div class="filediffs">
229 <div class="filediffs">
230
230
231 ## initial value could be marked as False later on
231 ## initial value could be marked as False later on
232 <% over_lines_changed_limit = False %>
232 <% over_lines_changed_limit = False %>
233 %for i, filediff in enumerate(diffset.files):
233 %for i, filediff in enumerate(diffset.files):
234
234
235 <%
235 <%
236 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
236 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
237 over_lines_changed_limit = lines_changed > lines_changed_limit
237 over_lines_changed_limit = lines_changed > lines_changed_limit
238 %>
238 %>
239 ## anchor with support of sticky header
239 ## anchor with support of sticky header
240 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
240 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
241
241
242 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
242 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
243 <div
243 <div
244 class="filediff"
244 class="filediff"
245 data-f-path="${filediff.patch['filename']}"
245 data-f-path="${filediff.patch['filename']}"
246 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
246 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
247 >
247 >
248 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
248 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
249 <div class="filediff-collapse-indicator icon-"></div>
249 <div class="filediff-collapse-indicator icon-"></div>
250 ${diff_ops(filediff)}
250 ${diff_ops(filediff)}
251 </label>
251 </label>
252
252
253 ${diff_menu(filediff, use_comments=use_comments)}
253 ${diff_menu(filediff, use_comments=use_comments)}
254 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
254 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
255
255
256 ## new/deleted/empty content case
256 ## new/deleted/empty content case
257 % if not filediff.hunks:
257 % if not filediff.hunks:
258 ## Comment container, on "fakes" hunk that contains all data to render comments
258 ## Comment container, on "fakes" hunk that contains all data to render comments
259 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
259 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
260 % endif
260 % endif
261
261
262 %if filediff.limited_diff:
262 %if filediff.limited_diff:
263 <tr class="cb-warning cb-collapser">
263 <tr class="cb-warning cb-collapser">
264 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
264 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
265 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
265 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
266 </td>
266 </td>
267 </tr>
267 </tr>
268 %else:
268 %else:
269 %if over_lines_changed_limit:
269 %if over_lines_changed_limit:
270 <tr class="cb-warning cb-collapser">
270 <tr class="cb-warning cb-collapser">
271 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
271 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
272 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
272 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
273 <a href="#" class="cb-expand"
273 <a href="#" class="cb-expand"
274 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
274 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
275 </a>
275 </a>
276 <a href="#" class="cb-collapse"
276 <a href="#" class="cb-collapse"
277 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
277 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
278 </a>
278 </a>
279 </td>
279 </td>
280 </tr>
280 </tr>
281 %endif
281 %endif
282 %endif
282 %endif
283
283
284 % for hunk in filediff.hunks:
284 % for hunk in filediff.hunks:
285 <tr class="cb-hunk">
285 <tr class="cb-hunk">
286 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
286 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
287 ## TODO: dan: add ajax loading of more context here
287 ## TODO: dan: add ajax loading of more context here
288 ## <a href="#">
288 ## <a href="#">
289 <i class="icon-more"></i>
289 <i class="icon-more"></i>
290 ## </a>
290 ## </a>
291 </td>
291 </td>
292 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
292 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
293 @@
293 @@
294 -${hunk.source_start},${hunk.source_length}
294 -${hunk.source_start},${hunk.source_length}
295 +${hunk.target_start},${hunk.target_length}
295 +${hunk.target_start},${hunk.target_length}
296 ${hunk.section_header}
296 ${hunk.section_header}
297 </td>
297 </td>
298 </tr>
298 </tr>
299 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
299 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
300 % endfor
300 % endfor
301
301
302 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
302 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
303
303
304 ## outdated comments that do not fit into currently displayed lines
304 ## outdated comments that do not fit into currently displayed lines
305 % for lineno, comments in unmatched_comments.items():
305 % for lineno, comments in unmatched_comments.items():
306
306
307 %if c.user_session_attrs["diffmode"] == 'unified':
307 %if c.user_session_attrs["diffmode"] == 'unified':
308 % if loop.index == 0:
308 % if loop.index == 0:
309 <tr class="cb-hunk">
309 <tr class="cb-hunk">
310 <td colspan="3"></td>
310 <td colspan="3"></td>
311 <td>
311 <td>
312 <div>
312 <div>
313 ${_('Unmatched inline comments below')}
313 ${_('Unmatched inline comments below')}
314 </div>
314 </div>
315 </td>
315 </td>
316 </tr>
316 </tr>
317 % endif
317 % endif
318 <tr class="cb-line">
318 <tr class="cb-line">
319 <td class="cb-data cb-context"></td>
319 <td class="cb-data cb-context"></td>
320 <td class="cb-lineno cb-context"></td>
320 <td class="cb-lineno cb-context"></td>
321 <td class="cb-lineno cb-context"></td>
321 <td class="cb-lineno cb-context"></td>
322 <td class="cb-content cb-context">
322 <td class="cb-content cb-context">
323 ${inline_comments_container(comments, inline_comments)}
323 ${inline_comments_container(comments, inline_comments)}
324 </td>
324 </td>
325 </tr>
325 </tr>
326 %elif c.user_session_attrs["diffmode"] == 'sideside':
326 %elif c.user_session_attrs["diffmode"] == 'sideside':
327 % if loop.index == 0:
327 % if loop.index == 0:
328 <tr class="cb-comment-info">
328 <tr class="cb-comment-info">
329 <td colspan="2"></td>
329 <td colspan="2"></td>
330 <td class="cb-line">
330 <td class="cb-line">
331 <div>
331 <div>
332 ${_('Unmatched inline comments below')}
332 ${_('Unmatched inline comments below')}
333 </div>
333 </div>
334 </td>
334 </td>
335 <td colspan="2"></td>
335 <td colspan="2"></td>
336 <td class="cb-line">
336 <td class="cb-line">
337 <div>
337 <div>
338 ${_('Unmatched comments below')}
338 ${_('Unmatched comments below')}
339 </div>
339 </div>
340 </td>
340 </td>
341 </tr>
341 </tr>
342 % endif
342 % endif
343 <tr class="cb-line">
343 <tr class="cb-line">
344 <td class="cb-data cb-context"></td>
344 <td class="cb-data cb-context"></td>
345 <td class="cb-lineno cb-context"></td>
345 <td class="cb-lineno cb-context"></td>
346 <td class="cb-content cb-context">
346 <td class="cb-content cb-context">
347 % if lineno.startswith('o'):
347 % if lineno.startswith('o'):
348 ${inline_comments_container(comments, inline_comments)}
348 ${inline_comments_container(comments, inline_comments)}
349 % endif
349 % endif
350 </td>
350 </td>
351
351
352 <td class="cb-data cb-context"></td>
352 <td class="cb-data cb-context"></td>
353 <td class="cb-lineno cb-context"></td>
353 <td class="cb-lineno cb-context"></td>
354 <td class="cb-content cb-context">
354 <td class="cb-content cb-context">
355 % if lineno.startswith('n'):
355 % if lineno.startswith('n'):
356 ${inline_comments_container(comments, inline_comments)}
356 ${inline_comments_container(comments, inline_comments)}
357 % endif
357 % endif
358 </td>
358 </td>
359 </tr>
359 </tr>
360 %endif
360 %endif
361
361
362 % endfor
362 % endfor
363
363
364 </table>
364 </table>
365 </div>
365 </div>
366 %endfor
366 %endfor
367
367
368 ## outdated comments that are made for a file that has been deleted
368 ## outdated comments that are made for a file that has been deleted
369 % for filename, comments_dict in (deleted_files_comments or {}).items():
369 % for filename, comments_dict in (deleted_files_comments or {}).items():
370
370
371 <%
371 <%
372 display_state = 'display: none'
372 display_state = 'display: none'
373 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
373 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
374 if open_comments_in_file:
374 if open_comments_in_file:
375 display_state = ''
375 display_state = ''
376 fid = str(id(filename))
376 fid = str(id(filename))
377 %>
377 %>
378 <div class="filediffs filediff-outdated" style="${display_state}">
378 <div class="filediffs filediff-outdated" style="${display_state}">
379 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
379 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
380 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
380 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
381 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
381 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
382 <div class="filediff-collapse-indicator icon-"></div>
382 <div class="filediff-collapse-indicator icon-"></div>
383
383
384 <span class="pill">
384 <span class="pill">
385 ## file was deleted
385 ## file was deleted
386 ${filename}
386 ${filename}
387 </span>
387 </span>
388 <span class="pill-group pull-left" >
388 <span class="pill-group pull-left" >
389 ## file op, doesn't need translation
389 ## file op, doesn't need translation
390 <span class="pill" op="removed">removed in this version</span>
390 <span class="pill" op="removed">removed in this version</span>
391 </span>
391 </span>
392 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
392 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
393 <span class="pill-group pull-right">
393 <span class="pill-group pull-right">
394 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
394 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
395 </span>
395 </span>
396 </label>
396 </label>
397
397
398 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
398 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
399 <tr>
399 <tr>
400 % if c.user_session_attrs["diffmode"] == 'unified':
400 % if c.user_session_attrs["diffmode"] == 'unified':
401 <td></td>
401 <td></td>
402 %endif
402 %endif
403
403
404 <td></td>
404 <td></td>
405 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
405 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
406 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
406 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
407 </td>
407 </td>
408 </tr>
408 </tr>
409 %if c.user_session_attrs["diffmode"] == 'unified':
409 %if c.user_session_attrs["diffmode"] == 'unified':
410 <tr class="cb-line">
410 <tr class="cb-line">
411 <td class="cb-data cb-context"></td>
411 <td class="cb-data cb-context"></td>
412 <td class="cb-lineno cb-context"></td>
412 <td class="cb-lineno cb-context"></td>
413 <td class="cb-lineno cb-context"></td>
413 <td class="cb-lineno cb-context"></td>
414 <td class="cb-content cb-context">
414 <td class="cb-content cb-context">
415 ${inline_comments_container(comments_dict['comments'], inline_comments)}
415 ${inline_comments_container(comments_dict['comments'], inline_comments)}
416 </td>
416 </td>
417 </tr>
417 </tr>
418 %elif c.user_session_attrs["diffmode"] == 'sideside':
418 %elif c.user_session_attrs["diffmode"] == 'sideside':
419 <tr class="cb-line">
419 <tr class="cb-line">
420 <td class="cb-data cb-context"></td>
420 <td class="cb-data cb-context"></td>
421 <td class="cb-lineno cb-context"></td>
421 <td class="cb-lineno cb-context"></td>
422 <td class="cb-content cb-context"></td>
422 <td class="cb-content cb-context"></td>
423
423
424 <td class="cb-data cb-context"></td>
424 <td class="cb-data cb-context"></td>
425 <td class="cb-lineno cb-context"></td>
425 <td class="cb-lineno cb-context"></td>
426 <td class="cb-content cb-context">
426 <td class="cb-content cb-context">
427 ${inline_comments_container(comments_dict['comments'], inline_comments)}
427 ${inline_comments_container(comments_dict['comments'], inline_comments)}
428 </td>
428 </td>
429 </tr>
429 </tr>
430 %endif
430 %endif
431 </table>
431 </table>
432 </div>
432 </div>
433 </div>
433 </div>
434 % endfor
434 % endfor
435
435
436 </div>
436 </div>
437 </div>
437 </div>
438 </%def>
438 </%def>
439
439
440 <%def name="diff_ops(filediff)">
440 <%def name="diff_ops(filediff)">
441 <%
441 <%
442 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
442 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
443 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
443 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
444 %>
444 %>
445 <span class="pill">
445 <span class="pill">
446 <i class="icon-file-text"></i>
446 <i class="icon-file-text"></i>
447 %if filediff.source_file_path and filediff.target_file_path:
447 %if filediff.source_file_path and filediff.target_file_path:
448 %if filediff.source_file_path != filediff.target_file_path:
448 %if filediff.source_file_path != filediff.target_file_path:
449 ## file was renamed, or copied
449 ## file was renamed, or copied
450 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
450 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
451 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
451 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
452 <% final_path = filediff.target_file_path %>
452 <% final_path = filediff.target_file_path %>
453 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
453 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
454 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
454 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
455 <% final_path = filediff.target_file_path %>
455 <% final_path = filediff.target_file_path %>
456 %endif
456 %endif
457 %else:
457 %else:
458 ## file was modified
458 ## file was modified
459 ${filediff.source_file_path}
459 ${filediff.source_file_path}
460 <% final_path = filediff.source_file_path %>
460 <% final_path = filediff.source_file_path %>
461 %endif
461 %endif
462 %else:
462 %else:
463 %if filediff.source_file_path:
463 %if filediff.source_file_path:
464 ## file was deleted
464 ## file was deleted
465 ${filediff.source_file_path}
465 ${filediff.source_file_path}
466 <% final_path = filediff.source_file_path %>
466 <% final_path = filediff.source_file_path %>
467 %else:
467 %else:
468 ## file was added
468 ## file was added
469 ${filediff.target_file_path}
469 ${filediff.target_file_path}
470 <% final_path = filediff.target_file_path %>
470 <% final_path = filediff.target_file_path %>
471 %endif
471 %endif
472 %endif
472 %endif
473 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
473 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
474 </span>
474 </span>
475 ## anchor link
475 ## anchor link
476 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
476 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
477
477
478 <span class="pill-group pull-right">
478 <span class="pill-group pull-right">
479
479
480 ## ops pills
480 ## ops pills
481 %if filediff.limited_diff:
481 %if filediff.limited_diff:
482 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
482 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
483 %endif
483 %endif
484
484
485 %if NEW_FILENODE in filediff.patch['stats']['ops']:
485 %if NEW_FILENODE in filediff.patch['stats']['ops']:
486 <span class="pill" op="created">created</span>
486 <span class="pill" op="created">created</span>
487 %if filediff['target_mode'].startswith('120'):
487 %if filediff['target_mode'].startswith('120'):
488 <span class="pill" op="symlink">symlink</span>
488 <span class="pill" op="symlink">symlink</span>
489 %else:
489 %else:
490 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
490 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
491 %endif
491 %endif
492 %endif
492 %endif
493
493
494 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
494 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
495 <span class="pill" op="renamed">renamed</span>
495 <span class="pill" op="renamed">renamed</span>
496 %endif
496 %endif
497
497
498 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
498 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
499 <span class="pill" op="copied">copied</span>
499 <span class="pill" op="copied">copied</span>
500 %endif
500 %endif
501
501
502 %if DEL_FILENODE in filediff.patch['stats']['ops']:
502 %if DEL_FILENODE in filediff.patch['stats']['ops']:
503 <span class="pill" op="removed">removed</span>
503 <span class="pill" op="removed">removed</span>
504 %endif
504 %endif
505
505
506 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
506 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
507 <span class="pill" op="mode">
507 <span class="pill" op="mode">
508 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
508 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
509 </span>
509 </span>
510 %endif
510 %endif
511
511
512 %if BIN_FILENODE in filediff.patch['stats']['ops']:
512 %if BIN_FILENODE in filediff.patch['stats']['ops']:
513 <span class="pill" op="binary">binary</span>
513 <span class="pill" op="binary">binary</span>
514 %if MOD_FILENODE in filediff.patch['stats']['ops']:
514 %if MOD_FILENODE in filediff.patch['stats']['ops']:
515 <span class="pill" op="modified">modified</span>
515 <span class="pill" op="modified">modified</span>
516 %endif
516 %endif
517 %endif
517 %endif
518
518
519 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
519 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
520 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
520 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
521
521
522 </span>
522 </span>
523
523
524 </%def>
524 </%def>
525
525
526 <%def name="nice_mode(filemode)">
526 <%def name="nice_mode(filemode)">
527 ${(filemode.startswith('100') and filemode[3:] or filemode)}
527 ${(filemode.startswith('100') and filemode[3:] or filemode)}
528 </%def>
528 </%def>
529
529
530 <%def name="diff_menu(filediff, use_comments=False)">
530 <%def name="diff_menu(filediff, use_comments=False)">
531 <div class="filediff-menu">
531 <div class="filediff-menu">
532
532
533 %if filediff.diffset.source_ref:
533 %if filediff.diffset.source_ref:
534
534
535 ## FILE BEFORE CHANGES
535 ## FILE BEFORE CHANGES
536 %if filediff.operation in ['D', 'M']:
536 %if filediff.operation in ['D', 'M']:
537 <a
537 <a
538 class="tooltip"
538 class="tooltip"
539 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
539 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
540 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
540 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
541 >
541 >
542 ${_('Show file before')}
542 ${_('Show file before')}
543 </a> |
543 </a> |
544 %else:
544 %else:
545 <span
545 <span
546 class="tooltip"
546 class="tooltip"
547 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
547 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
548 >
548 >
549 ${_('Show file before')}
549 ${_('Show file before')}
550 </span> |
550 </span> |
551 %endif
551 %endif
552
552
553 ## FILE AFTER CHANGES
553 ## FILE AFTER CHANGES
554 %if filediff.operation in ['A', 'M']:
554 %if filediff.operation in ['A', 'M']:
555 <a
555 <a
556 class="tooltip"
556 class="tooltip"
557 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
557 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
558 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
558 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
559 >
559 >
560 ${_('Show file after')}
560 ${_('Show file after')}
561 </a>
561 </a>
562 %else:
562 %else:
563 <span
563 <span
564 class="tooltip"
564 class="tooltip"
565 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
565 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
566 >
566 >
567 ${_('Show file after')}
567 ${_('Show file after')}
568 </span>
568 </span>
569 %endif
569 %endif
570
570
571 % if use_comments:
571 % if use_comments:
572 |
572 |
573 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
573 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
574 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
574 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
575 </a>
575 </a>
576 % endif
576 % endif
577
577
578 %endif
578 %endif
579
579
580 </div>
580 </div>
581 </%def>
581 </%def>
582
582
583
583
584 <%def name="inline_comments_container(comments, inline_comments)">
584 <%def name="inline_comments_container(comments, inline_comments)">
585 <div class="inline-comments">
585 <div class="inline-comments">
586 %for comment in comments:
586 %for comment in comments:
587 ${commentblock.comment_block(comment, inline=True)}
587 ${commentblock.comment_block(comment, inline=True)}
588 %endfor
588 %endfor
589 % if comments and comments[-1].outdated:
589 % if comments and comments[-1].outdated:
590 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
590 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
591 ${_('Add another comment')}
591 ${_('Add another comment')}
592 </span>
592 </span>
593 % else:
593 % else:
594 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
594 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
595 ${_('Add another comment')}
595 ${_('Add another comment')}
596 </span>
596 </span>
597 % endif
597 % endif
598
598
599 </div>
599 </div>
600 </%def>
600 </%def>
601
601
602 <%!
602 <%!
603 def get_comments_for(diff_type, comments, filename, line_version, line_number):
603 def get_comments_for(diff_type, comments, filename, line_version, line_number):
604 if hasattr(filename, 'unicode_path'):
604 if hasattr(filename, 'unicode_path'):
605 filename = filename.unicode_path
605 filename = filename.unicode_path
606
606
607 if not isinstance(filename, (unicode, str)):
607 if not isinstance(filename, (unicode, str)):
608 return None
608 return None
609
609
610 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
610 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
611
611
612 if comments and filename in comments:
612 if comments and filename in comments:
613 file_comments = comments[filename]
613 file_comments = comments[filename]
614 if line_key in file_comments:
614 if line_key in file_comments:
615 data = file_comments.pop(line_key)
615 data = file_comments.pop(line_key)
616 return data
616 return data
617 %>
617 %>
618
618
619 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
619 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
620 %for i, line in enumerate(hunk.sideside):
620 %for i, line in enumerate(hunk.sideside):
621 <%
621 <%
622 old_line_anchor, new_line_anchor = None, None
622 old_line_anchor, new_line_anchor = None, None
623
623
624 if line.original.lineno:
624 if line.original.lineno:
625 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
625 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
626 if line.modified.lineno:
626 if line.modified.lineno:
627 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
627 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
628 %>
628 %>
629
629
630 <tr class="cb-line">
630 <tr class="cb-line">
631 <td class="cb-data ${action_class(line.original.action)}"
631 <td class="cb-data ${action_class(line.original.action)}"
632 data-line-no="${line.original.lineno}"
632 data-line-no="${line.original.lineno}"
633 >
633 >
634 <div>
634 <div>
635
635
636 <% line_old_comments = None %>
636 <% line_old_comments = None %>
637 %if line.original.get_comment_args:
637 %if line.original.get_comment_args:
638 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
638 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
639 %endif
639 %endif
640 %if line_old_comments:
640 %if line_old_comments:
641 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
641 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
642 % if has_outdated:
642 % if has_outdated:
643 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
643 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
644 % else:
644 % else:
645 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
645 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
646 % endif
646 % endif
647 %endif
647 %endif
648 </div>
648 </div>
649 </td>
649 </td>
650 <td class="cb-lineno ${action_class(line.original.action)}"
650 <td class="cb-lineno ${action_class(line.original.action)}"
651 data-line-no="${line.original.lineno}"
651 data-line-no="${line.original.lineno}"
652 %if old_line_anchor:
652 %if old_line_anchor:
653 id="${old_line_anchor}"
653 id="${old_line_anchor}"
654 %endif
654 %endif
655 >
655 >
656 %if line.original.lineno:
656 %if line.original.lineno:
657 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
657 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
658 %endif
658 %endif
659 </td>
659 </td>
660 <td class="cb-content ${action_class(line.original.action)}"
660 <td class="cb-content ${action_class(line.original.action)}"
661 data-line-no="o${line.original.lineno}"
661 data-line-no="o${line.original.lineno}"
662 >
662 >
663 %if use_comments and line.original.lineno:
663 %if use_comments and line.original.lineno:
664 ${render_add_comment_button()}
664 ${render_add_comment_button()}
665 %endif
665 %endif
666 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
666 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
667
667
668 %if use_comments and line.original.lineno and line_old_comments:
668 %if use_comments and line.original.lineno and line_old_comments:
669 ${inline_comments_container(line_old_comments, inline_comments)}
669 ${inline_comments_container(line_old_comments, inline_comments)}
670 %endif
670 %endif
671
671
672 </td>
672 </td>
673 <td class="cb-data ${action_class(line.modified.action)}"
673 <td class="cb-data ${action_class(line.modified.action)}"
674 data-line-no="${line.modified.lineno}"
674 data-line-no="${line.modified.lineno}"
675 >
675 >
676 <div>
676 <div>
677
677
678 %if line.modified.get_comment_args:
678 %if line.modified.get_comment_args:
679 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
679 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
680 %else:
680 %else:
681 <% line_new_comments = None%>
681 <% line_new_comments = None%>
682 %endif
682 %endif
683 %if line_new_comments:
683 %if line_new_comments:
684 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
684 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
685 % if has_outdated:
685 % if has_outdated:
686 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
686 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
687 % else:
687 % else:
688 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
688 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
689 % endif
689 % endif
690 %endif
690 %endif
691 </div>
691 </div>
692 </td>
692 </td>
693 <td class="cb-lineno ${action_class(line.modified.action)}"
693 <td class="cb-lineno ${action_class(line.modified.action)}"
694 data-line-no="${line.modified.lineno}"
694 data-line-no="${line.modified.lineno}"
695 %if new_line_anchor:
695 %if new_line_anchor:
696 id="${new_line_anchor}"
696 id="${new_line_anchor}"
697 %endif
697 %endif
698 >
698 >
699 %if line.modified.lineno:
699 %if line.modified.lineno:
700 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
700 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
701 %endif
701 %endif
702 </td>
702 </td>
703 <td class="cb-content ${action_class(line.modified.action)}"
703 <td class="cb-content ${action_class(line.modified.action)}"
704 data-line-no="n${line.modified.lineno}"
704 data-line-no="n${line.modified.lineno}"
705 >
705 >
706 %if use_comments and line.modified.lineno:
706 %if use_comments and line.modified.lineno:
707 ${render_add_comment_button()}
707 ${render_add_comment_button()}
708 %endif
708 %endif
709 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
709 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
710 %if use_comments and line.modified.lineno and line_new_comments:
710 %if use_comments and line.modified.lineno and line_new_comments:
711 ${inline_comments_container(line_new_comments, inline_comments)}
711 ${inline_comments_container(line_new_comments, inline_comments)}
712 %endif
712 %endif
713 </td>
713 </td>
714 </tr>
714 </tr>
715 %endfor
715 %endfor
716 </%def>
716 </%def>
717
717
718
718
719 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
719 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
720 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
720 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
721
721
722 <%
722 <%
723 old_line_anchor, new_line_anchor = None, None
723 old_line_anchor, new_line_anchor = None, None
724 if old_line_no:
724 if old_line_no:
725 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
725 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
726 if new_line_no:
726 if new_line_no:
727 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
727 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
728 %>
728 %>
729 <tr class="cb-line">
729 <tr class="cb-line">
730 <td class="cb-data ${action_class(action)}">
730 <td class="cb-data ${action_class(action)}">
731 <div>
731 <div>
732
732
733 %if comments_args:
733 %if comments_args:
734 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
734 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
735 %else:
735 %else:
736 <% comments = None %>
736 <% comments = None %>
737 %endif
737 %endif
738
738
739 % if comments:
739 % if comments:
740 <% has_outdated = any([x.outdated for x in comments]) %>
740 <% has_outdated = any([x.outdated for x in comments]) %>
741 % if has_outdated:
741 % if has_outdated:
742 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
742 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
743 % else:
743 % else:
744 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
744 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
745 % endif
745 % endif
746 % endif
746 % endif
747 </div>
747 </div>
748 </td>
748 </td>
749 <td class="cb-lineno ${action_class(action)}"
749 <td class="cb-lineno ${action_class(action)}"
750 data-line-no="${old_line_no}"
750 data-line-no="${old_line_no}"
751 %if old_line_anchor:
751 %if old_line_anchor:
752 id="${old_line_anchor}"
752 id="${old_line_anchor}"
753 %endif
753 %endif
754 >
754 >
755 %if old_line_anchor:
755 %if old_line_anchor:
756 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
756 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
757 %endif
757 %endif
758 </td>
758 </td>
759 <td class="cb-lineno ${action_class(action)}"
759 <td class="cb-lineno ${action_class(action)}"
760 data-line-no="${new_line_no}"
760 data-line-no="${new_line_no}"
761 %if new_line_anchor:
761 %if new_line_anchor:
762 id="${new_line_anchor}"
762 id="${new_line_anchor}"
763 %endif
763 %endif
764 >
764 >
765 %if new_line_anchor:
765 %if new_line_anchor:
766 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
766 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
767 %endif
767 %endif
768 </td>
768 </td>
769 <td class="cb-content ${action_class(action)}"
769 <td class="cb-content ${action_class(action)}"
770 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
770 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
771 >
771 >
772 %if use_comments:
772 %if use_comments:
773 ${render_add_comment_button()}
773 ${render_add_comment_button()}
774 %endif
774 %endif
775 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
775 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
776 %if use_comments and comments:
776 %if use_comments and comments:
777 ${inline_comments_container(comments, inline_comments)}
777 ${inline_comments_container(comments, inline_comments)}
778 %endif
778 %endif
779 </td>
779 </td>
780 </tr>
780 </tr>
781 %endfor
781 %endfor
782 </%def>
782 </%def>
783
783
784
784
785 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
785 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
786 % if diff_mode == 'unified':
786 % if diff_mode == 'unified':
787 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
787 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
788 % elif diff_mode == 'sideside':
788 % elif diff_mode == 'sideside':
789 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
789 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
790 % else:
790 % else:
791 <tr class="cb-line">
791 <tr class="cb-line">
792 <td>unknown diff mode</td>
792 <td>unknown diff mode</td>
793 </tr>
793 </tr>
794 % endif
794 % endif
795 </%def>file changes
795 </%def>file changes
796
796
797
797
798 <%def name="render_add_comment_button()">
798 <%def name="render_add_comment_button()">
799 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
799 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
800 <span><i class="icon-comment"></i></span>
800 <span><i class="icon-comment"></i></span>
801 </button>
801 </button>
802 </%def>
802 </%def>
803
803
804 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
804 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
805 <% diffset_container_id = h.md5(diffset.target_ref) %>
805 <% diffset_container_id = h.md5(diffset.target_ref) %>
806
806
807 <div id="diff-file-sticky" class="diffset-menu clearinner">
807 <div id="diff-file-sticky" class="diffset-menu clearinner">
808 ## auto adjustable
808 ## auto adjustable
809 <div class="sidebar__inner">
809 <div class="sidebar__inner">
810 <div class="sidebar__bar">
810 <div class="sidebar__bar">
811 <div class="pull-right">
811 <div class="pull-right">
812 <div class="btn-group">
812 <div class="btn-group">
813 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
813 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
814 <i class="icon-wide-mode"></i>
814 <i class="icon-wide-mode"></i>
815 </a>
815 </a>
816 </div>
816 </div>
817 <div class="btn-group">
817 <div class="btn-group">
818
818
819 <a
819 <a
820 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
820 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
821 title="${h.tooltip(_('View diff as side by side'))}"
821 title="${h.tooltip(_('View diff as side by side'))}"
822 href="${h.current_route_path(request, diffmode='sideside')}">
822 href="${h.current_route_path(request, diffmode='sideside')}">
823 <span>${_('Side by Side')}</span>
823 <span>${_('Side by Side')}</span>
824 </a>
824 </a>
825
825
826 <a
826 <a
827 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
827 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
828 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
828 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
829 <span>${_('Unified')}</span>
829 <span>${_('Unified')}</span>
830 </a>
830 </a>
831
831
832 % if range_diff_on is True:
832 % if range_diff_on is True:
833 <a
833 <a
834 title="${_('Turn off: Show the diff as commit range')}"
834 title="${_('Turn off: Show the diff as commit range')}"
835 class="btn btn-primary"
835 class="btn btn-primary"
836 href="${h.current_route_path(request, **{"range-diff":"0"})}">
836 href="${h.current_route_path(request, **{"range-diff":"0"})}">
837 <span>${_('Range Diff')}</span>
837 <span>${_('Range Diff')}</span>
838 </a>
838 </a>
839 % elif range_diff_on is False:
839 % elif range_diff_on is False:
840 <a
840 <a
841 title="${_('Show the diff as commit range')}"
841 title="${_('Show the diff as commit range')}"
842 class="btn"
842 class="btn"
843 href="${h.current_route_path(request, **{"range-diff":"1"})}">
843 href="${h.current_route_path(request, **{"range-diff":"1"})}">
844 <span>${_('Range Diff')}</span>
844 <span>${_('Range Diff')}</span>
845 </a>
845 </a>
846 % endif
846 % endif
847 </div>
847 </div>
848 <div class="btn-group">
848 <div class="btn-group">
849
849
850 <div class="pull-left">
850 <div class="pull-left">
851 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
851 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
852 </div>
852 </div>
853
853
854 </div>
854 </div>
855 </div>
855 </div>
856 <div class="pull-left">
856 <div class="pull-left">
857 <div class="btn-group">
857 <div class="btn-group">
858 <div class="pull-left">
858 <div class="pull-left">
859 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
859 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
860 </div>
860 </div>
861
861
862 </div>
862 </div>
863 </div>
863 </div>
864 </div>
864 </div>
865 <div class="fpath-placeholder">
865 <div class="fpath-placeholder">
866 <i class="icon-file-text"></i>
866 <i class="icon-file-text"></i>
867 <strong class="fpath-placeholder-text">
867 <strong class="fpath-placeholder-text">
868 Context file:
868 Context file:
869 </strong>
869 </strong>
870 </div>
870 </div>
871 <div class="sidebar_inner_shadow"></div>
871 <div class="sidebar_inner_shadow"></div>
872 </div>
872 </div>
873 </div>
873 </div>
874
874
875 % if diffset:
875 % if diffset:
876 %if diffset.limited_diff:
876 %if diffset.limited_diff:
877 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
877 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
878 %else:
878 %else:
879 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
879 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
880 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
880 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
881
881
882 %endif
882 %endif
883 ## case on range-diff placeholder needs to be updated
883 ## case on range-diff placeholder needs to be updated
884 % if range_diff_on is True:
884 % if range_diff_on is True:
885 <% file_placeholder = _('Disabled on range diff') %>
885 <% file_placeholder = _('Disabled on range diff') %>
886 % endif
886 % endif
887
887
888 <script type="text/javascript">
888 <script type="text/javascript">
889 var feedFilesOptions = function (query, initialData) {
889 var feedFilesOptions = function (query, initialData) {
890 var data = {results: []};
890 var data = {results: []};
891 var isQuery = typeof query.term !== 'undefined';
891 var isQuery = typeof query.term !== 'undefined';
892
892
893 var section = _gettext('Changed files');
893 var section = _gettext('Changed files');
894 var filteredData = [];
894 var filteredData = [];
895
895
896 //filter results
896 //filter results
897 $.each(initialData.results, function (idx, value) {
897 $.each(initialData.results, function (idx, value) {
898
898
899 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
899 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
900 filteredData.push({
900 filteredData.push({
901 'id': this.id,
901 'id': this.id,
902 'text': this.text,
902 'text': this.text,
903 "ops": this.ops,
903 "ops": this.ops,
904 })
904 })
905 }
905 }
906
906
907 });
907 });
908
908
909 data.results = filteredData;
909 data.results = filteredData;
910
910
911 query.callback(data);
911 query.callback(data);
912 };
912 };
913
913
914 var selectionFormatter = function(data, escapeMarkup) {
914 var selectionFormatter = function(data, escapeMarkup) {
915 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
915 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
916 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
916 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
917 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
917 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
918 '<span class="pill" op="added">{0}</span>' +
918 '<span class="pill" op="added">{0}</span>' +
919 '<span class="pill" op="deleted">{1}</span>' +
919 '<span class="pill" op="deleted">{1}</span>' +
920 '</div>'
920 '</div>'
921 ;
921 ;
922 var added = data['ops']['added'];
922 var added = data['ops']['added'];
923 if (added === 0) {
923 if (added === 0) {
924 // don't show +0
924 // don't show +0
925 added = 0;
925 added = 0;
926 } else {
926 } else {
927 added = '+' + added;
927 added = '+' + added;
928 }
928 }
929
929
930 var deleted = -1*data['ops']['deleted'];
930 var deleted = -1*data['ops']['deleted'];
931
931
932 tmpl += pill.format(added, deleted);
932 tmpl += pill.format(added, deleted);
933 return container.format(tmpl);
933 return container.format(tmpl);
934 };
934 };
935 var formatFileResult = function(result, container, query, escapeMarkup) {
935 var formatFileResult = function(result, container, query, escapeMarkup) {
936 return selectionFormatter(result, escapeMarkup);
936 return selectionFormatter(result, escapeMarkup);
937 };
937 };
938
938
939 var formatSelection = function (data, container) {
939 var formatSelection = function (data, container) {
940 return '${file_placeholder}'
940 return '${file_placeholder}'
941 };
941 };
942
942
943 if (window.preloadFileFilterData === undefined) {
943 if (window.preloadFileFilterData === undefined) {
944 window.preloadFileFilterData = {}
944 window.preloadFileFilterData = {}
945 }
945 }
946
946
947 preloadFileFilterData["${diffset_container_id}"] = {
947 preloadFileFilterData["${diffset_container_id}"] = {
948 results: [
948 results: [
949 % for filediff in diffset.files:
949 % for filediff in diffset.files:
950 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
950 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
951 text:"${filediff.patch['filename']}",
951 text:"${filediff.patch['filename']}",
952 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
952 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
953 % endfor
953 % endfor
954 ]
954 ]
955 };
955 };
956
956
957 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
957 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
958 var diffFileFilter = $(diffFileFilterId).select2({
958 var diffFileFilter = $(diffFileFilterId).select2({
959 'dropdownAutoWidth': true,
959 'dropdownAutoWidth': true,
960 'width': 'auto',
960 'width': 'auto',
961
961
962 containerCssClass: "drop-menu",
962 containerCssClass: "drop-menu",
963 dropdownCssClass: "drop-menu-dropdown",
963 dropdownCssClass: "drop-menu-dropdown",
964 data: preloadFileFilterData["${diffset_container_id}"],
964 data: preloadFileFilterData["${diffset_container_id}"],
965 query: function(query) {
965 query: function(query) {
966 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
966 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
967 },
967 },
968 initSelection: function(element, callback) {
968 initSelection: function(element, callback) {
969 callback({'init': true});
969 callback({'init': true});
970 },
970 },
971 formatResult: formatFileResult,
971 formatResult: formatFileResult,
972 formatSelection: formatSelection
972 formatSelection: formatSelection
973 });
973 });
974
974
975 % if range_diff_on is True:
975 % if range_diff_on is True:
976 diffFileFilter.select2("enable", false);
976 diffFileFilter.select2("enable", false);
977 % endif
977 % endif
978
978
979 $(diffFileFilterId).on('select2-selecting', function (e) {
979 $(diffFileFilterId).on('select2-selecting', function (e) {
980 var idSelector = e.choice.id;
980 var idSelector = e.choice.id;
981
981
982 // expand the container if we quick-select the field
982 // expand the container if we quick-select the field
983 $('#'+idSelector).next().prop('checked', false);
983 $('#'+idSelector).next().prop('checked', false);
984 // hide the mast as we later do preventDefault()
984 // hide the mast as we later do preventDefault()
985 $("#select2-drop-mask").click();
985 $("#select2-drop-mask").click();
986
986
987 window.location.hash = '#'+idSelector;
987 window.location.hash = '#'+idSelector;
988 updateSticky();
988 updateSticky();
989
989
990 e.preventDefault();
990 e.preventDefault();
991 });
991 });
992
992
993 </script>
993 </script>
994 % endif
994 % endif
995
995
996 <script type="text/javascript">
996 <script type="text/javascript">
997 $(document).ready(function () {
997 $(document).ready(function () {
998
998
999 var contextPrefix = _gettext('Context file: ');
999 var contextPrefix = _gettext('Context file: ');
1000 ## sticky sidebar
1000 ## sticky sidebar
1001 var sidebarElement = document.getElementById('diff-file-sticky');
1001 var sidebarElement = document.getElementById('diff-file-sticky');
1002 sidebar = new StickySidebar(sidebarElement, {
1002 sidebar = new StickySidebar(sidebarElement, {
1003 topSpacing: 0,
1003 topSpacing: 0,
1004 bottomSpacing: 0,
1004 bottomSpacing: 0,
1005 innerWrapperSelector: '.sidebar__inner'
1005 innerWrapperSelector: '.sidebar__inner'
1006 });
1006 });
1007 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1007 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1008 // reset our file so it's not holding new value
1008 // reset our file so it's not holding new value
1009 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1009 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1010 });
1010 });
1011
1011
1012 updateSticky = function () {
1012 updateSticky = function () {
1013 sidebar.updateSticky();
1013 sidebar.updateSticky();
1014 Waypoint.refreshAll();
1014 Waypoint.refreshAll();
1015 };
1015 };
1016
1016
1017 var animateText = function (fPath, anchorId) {
1017 var animateText = function (fPath, anchorId) {
1018 fPath = Select2.util.escapeMarkup(fPath);
1018 fPath = Select2.util.escapeMarkup(fPath);
1019 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1019 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1020 };
1020 };
1021
1021
1022 ## dynamic file waypoints
1022 ## dynamic file waypoints
1023 var setFPathInfo = function(fPath, anchorId){
1023 var setFPathInfo = function(fPath, anchorId){
1024 animateText(fPath, anchorId)
1024 animateText(fPath, anchorId)
1025 };
1025 };
1026
1026
1027 var codeBlock = $('.filediff');
1027 var codeBlock = $('.filediff');
1028
1028
1029 // forward waypoint
1029 // forward waypoint
1030 codeBlock.waypoint(
1030 codeBlock.waypoint(
1031 function(direction) {
1031 function(direction) {
1032 if (direction === "down"){
1032 if (direction === "down"){
1033 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1033 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1034 }
1034 }
1035 }, {
1035 }, {
1036 offset: function () {
1036 offset: function () {
1037 return 70;
1037 return 70;
1038 },
1038 },
1039 context: '.fpath-placeholder'
1039 context: '.fpath-placeholder'
1040 }
1040 }
1041 );
1041 );
1042
1042
1043 // backward waypoint
1043 // backward waypoint
1044 codeBlock.waypoint(
1044 codeBlock.waypoint(
1045 function(direction) {
1045 function(direction) {
1046 if (direction === "up"){
1046 if (direction === "up"){
1047 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1047 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1048 }
1048 }
1049 }, {
1049 }, {
1050 offset: function () {
1050 offset: function () {
1051 return -this.element.clientHeight + 90;
1051 return -this.element.clientHeight + 90;
1052 },
1052 },
1053 context: '.fpath-placeholder'
1053 context: '.fpath-placeholder'
1054 }
1054 }
1055 );
1055 );
1056
1056
1057 toggleWideDiff = function (el) {
1057 toggleWideDiff = function (el) {
1058 updateSticky();
1058 updateSticky();
1059 var wide = Rhodecode.comments.toggleWideMode(this);
1059 var wide = Rhodecode.comments.toggleWideMode(this);
1060 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1060 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1061 if (wide === true) {
1061 if (wide === true) {
1062 $(el).addClass('btn-active');
1062 $(el).addClass('btn-active');
1063 } else {
1063 } else {
1064 $(el).removeClass('btn-active');
1064 $(el).removeClass('btn-active');
1065 }
1065 }
1066 return null;
1066 return null;
1067 };
1067 };
1068
1068
1069 var preloadDiffMenuData = {
1069 var preloadDiffMenuData = {
1070 results: [
1070 results: [
1071
1071
1072 ## Whitespace change
1072 ## Whitespace change
1073 % if request.GET.get('ignorews', '') == '1':
1073 % if request.GET.get('ignorews', '') == '1':
1074 {
1074 {
1075 id: 2,
1075 id: 2,
1076 text: _gettext('Show whitespace changes'),
1076 text: _gettext('Show whitespace changes'),
1077 action: function () {},
1077 action: function () {},
1078 url: "${h.current_route_path(request, ignorews=0)|n}"
1078 url: "${h.current_route_path(request, ignorews=0)|n}"
1079 },
1079 },
1080 % else:
1080 % else:
1081 {
1081 {
1082 id: 2,
1082 id: 2,
1083 text: _gettext('Hide whitespace changes'),
1083 text: _gettext('Hide whitespace changes'),
1084 action: function () {},
1084 action: function () {},
1085 url: "${h.current_route_path(request, ignorews=1)|n}"
1085 url: "${h.current_route_path(request, ignorews=1)|n}"
1086 },
1086 },
1087 % endif
1087 % endif
1088
1088
1089 ## FULL CONTEXT
1089 ## FULL CONTEXT
1090 % if request.GET.get('fullcontext', '') == '1':
1090 % if request.GET.get('fullcontext', '') == '1':
1091 {
1091 {
1092 id: 3,
1092 id: 3,
1093 text: _gettext('Hide full context diff'),
1093 text: _gettext('Hide full context diff'),
1094 action: function () {},
1094 action: function () {},
1095 url: "${h.current_route_path(request, fullcontext=0)|n}"
1095 url: "${h.current_route_path(request, fullcontext=0)|n}"
1096 },
1096 },
1097 % else:
1097 % else:
1098 {
1098 {
1099 id: 3,
1099 id: 3,
1100 text: _gettext('Show full context diff'),
1100 text: _gettext('Show full context diff'),
1101 action: function () {},
1101 action: function () {},
1102 url: "${h.current_route_path(request, fullcontext=1)|n}"
1102 url: "${h.current_route_path(request, fullcontext=1)|n}"
1103 },
1103 },
1104 % endif
1104 % endif
1105
1105
1106 ]
1106 ]
1107 };
1107 };
1108
1108
1109 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1109 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1110 $(diffMenuId).select2({
1110 $(diffMenuId).select2({
1111 minimumResultsForSearch: -1,
1111 minimumResultsForSearch: -1,
1112 containerCssClass: "drop-menu-no-width",
1112 containerCssClass: "drop-menu-no-width",
1113 dropdownCssClass: "drop-menu-dropdown",
1113 dropdownCssClass: "drop-menu-dropdown",
1114 dropdownAutoWidth: true,
1114 dropdownAutoWidth: true,
1115 data: preloadDiffMenuData,
1115 data: preloadDiffMenuData,
1116 placeholder: "${_('...')}",
1116 placeholder: "${_('...')}",
1117 });
1117 });
1118 $(diffMenuId).on('select2-selecting', function (e) {
1118 $(diffMenuId).on('select2-selecting', function (e) {
1119 e.choice.action();
1119 e.choice.action();
1120 if (e.choice.url !== null) {
1120 if (e.choice.url !== null) {
1121 window.location = e.choice.url
1121 window.location = e.choice.url
1122 }
1122 }
1123 });
1123 });
1124 toggleExpand = function (el, diffsetEl) {
1124 toggleExpand = function (el, diffsetEl) {
1125 var el = $(el);
1125 var el = $(el);
1126 if (el.hasClass('collapsed')) {
1126 if (el.hasClass('collapsed')) {
1127 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1127 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1128 el.removeClass('collapsed');
1128 el.removeClass('collapsed');
1129 el.html(
1129 el.html(
1130 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1130 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1131 _gettext('Collapse all files'));
1131 _gettext('Collapse all files'));
1132 }
1132 }
1133 else {
1133 else {
1134 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1134 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1135 el.addClass('collapsed');
1135 el.addClass('collapsed');
1136 el.html(
1136 el.html(
1137 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1137 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1138 _gettext('Expand all files'));
1138 _gettext('Expand all files'));
1139 }
1139 }
1140 updateSticky()
1140 updateSticky()
1141 };
1141 };
1142
1142
1143 toggleCommitExpand = function (el) {
1144 var $el = $(el);
1145 var commits = $el.data('toggleCommitsCnt');
1146 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1147 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1148
1149 if ($el.hasClass('collapsed')) {
1150 $('.compare_select').show();
1151 $('.compare_select_hidden').hide();
1152
1153 $el.removeClass('collapsed');
1154 $el.html(
1155 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1156 collapseMsg);
1157 }
1158 else {
1159 $('.compare_select').hide();
1160 $('.compare_select_hidden').show();
1161 $el.addClass('collapsed');
1162 $el.html(
1163 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1164 expandMsg);
1165 }
1166 updateSticky();
1167 };
1168
1143 // get stored diff mode and pre-enable it
1169 // get stored diff mode and pre-enable it
1144 if (templateContext.session_attrs.wide_diff_mode === "true") {
1170 if (templateContext.session_attrs.wide_diff_mode === "true") {
1145 Rhodecode.comments.toggleWideMode(null);
1171 Rhodecode.comments.toggleWideMode(null);
1146 $('.toggle-wide-diff').addClass('btn-active');
1172 $('.toggle-wide-diff').addClass('btn-active');
1147 updateSticky();
1173 updateSticky();
1148 }
1174 }
1149 });
1175 });
1150 </script>
1176 </script>
1151
1177
1152 </%def>
1178 </%def>
@@ -1,82 +1,81 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">
7 ${h.short_id(c.ancestor)}
7 ${h.short_id(c.ancestor)}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
10 </div>
10 </div>
11 %endif
11 %endif
12
12
13 <div class="container">
13 <div class="container">
14 <input type="hidden" name="__start__" value="revisions:sequence">
14 <input type="hidden" name="__start__" value="revisions:sequence">
15 <table class="rctable compare_view_commits">
15 <table class="rctable compare_view_commits">
16 <tr>
16 <tr>
17 <th>${_('Time')}</th>
17 <th>${_('Time')}</th>
18 <th>${_('Author')}</th>
18 <th>${_('Author')}</th>
19 <th>${_('Commit')}</th>
19 <th>${_('Commit')}</th>
20 <th></th>
20 <th></th>
21 <th>${_('Description')}</th>
21 <th>${_('Description')}</th>
22 </tr>
22 </tr>
23 ## to speed up lookups cache some functions before the loop
23 ## to speed up lookups cache some functions before the loop
24 <%
24 <%
25 active_patterns = h.get_active_pattern_entries(c.repo_name)
25 active_patterns = h.get_active_pattern_entries(c.repo_name)
26 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
26 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
27 %>
27 %>
28 %for commit in c.commit_ranges:
28 %for commit in c.commit_ranges:
29 <tr id="row-${commit.raw_id}"
29 <tr id="row-${commit.raw_id}"
30 commit_id="${commit.raw_id}"
30 commit_id="${commit.raw_id}"
31 class="compare_select"
31 class="compare_select"
32 style="${'display: none' if c.collapse_all_commits else ''}"
32 style="${'display: none' if c.collapse_all_commits else ''}"
33 >
33 >
34 <td class="td-time">
34 <td class="td-time">
35 ${h.age_component(commit.date)}
35 ${h.age_component(commit.date)}
36 </td>
36 </td>
37 <td class="td-user">
37 <td class="td-user">
38 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
38 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
39 </td>
39 </td>
40 <td class="td-hash">
40 <td class="td-hash">
41 <code>
41 <code>
42 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
42 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
43 r${commit.idx}:${h.short_id(commit.raw_id)}
43 r${commit.idx}:${h.short_id(commit.raw_id)}
44 </a>
44 </a>
45 ${h.hidden('revisions',commit.raw_id)}
45 ${h.hidden('revisions',commit.raw_id)}
46 </code>
46 </code>
47 </td>
47 </td>
48 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
48 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
49 <i class="icon-expand-linked"></i>
49 <i class="icon-expand-linked"></i>
50 </td>
50 </td>
51 <td class="mid td-description">
51 <td class="mid td-description">
52 <div class="log-container truncate-wrap">
52 <div class="log-container truncate-wrap">
53 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
53 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
54 </div>
54 </div>
55 </td>
55 </td>
56 </tr>
56 </tr>
57 %endfor
57 %endfor
58 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
58 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
59 <td colspan="5">
59 <td colspan="5">
60 ${_ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
60 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
61 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${_ungettext('show it','show them', len(c.commit_ranges))}</a>
62 </td>
61 </td>
63 </tr>
62 </tr>
64 % if not c.commit_ranges:
63 % if not c.commit_ranges:
65 <tr class="compare_select">
64 <tr class="compare_select">
66 <td colspan="5">
65 <td colspan="5">
67 ${_('No commits in this compare')}
66 ${_('No commits in this compare')}
68 </td>
67 </td>
69 </tr>
68 </tr>
70 % endif
69 % endif
71 </table>
70 </table>
72 <input type="hidden" name="__end__" value="revisions:sequence">
71 <input type="hidden" name="__end__" value="revisions:sequence">
73
72
74 </div>
73 </div>
75
74
76 <script>
75 <script>
77 commitsController = new CommitsController();
76 commitsController = new CommitsController();
78 $('.compare_select').on('click',function(e){
77 $('.compare_select').on('click',function(e){
79 var cid = $(this).attr('commit_id');
78 var cid = $(this).attr('commit_id');
80 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
79 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
81 });
80 });
82 </script>
81 </script>
@@ -1,311 +1,308 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 %if c.compare_home:
6 %if c.compare_home:
7 ${_('%s Compare') % c.repo_name}
7 ${_('%s Compare') % c.repo_name}
8 %else:
8 %else:
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 %endif
10 %endif
11 %if c.rhodecode_name:
11 %if c.rhodecode_name:
12 &middot; ${h.branding(c.rhodecode_name)}
12 &middot; ${h.branding(c.rhodecode_name)}
13 %endif
13 %endif
14 </%def>
14 </%def>
15
15
16 <%def name="breadcrumbs_links()"></%def>
16 <%def name="breadcrumbs_links()"></%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='compare')}
23 ${self.repo_menu(active='compare')}
24 </%def>
24 </%def>
25
25
26 <%def name="main()">
26 <%def name="main()">
27 <script type="text/javascript">
27 <script type="text/javascript">
28 // set fake commitId on this commit-range page
28 // set fake commitId on this commit-range page
29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
30 </script>
30 </script>
31
31
32 <div class="box">
32 <div class="box">
33 <div class="summary changeset">
33 <div class="summary changeset">
34 <div class="summary-detail">
34 <div class="summary-detail">
35 <div class="summary-detail-header">
35 <div class="summary-detail-header">
36 <span class="breadcrumbs files_location">
36 <span class="breadcrumbs files_location">
37 <h4>
37 <h4>
38 ${_('Compare Commits')}
38 ${_('Compare Commits')}
39 % if c.file_path:
39 % if c.file_path:
40 ${_('for file')} <a href="#${('a_' + h.FID('',c.file_path))}">${c.file_path}</a>
40 ${_('for file')} <a href="#${('a_' + h.FID('',c.file_path))}">${c.file_path}</a>
41 % endif
41 % endif
42
42
43 % if c.commit_ranges:
43 % if c.commit_ranges:
44 <code>
44 <code>
45 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
45 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
46 </code>
46 </code>
47 % endif
47 % endif
48 </h4>
48 </h4>
49 </span>
49 </span>
50
50
51 <div class="clear-fix"></div>
51 <div class="clear-fix"></div>
52 </div>
52 </div>
53
53
54 <div class="fieldset">
54 <div class="fieldset">
55 <div class="left-label-summary">
55 <div class="left-label-summary">
56 <p class="spacing">${_('Target')}:</p>
56 <p class="spacing">${_('Target')}:</p>
57 <div class="right-label-summary">
57 <div class="right-label-summary">
58 <div class="code-header" >
58 <div class="code-header" >
59 <div class="compare_header">
59 <div class="compare_header">
60 ## The hidden elements are replaced with a select2 widget
60 ## The hidden elements are replaced with a select2 widget
61 ${h.hidden('compare_source')}
61 ${h.hidden('compare_source')}
62 </div>
62 </div>
63 </div>
63 </div>
64 </div>
64 </div>
65 </div>
65 </div>
66 </div>
66 </div>
67
67
68 <div class="fieldset">
68 <div class="fieldset">
69 <div class="left-label-summary">
69 <div class="left-label-summary">
70 <p class="spacing">${_('Source')}:</p>
70 <p class="spacing">${_('Source')}:</p>
71 <div class="right-label-summary">
71 <div class="right-label-summary">
72 <div class="code-header" >
72 <div class="code-header" >
73 <div class="compare_header">
73 <div class="compare_header">
74 ## The hidden elements are replaced with a select2 widget
74 ## The hidden elements are replaced with a select2 widget
75 ${h.hidden('compare_target')}
75 ${h.hidden('compare_target')}
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79 </div>
79 </div>
80 </div>
80 </div>
81
81
82 <div class="fieldset">
82 <div class="fieldset">
83 <div class="left-label-summary">
83 <div class="left-label-summary">
84 <p class="spacing">${_('Actions')}:</p>
84 <p class="spacing">${_('Actions')}:</p>
85 <div class="right-label-summary">
85 <div class="right-label-summary">
86 <div class="code-header" >
86 <div class="code-header" >
87 <div class="compare_header">
87 <div class="compare_header">
88 <div class="compare-buttons">
88 <div class="compare-buttons">
89 % if c.compare_home:
89 % if c.compare_home:
90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
91 %if c.rhodecode_db_repo.fork:
91 %if c.rhodecode_db_repo.fork:
92
92
93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
94 href="${h.route_path('repo_compare',
94 href="${h.route_path('repo_compare',
95 repo_name=c.rhodecode_db_repo.fork.repo_name,
95 repo_name=c.rhodecode_db_repo.fork.repo_name,
96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
97 source_ref=c.rhodecode_db_repo.landing_rev[1],
97 source_ref=c.rhodecode_db_repo.landing_rev[1],
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
100 _query=dict(merge=1))}"
100 _query=dict(merge=1))}"
101 >
101 >
102 ${_('Compare with origin')}
102 ${_('Compare with origin')}
103 </a>
103 </a>
104
104
105 %endif
105 %endif
106
106
107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
109 <div id="changeset_compare_view_content">
109 <div id="changeset_compare_view_content">
110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
111 </div>
111 </div>
112
112
113 % elif c.preview_mode:
113 % elif c.preview_mode:
114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
117
117
118 % else:
118 % else:
119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
121
121
122 ## allow comment only if there are commits to comment on
122 ## allow comment only if there are commits to comment on
123 % if c.diffset and c.diffset.files and c.commit_ranges:
123 % if c.diffset and c.diffset.files and c.commit_ranges:
124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
125 % else:
125 % else:
126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
127 % endif
127 % endif
128 % endif
128 % endif
129 </div>
129 </div>
130 </div>
130 </div>
131 </div>
131 </div>
132 </div>
132 </div>
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 ## commit status form
136 ## commit status form
137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
138 <div class="left-label-summary">
138 <div class="left-label-summary">
139 <p class="spacing">${_('Commit status')}:</p>
139 <p class="spacing">${_('Commit status')}:</p>
140 <div class="right-label-summary">
140 <div class="right-label-summary">
141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
142 ## main comment form and it status
142 ## main comment form and it status
143 <%
143 <%
144 def revs(_revs):
144 def revs(_revs):
145 form_inputs = []
145 form_inputs = []
146 for cs in _revs:
146 for cs in _revs:
147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
148 form_inputs.append(tmpl)
148 form_inputs.append(tmpl)
149 return form_inputs
149 return form_inputs
150 %>
150 %>
151 <div>
151 <div>
152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
153 </div>
153 </div>
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div class="clear-fix"></div>
157 <div class="clear-fix"></div>
158 </div> <!-- end summary-detail -->
158 </div> <!-- end summary-detail -->
159 </div> <!-- end summary -->
159 </div> <!-- end summary -->
160
160
161 ## use JS script to load it quickly before potentially large diffs render long time
161 ## use JS script to load it quickly before potentially large diffs render long time
162 ## this prevents from situation when large diffs block rendering of select2 fields
162 ## this prevents from situation when large diffs block rendering of select2 fields
163 <script type="text/javascript">
163 <script type="text/javascript">
164
164
165 var cache = {};
165 var cache = {};
166
166
167 var formatSelection = function(repoName){
167 var formatSelection = function(repoName){
168 return function(data, container, escapeMarkup) {
168 return function(data, container, escapeMarkup) {
169 var selection = data ? this.text(data) : "";
169 var selection = data ? this.text(data) : "";
170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
171 }
171 }
172 };
172 };
173
173
174 var feedCompareData = function(query, cachedValue){
174 var feedCompareData = function(query, cachedValue){
175 var data = {results: []};
175 var data = {results: []};
176 //filter results
176 //filter results
177 $.each(cachedValue.results, function() {
177 $.each(cachedValue.results, function() {
178 var section = this.text;
178 var section = this.text;
179 var children = [];
179 var children = [];
180 $.each(this.children, function() {
180 $.each(this.children, function() {
181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
182 children.push({
182 children.push({
183 'id': this.id,
183 'id': this.id,
184 'text': this.text,
184 'text': this.text,
185 'type': this.type
185 'type': this.type
186 })
186 })
187 }
187 }
188 });
188 });
189 data.results.push({
189 data.results.push({
190 'text': section,
190 'text': section,
191 'children': children
191 'children': children
192 })
192 })
193 });
193 });
194 //push the typed in changeset
194 //push the typed in changeset
195 data.results.push({
195 data.results.push({
196 'text': _gettext('specify commit'),
196 'text': _gettext('specify commit'),
197 'children': [{
197 'children': [{
198 'id': query.term,
198 'id': query.term,
199 'text': query.term,
199 'text': query.term,
200 'type': 'rev'
200 'type': 'rev'
201 }]
201 }]
202 });
202 });
203 query.callback(data);
203 query.callback(data);
204 };
204 };
205
205
206 var loadCompareData = function(repoName, query, cache){
206 var loadCompareData = function(repoName, query, cache){
207 $.ajax({
207 $.ajax({
208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
209 data: {},
209 data: {},
210 dataType: 'json',
210 dataType: 'json',
211 type: 'GET',
211 type: 'GET',
212 success: function(data) {
212 success: function(data) {
213 cache[repoName] = data;
213 cache[repoName] = data;
214 query.callback({results: data.results});
214 query.callback({results: data.results});
215 }
215 }
216 })
216 })
217 };
217 };
218
218
219 var enable_fields = ${"false" if c.preview_mode else "true"};
219 var enable_fields = ${"false" if c.preview_mode else "true"};
220 $("#compare_source").select2({
220 $("#compare_source").select2({
221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
222 containerCssClass: "drop-menu",
222 containerCssClass: "drop-menu",
223 dropdownCssClass: "drop-menu-dropdown",
223 dropdownCssClass: "drop-menu-dropdown",
224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
225 dropdownAutoWidth: true,
225 dropdownAutoWidth: true,
226 query: function(query) {
226 query: function(query) {
227 var repoName = '${c.source_repo.repo_name}';
227 var repoName = '${c.source_repo.repo_name}';
228 var cachedValue = cache[repoName];
228 var cachedValue = cache[repoName];
229
229
230 if (cachedValue){
230 if (cachedValue){
231 feedCompareData(query, cachedValue);
231 feedCompareData(query, cachedValue);
232 }
232 }
233 else {
233 else {
234 loadCompareData(repoName, query, cache);
234 loadCompareData(repoName, query, cache);
235 }
235 }
236 }
236 }
237 }).select2("enable", enable_fields);
237 }).select2("enable", enable_fields);
238
238
239 $("#compare_target").select2({
239 $("#compare_target").select2({
240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
241 dropdownAutoWidth: true,
241 dropdownAutoWidth: true,
242 containerCssClass: "drop-menu",
242 containerCssClass: "drop-menu",
243 dropdownCssClass: "drop-menu-dropdown",
243 dropdownCssClass: "drop-menu-dropdown",
244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
245 query: function(query) {
245 query: function(query) {
246 var repoName = '${c.target_repo.repo_name}';
246 var repoName = '${c.target_repo.repo_name}';
247 var cachedValue = cache[repoName];
247 var cachedValue = cache[repoName];
248
248
249 if (cachedValue){
249 if (cachedValue){
250 feedCompareData(query, cachedValue);
250 feedCompareData(query, cachedValue);
251 }
251 }
252 else {
252 else {
253 loadCompareData(repoName, query, cache);
253 loadCompareData(repoName, query, cache);
254 }
254 }
255 }
255 }
256 }).select2("enable", enable_fields);
256 }).select2("enable", enable_fields);
257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
259
259
260 $('#compare_revs').on('click', function(e) {
260 $('#compare_revs').on('click', function(e) {
261 var source = $('#compare_source').select2('data') || initial_compare_source;
261 var source = $('#compare_source').select2('data') || initial_compare_source;
262 var target = $('#compare_target').select2('data') || initial_compare_target;
262 var target = $('#compare_target').select2('data') || initial_compare_target;
263 if (source && target) {
263 if (source && target) {
264 var url_data = {
264 var url_data = {
265 repo_name: "${c.repo_name}",
265 repo_name: "${c.repo_name}",
266 source_ref: source.id,
266 source_ref: source.id,
267 source_ref_type: source.type,
267 source_ref_type: source.type,
268 target_ref: target.id,
268 target_ref: target.id,
269 target_ref_type: target.type
269 target_ref_type: target.type
270 };
270 };
271 window.location = pyroutes.url('repo_compare', url_data);
271 window.location = pyroutes.url('repo_compare', url_data);
272 }
272 }
273 });
273 });
274 $('#compare_changeset_status_toggle').on('click', function(e) {
274 $('#compare_changeset_status_toggle').on('click', function(e) {
275 $('#compare_changeset_status').toggle();
275 $('#compare_changeset_status').toggle();
276 });
276 });
277
277
278 </script>
278 </script>
279
279
280 ## table diff data
280 ## table diff data
281 <div class="table">
281 <div class="table">
282 % if not c.compare_home:
282 % if not c.compare_home:
283 <div id="changeset_compare_view_content">
283 <div id="changeset_compare_view_content">
284 <div class="pull-left">
284 <div class="pull-left">
285 <div class="btn-group">
285 <div class="btn-group">
286 <a
286 <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)} >
287 class="btn"
287 % if c.collapse_all_commits:
288 href="#"
288 <i class="icon-plus-squared-alt icon-no-margin"></i>
289 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
289 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
290 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
290 % else:
291 </a>
291 <i class="icon-minus-squared-alt icon-no-margin"></i>
292 <a
292 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
293 class="btn"
293 % endif
294 href="#"
295 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
296 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
297 </a>
294 </a>
298 </div>
295 </div>
299 </div>
296 </div>
300 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
297 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
301 ## commit compare generated below
298 ## commit compare generated below
302 <%include file="compare_commits.mako"/>
299 <%include file="compare_commits.mako"/>
303 ${cbdiffs.render_diffset_menu(c.diffset)}
300 ${cbdiffs.render_diffset_menu(c.diffset)}
304 ${cbdiffs.render_diffset(c.diffset)}
301 ${cbdiffs.render_diffset(c.diffset)}
305 </div>
302 </div>
306 % endif
303 % endif
307
304
308 </div>
305 </div>
309 </div>
306 </div>
310
307
311 </%def>
308 </%def>
@@ -1,822 +1,819 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 <%
13 <%
14 pr_title = c.pull_request.title
14 pr_title = c.pull_request.title
15 if c.pull_request.is_closed():
15 if c.pull_request.is_closed():
16 pr_title = '[{}] {}'.format(_('Closed'), pr_title)
16 pr_title = '[{}] {}'.format(_('Closed'), pr_title)
17 %>
17 %>
18
18
19 <div id="pr-title">
19 <div id="pr-title">
20 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${pr_title}">
20 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${pr_title}">
21 </div>
21 </div>
22 <div id="pr-title-edit" class="input" style="display: none;">
22 <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}">
23 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
24 </div>
24 </div>
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_nav()">
27 <%def name="menu_bar_nav()">
28 ${self.menu_items(active='repositories')}
28 ${self.menu_items(active='repositories')}
29 </%def>
29 </%def>
30
30
31 <%def name="menu_bar_subnav()">
31 <%def name="menu_bar_subnav()">
32 ${self.repo_menu(active='showpullrequest')}
32 ${self.repo_menu(active='showpullrequest')}
33 </%def>
33 </%def>
34
34
35 <%def name="main()">
35 <%def name="main()">
36
36
37 <script type="text/javascript">
37 <script type="text/javascript">
38 // TODO: marcink switch this to pyroutes
38 // 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__')}";
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__')}";
40 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
40 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
41 </script>
41 </script>
42 <div class="box">
42 <div class="box">
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <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)}
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)}
52 %if c.allowed_to_update:
52 %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">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % 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)}
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)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${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')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <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>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %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>
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>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
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>
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>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
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>
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>
90 % endif
90 % endif
91 </div>
91 </div>
92 %if h.is_hg(c.pull_request.source_repo):
92 %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()) %>
93 <% 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):
94 %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) %>
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
96 %endif
96 %endif
97
97
98 <div class="">
98 <div class="">
99 <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>
100 <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>
101 </div>
102
102
103 </div>
103 </div>
104 </div>
104 </div>
105 <div class="field">
105 <div class="field">
106 <div class="label-summary">
106 <div class="label-summary">
107 <label>${_('Target')}:</label>
107 <label>${_('Target')}:</label>
108 </div>
108 </div>
109 <div class="input">
109 <div class="input">
110 <div class="pr-targetinfo">
110 <div class="pr-targetinfo">
111 ## branch link is only valid if it is a branch
111 ## branch link is only valid if it is a branch
112 <span class="tag">
112 <span class="tag">
113 %if c.pull_request.target_ref_parts.type == 'branch':
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>
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:
115 %else:
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 %endif
117 %endif
118 </span>
118 </span>
119 <span class="clone-url">
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>
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>
121 </span>
122 </div>
122 </div>
123 </div>
123 </div>
124 </div>
124 </div>
125
125
126 ## Link to the shadow repository.
126 ## Link to the shadow repository.
127 <div class="field">
127 <div class="field">
128 <div class="label-summary">
128 <div class="label-summary">
129 <label>${_('Merge')}:</label>
129 <label>${_('Merge')}:</label>
130 </div>
130 </div>
131 <div class="input">
131 <div class="input">
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 %if h.is_hg(c.pull_request.target_repo):
133 %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) %>
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) %>
135 %elif h.is_git(c.pull_request.target_repo):
135 %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) %>
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) %>
137 %endif
137 %endif
138 <div class="">
138 <div class="">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
139 <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>
140 <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>
141 </div>
142 % else:
142 % else:
143 <div class="">
143 <div class="">
144 ${_('Shadow repository data not available')}.
144 ${_('Shadow repository data not available')}.
145 </div>
145 </div>
146 % endif
146 % endif
147 </div>
147 </div>
148 </div>
148 </div>
149
149
150 <div class="field">
150 <div class="field">
151 <div class="label-summary">
151 <div class="label-summary">
152 <label>${_('Review')}:</label>
152 <label>${_('Review')}:</label>
153 </div>
153 </div>
154 <div class="input">
154 <div class="input">
155 %if c.pull_request_review_status:
155 %if c.pull_request_review_status:
156 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
156 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
157 <span class="changeset-status-lbl">
157 <span class="changeset-status-lbl">
158 %if c.pull_request.is_closed():
158 %if c.pull_request.is_closed():
159 ${_('Closed')},
159 ${_('Closed')},
160 %endif
160 %endif
161 ${h.commit_status_lbl(c.pull_request_review_status)}
161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 </span>
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)}
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
164 %endif
165 </div>
165 </div>
166 </div>
166 </div>
167 <div class="field">
167 <div class="field">
168 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
168 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
169 <label>${_('Description')}:</label>
169 <label>${_('Description')}:</label>
170 </div>
170 </div>
171 <div id="pr-desc" class="input">
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>
172 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
173 </div>
173 </div>
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
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}">
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)}
176 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
177 </div>
177 </div>
178 </div>
178 </div>
179
179
180 <div class="field">
180 <div class="field">
181 <div class="label-summary">
181 <div class="label-summary">
182 <label>${_('Versions')}:</label>
182 <label>${_('Versions')}:</label>
183 </div>
183 </div>
184
184
185 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
185 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
186 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
186 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
187
187
188 <div class="pr-versions">
188 <div class="pr-versions">
189 % if c.show_version_changes:
189 % if c.show_version_changes:
190 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
190 <% 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']) %>
191 <% 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"
192 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-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))}"
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))}"
194 data-toggle-off="${_('Hide all versions of this pull request')}">
194 data-toggle-off="${_('Hide all versions of this pull request')}">
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))}
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))}
196 </a>
196 </a>
197 <table>
197 <table>
198 ## SHOW ALL VERSIONS OF PR
198 ## SHOW ALL VERSIONS OF PR
199 <% ver_pr = None %>
199 <% ver_pr = None %>
200
200
201 % for data in reversed(list(enumerate(c.versions, 1))):
201 % for data in reversed(list(enumerate(c.versions, 1))):
202 <% ver_pos = data[0] %>
202 <% ver_pos = data[0] %>
203 <% ver = data[1] %>
203 <% ver = data[1] %>
204 <% ver_pr = ver.pull_request_version_id %>
204 <% 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' %>
205 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
206
206
207 <tr class="version-pr" style="display: ${display_row}">
207 <tr class="version-pr" style="display: ${display_row}">
208 <td>
208 <td>
209 <code>
209 <code>
210 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
210 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
211 </code>
211 </code>
212 </td>
212 </td>
213 <td>
213 <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}"/>
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}"/>
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}"/>
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}"/>
216 </td>
216 </td>
217 <td>
217 <td>
218 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
218 <% 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>
219 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
220
220
221 </td>
221 </td>
222 <td>
222 <td>
223 % if c.at_version_num != ver_pr:
223 % if c.at_version_num != ver_pr:
224 <i class="icon-comment"></i>
224 <i class="icon-comment"></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']))}">
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']))}">
226 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
226 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
227 </code>
227 </code>
228 % endif
228 % endif
229 </td>
229 </td>
230 <td>
230 <td>
231 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
231 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
232 </td>
232 </td>
233 <td>
233 <td>
234 ${h.age_component(ver.updated_on, time_is_local=True)}
234 ${h.age_component(ver.updated_on, time_is_local=True)}
235 </td>
235 </td>
236 </tr>
236 </tr>
237 % endfor
237 % endfor
238
238
239 <tr>
239 <tr>
240 <td colspan="6">
240 <td colspan="6">
241 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
241 <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')}"
242 data-label-text-locked="${_('select versions to show changes')}"
243 data-label-text-diff="${_('show changes between versions')}"
243 data-label-text-diff="${_('show changes between versions')}"
244 data-label-text-show="${_('show pull request for this version')}"
244 data-label-text-show="${_('show pull request for this version')}"
245 >
245 >
246 ${_('select versions to show changes')}
246 ${_('select versions to show changes')}
247 </button>
247 </button>
248 </td>
248 </td>
249 </tr>
249 </tr>
250 </table>
250 </table>
251 % else:
251 % else:
252 <div class="input">
252 <div class="input">
253 ${_('Pull request versions not available')}.
253 ${_('Pull request versions not available')}.
254 </div>
254 </div>
255 % endif
255 % endif
256 </div>
256 </div>
257 </div>
257 </div>
258
258
259 <div id="pr-save" class="field" style="display: none;">
259 <div id="pr-save" class="field" style="display: none;">
260 <div class="label-summary"></div>
260 <div class="label-summary"></div>
261 <div class="input">
261 <div class="input">
262 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
262 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
263 </div>
263 </div>
264 </div>
264 </div>
265 </div>
265 </div>
266 </div>
266 </div>
267 <div>
267 <div>
268 ## AUTHOR
268 ## AUTHOR
269 <div class="reviewers-title block-right">
269 <div class="reviewers-title block-right">
270 <div class="pr-details-title">
270 <div class="pr-details-title">
271 ${_('Author of this pull request')}
271 ${_('Author of this pull request')}
272 </div>
272 </div>
273 </div>
273 </div>
274 <div class="block-right pr-details-content reviewers">
274 <div class="block-right pr-details-content reviewers">
275 <ul class="group_members">
275 <ul class="group_members">
276 <li>
276 <li>
277 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
277 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
278 </li>
278 </li>
279 </ul>
279 </ul>
280 </div>
280 </div>
281
281
282 ## REVIEW RULES
282 ## REVIEW RULES
283 <div id="review_rules" style="display: none" class="reviewers-title block-right">
283 <div id="review_rules" style="display: none" class="reviewers-title block-right">
284 <div class="pr-details-title">
284 <div class="pr-details-title">
285 ${_('Reviewer rules')}
285 ${_('Reviewer rules')}
286 %if c.allowed_to_update:
286 %if c.allowed_to_update:
287 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
287 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
288 %endif
288 %endif
289 </div>
289 </div>
290 <div class="pr-reviewer-rules">
290 <div class="pr-reviewer-rules">
291 ## review rules will be appended here, by default reviewers logic
291 ## review rules will be appended here, by default reviewers logic
292 </div>
292 </div>
293 <input id="review_data" type="hidden" name="review_data" value="">
293 <input id="review_data" type="hidden" name="review_data" value="">
294 </div>
294 </div>
295
295
296 ## REVIEWERS
296 ## REVIEWERS
297 <div class="reviewers-title block-right">
297 <div class="reviewers-title block-right">
298 <div class="pr-details-title">
298 <div class="pr-details-title">
299 ${_('Pull request reviewers')}
299 ${_('Pull request reviewers')}
300 %if c.allowed_to_update:
300 %if c.allowed_to_update:
301 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
301 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
302 %endif
302 %endif
303 </div>
303 </div>
304 </div>
304 </div>
305 <div id="reviewers" class="block-right pr-details-content reviewers">
305 <div id="reviewers" class="block-right pr-details-content reviewers">
306
306
307 ## members redering block
307 ## members redering block
308 <input type="hidden" name="__start__" value="review_members:sequence">
308 <input type="hidden" name="__start__" value="review_members:sequence">
309 <ul id="review_members" class="group_members">
309 <ul id="review_members" class="group_members">
310
310
311 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
311 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
312 <script>
312 <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};
313 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')}";
314 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')}";
315 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)};
316 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
317
317
318 var entry = renderTemplate('reviewMemberEntry', {
318 var entry = renderTemplate('reviewMemberEntry', {
319 'member': member,
319 'member': member,
320 'mandatory': member.mandatory,
320 'mandatory': member.mandatory,
321 'reasons': member.reasons,
321 'reasons': member.reasons,
322 'allowed_to_update': allowed_to_update,
322 'allowed_to_update': allowed_to_update,
323 'review_status': status,
323 'review_status': status,
324 'review_status_label': status_lbl,
324 'review_status_label': status_lbl,
325 'user_group': member.user_group,
325 'user_group': member.user_group,
326 'create': false
326 'create': false
327 });
327 });
328 $('#review_members').append(entry)
328 $('#review_members').append(entry)
329 </script>
329 </script>
330
330
331 % endfor
331 % endfor
332
332
333 </ul>
333 </ul>
334
334
335 <input type="hidden" name="__end__" value="review_members:sequence">
335 <input type="hidden" name="__end__" value="review_members:sequence">
336 ## end members redering block
336 ## end members redering block
337
337
338 %if not c.pull_request.is_closed():
338 %if not c.pull_request.is_closed():
339 <div id="add_reviewer" class="ac" style="display: none;">
339 <div id="add_reviewer" class="ac" style="display: none;">
340 %if c.allowed_to_update:
340 %if c.allowed_to_update:
341 % if not c.forbid_adding_reviewers:
341 % if not c.forbid_adding_reviewers:
342 <div id="add_reviewer_input" class="reviewer_ac">
342 <div id="add_reviewer_input" class="reviewer_ac">
343 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
343 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
344 <div id="reviewers_container"></div>
344 <div id="reviewers_container"></div>
345 </div>
345 </div>
346 % endif
346 % endif
347 <div class="pull-right">
347 <div class="pull-right">
348 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
348 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
349 </div>
349 </div>
350 %endif
350 %endif
351 </div>
351 </div>
352 %endif
352 %endif
353 </div>
353 </div>
354 </div>
354 </div>
355 </div>
355 </div>
356
356
357 <div class="box">
357 <div class="box">
358
358
359 % if c.state_progressing:
359 % if c.state_progressing:
360 <h2 style="text-align: center">
360 <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>
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>
362 </h2>
362 </h2>
363
363
364 % else:
364 % else:
365
365
366 ## Diffs rendered here
366 ## Diffs rendered here
367 <div class="table" >
367 <div class="table" >
368 <div id="changeset_compare_view_content">
368 <div id="changeset_compare_view_content">
369 ##CS
369 ##CS
370 % if c.missing_requirements:
370 % if c.missing_requirements:
371 <div class="box">
371 <div class="box">
372 <div class="alert alert-warning">
372 <div class="alert alert-warning">
373 <div>
373 <div>
374 <strong>${_('Missing requirements:')}</strong>
374 <strong>${_('Missing requirements:')}</strong>
375 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
375 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
376 </div>
376 </div>
377 </div>
377 </div>
378 </div>
378 </div>
379 % elif c.missing_commits:
379 % elif c.missing_commits:
380 <div class="box">
380 <div class="box">
381 <div class="alert alert-warning">
381 <div class="alert alert-warning">
382 <div>
382 <div>
383 <strong>${_('Missing commits')}:</strong>
383 <strong>${_('Missing commits')}:</strong>
384 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
384 ${_('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.')}
385 ${_('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}
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}
387 </div>
387 </div>
388 </div>
388 </div>
389 </div>
389 </div>
390 % endif
390 % endif
391
391
392 <div class="compare_view_commits_title">
392 <div class="compare_view_commits_title">
393 % if not c.compare_mode:
393 % if not c.compare_mode:
394
394
395 % if c.at_version_pos:
395 % if c.at_version_pos:
396 <h4>
396 <h4>
397 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
397 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
398 </h4>
398 </h4>
399 % endif
399 % endif
400
400
401 <div class="pull-left">
401 <div class="pull-left">
402 <div class="btn-group">
402 <div class="btn-group">
403 <a
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)} >
404 class="btn"
404 % if c.collapse_all_commits:
405 href="#"
405 <i class="icon-plus-squared-alt icon-no-margin"></i>
406 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
406 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
407 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
407 % else:
408 </a>
408 <i class="icon-minus-squared-alt icon-no-margin"></i>
409 <a
409 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
410 class="btn"
410 % endif
411 href="#"
412 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
413 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
414 </a>
411 </a>
415 </div>
412 </div>
416 </div>
413 </div>
417
414
418 <div class="pull-right">
415 <div class="pull-right">
419 % if c.allowed_to_update and not c.pull_request.is_closed():
416 % if c.allowed_to_update and not c.pull_request.is_closed():
420
417
421 <div class="btn-group btn-group-actions">
418 <div class="btn-group btn-group-actions">
422 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
419 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
423 ${_('Update commits')}
420 ${_('Update commits')}
424 </a>
421 </a>
425
422
426 <a id="update_commits_switcher" class="btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button">
423 <a id="update_commits_switcher" class="btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button">
427 <i class="icon-down"></i>
424 <i class="icon-down"></i>
428 </a>
425 </a>
429
426
430 <div class="btn-action-switcher-container" id="update-commits-switcher">
427 <div class="btn-action-switcher-container" id="update-commits-switcher">
431 <ul class="btn-action-switcher" role="menu">
428 <ul class="btn-action-switcher" role="menu">
432 <li>
429 <li>
433 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
430 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
434 ${_('Force update commits')}
431 ${_('Force update commits')}
435 </a>
432 </a>
436 <div class="action-help-block">
433 <div class="action-help-block">
437 ${_('Update commits and force refresh this pull request.')}
434 ${_('Update commits and force refresh this pull request.')}
438 </div>
435 </div>
439 </li>
436 </li>
440 </ul>
437 </ul>
441 </div>
438 </div>
442 </div>
439 </div>
443
440
444 % else:
441 % else:
445 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
442 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
446 % endif
443 % endif
447
444
448 </div>
445 </div>
449 % endif
446 % endif
450 </div>
447 </div>
451
448
452 % if not c.missing_commits:
449 % if not c.missing_commits:
453 % if c.compare_mode:
450 % if c.compare_mode:
454 % if c.at_version:
451 % if c.at_version:
455 <h4>
452 <h4>
456 ${_('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')}:
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')}:
457 </h4>
454 </h4>
458
455
459 <div class="subtitle-compare">
456 <div class="subtitle-compare">
460 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
457 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
461 </div>
458 </div>
462
459
463 <div class="container">
460 <div class="container">
464 <table class="rctable compare_view_commits">
461 <table class="rctable compare_view_commits">
465 <tr>
462 <tr>
466 <th></th>
463 <th></th>
467 <th>${_('Time')}</th>
464 <th>${_('Time')}</th>
468 <th>${_('Author')}</th>
465 <th>${_('Author')}</th>
469 <th>${_('Commit')}</th>
466 <th>${_('Commit')}</th>
470 <th></th>
467 <th></th>
471 <th>${_('Description')}</th>
468 <th>${_('Description')}</th>
472 </tr>
469 </tr>
473
470
474 % for c_type, commit in c.commit_changes:
471 % for c_type, commit in c.commit_changes:
475 % if c_type in ['a', 'r']:
472 % if c_type in ['a', 'r']:
476 <%
473 <%
477 if c_type == 'a':
474 if c_type == 'a':
478 cc_title = _('Commit added in displayed changes')
475 cc_title = _('Commit added in displayed changes')
479 elif c_type == 'r':
476 elif c_type == 'r':
480 cc_title = _('Commit removed in displayed changes')
477 cc_title = _('Commit removed in displayed changes')
481 else:
478 else:
482 cc_title = ''
479 cc_title = ''
483 %>
480 %>
484 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
481 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
485 <td>
482 <td>
486 <div class="commit-change-indicator color-${c_type}-border">
483 <div class="commit-change-indicator color-${c_type}-border">
487 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
484 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
488 ${c_type.upper()}
485 ${c_type.upper()}
489 </div>
486 </div>
490 </div>
487 </div>
491 </td>
488 </td>
492 <td class="td-time">
489 <td class="td-time">
493 ${h.age_component(commit.date)}
490 ${h.age_component(commit.date)}
494 </td>
491 </td>
495 <td class="td-user">
492 <td class="td-user">
496 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
493 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
497 </td>
494 </td>
498 <td class="td-hash">
495 <td class="td-hash">
499 <code>
496 <code>
500 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
497 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
501 r${commit.idx}:${h.short_id(commit.raw_id)}
498 r${commit.idx}:${h.short_id(commit.raw_id)}
502 </a>
499 </a>
503 ${h.hidden('revisions', commit.raw_id)}
500 ${h.hidden('revisions', commit.raw_id)}
504 </code>
501 </code>
505 </td>
502 </td>
506 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
503 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
507 <i class="icon-expand-linked"></i>
504 <i class="icon-expand-linked"></i>
508 </td>
505 </td>
509 <td class="mid td-description">
506 <td class="mid td-description">
510 <div class="log-container truncate-wrap">
507 <div class="log-container truncate-wrap">
511 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
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>
512 </div>
509 </div>
513 </td>
510 </td>
514 </tr>
511 </tr>
515 % endif
512 % endif
516 % endfor
513 % endfor
517 </table>
514 </table>
518 </div>
515 </div>
519
516
520 % endif
517 % endif
521
518
522 % else:
519 % else:
523 <%include file="/compare/compare_commits.mako" />
520 <%include file="/compare/compare_commits.mako" />
524 % endif
521 % endif
525
522
526 <div class="cs_files">
523 <div class="cs_files">
527 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
524 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
528 % if c.at_version:
525 % if c.at_version:
529 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
526 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
530 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
527 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
531 % else:
528 % else:
532 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
529 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
533 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
530 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
534 % endif
531 % endif
535
532
536 <%
533 <%
537 pr_menu_data = {
534 pr_menu_data = {
538 'outdated_comm_count_ver': outdated_comm_count_ver
535 'outdated_comm_count_ver': outdated_comm_count_ver
539 }
536 }
540 %>
537 %>
541
538
542 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
539 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
543
540
544 % if c.range_diff_on:
541 % if c.range_diff_on:
545 % for commit in c.commit_ranges:
542 % for commit in c.commit_ranges:
546 ${cbdiffs.render_diffset(
543 ${cbdiffs.render_diffset(
547 c.changes[commit.raw_id],
544 c.changes[commit.raw_id],
548 commit=commit, use_comments=True,
545 commit=commit, use_comments=True,
549 collapse_when_files_over=5,
546 collapse_when_files_over=5,
550 disable_new_comments=True,
547 disable_new_comments=True,
551 deleted_files_comments=c.deleted_files_comments,
548 deleted_files_comments=c.deleted_files_comments,
552 inline_comments=c.inline_comments,
549 inline_comments=c.inline_comments,
553 pull_request_menu=pr_menu_data)}
550 pull_request_menu=pr_menu_data)}
554 % endfor
551 % endfor
555 % else:
552 % else:
556 ${cbdiffs.render_diffset(
553 ${cbdiffs.render_diffset(
557 c.diffset, use_comments=True,
554 c.diffset, use_comments=True,
558 collapse_when_files_over=30,
555 collapse_when_files_over=30,
559 disable_new_comments=not c.allowed_to_comment,
556 disable_new_comments=not c.allowed_to_comment,
560 deleted_files_comments=c.deleted_files_comments,
557 deleted_files_comments=c.deleted_files_comments,
561 inline_comments=c.inline_comments,
558 inline_comments=c.inline_comments,
562 pull_request_menu=pr_menu_data)}
559 pull_request_menu=pr_menu_data)}
563 % endif
560 % endif
564
561
565 </div>
562 </div>
566 % else:
563 % else:
567 ## skipping commits we need to clear the view for missing commits
564 ## skipping commits we need to clear the view for missing commits
568 <div style="clear:both;"></div>
565 <div style="clear:both;"></div>
569 % endif
566 % endif
570
567
571 </div>
568 </div>
572 </div>
569 </div>
573
570
574 ## template for inline comment form
571 ## template for inline comment form
575 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
572 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
576
573
577 ## comments heading with count
574 ## comments heading with count
578 <div class="comments-heading">
575 <div class="comments-heading">
579 <i class="icon-comment"></i>
576 <i class="icon-comment"></i>
580 ${_('Comments')} ${len(c.comments)}
577 ${_('Comments')} ${len(c.comments)}
581 </div>
578 </div>
582
579
583 ## render general comments
580 ## render general comments
584 <div id="comment-tr-show">
581 <div id="comment-tr-show">
585 % if general_outdated_comm_count_ver:
582 % if general_outdated_comm_count_ver:
586 <div class="info-box">
583 <div class="info-box">
587 % if general_outdated_comm_count_ver == 1:
584 % if general_outdated_comm_count_ver == 1:
588 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
585 ${_('there is {num} general comment 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 it')}</a>
586 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
590 % else:
587 % else:
591 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
588 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
592 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
589 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
593 % endif
590 % endif
594 </div>
591 </div>
595 % endif
592 % endif
596 </div>
593 </div>
597
594
598 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
595 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
599
596
600 % if not c.pull_request.is_closed():
597 % if not c.pull_request.is_closed():
601 ## main comment form and it status
598 ## main comment form and it status
602 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
599 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
603 pull_request_id=c.pull_request.pull_request_id),
600 pull_request_id=c.pull_request.pull_request_id),
604 c.pull_request_review_status,
601 c.pull_request_review_status,
605 is_pull_request=True, change_status=c.allowed_to_change_status)}
602 is_pull_request=True, change_status=c.allowed_to_change_status)}
606
603
607 ## merge status, and merge action
604 ## merge status, and merge action
608 <div class="pull-request-merge">
605 <div class="pull-request-merge">
609 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
606 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
610 </div>
607 </div>
611
608
612 %endif
609 %endif
613
610
614 % endif
611 % endif
615 </div>
612 </div>
616
613
617 <script type="text/javascript">
614 <script type="text/javascript">
618
615
619 versionController = new VersionController();
616 versionController = new VersionController();
620 versionController.init();
617 versionController.init();
621
618
622 reviewersController = new ReviewersController();
619 reviewersController = new ReviewersController();
623 commitsController = new CommitsController();
620 commitsController = new CommitsController();
624
621
625 updateController = new UpdatePrController();
622 updateController = new UpdatePrController();
626
623
627 $(function(){
624 $(function(){
628
625
629 // custom code mirror
626 // custom code mirror
630 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
627 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
631
628
632 var PRDetails = {
629 var PRDetails = {
633 editButton: $('#open_edit_pullrequest'),
630 editButton: $('#open_edit_pullrequest'),
634 closeButton: $('#close_edit_pullrequest'),
631 closeButton: $('#close_edit_pullrequest'),
635 deleteButton: $('#delete_pullrequest'),
632 deleteButton: $('#delete_pullrequest'),
636 viewFields: $('#pr-desc, #pr-title'),
633 viewFields: $('#pr-desc, #pr-title'),
637 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
634 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
638
635
639 init: function() {
636 init: function() {
640 var that = this;
637 var that = this;
641 this.editButton.on('click', function(e) { that.edit(); });
638 this.editButton.on('click', function(e) { that.edit(); });
642 this.closeButton.on('click', function(e) { that.view(); });
639 this.closeButton.on('click', function(e) { that.view(); });
643 },
640 },
644
641
645 edit: function(event) {
642 edit: function(event) {
646 this.viewFields.hide();
643 this.viewFields.hide();
647 this.editButton.hide();
644 this.editButton.hide();
648 this.deleteButton.hide();
645 this.deleteButton.hide();
649 this.closeButton.show();
646 this.closeButton.show();
650 this.editFields.show();
647 this.editFields.show();
651 codeMirrorInstance.refresh();
648 codeMirrorInstance.refresh();
652 },
649 },
653
650
654 view: function(event) {
651 view: function(event) {
655 this.editButton.show();
652 this.editButton.show();
656 this.deleteButton.show();
653 this.deleteButton.show();
657 this.editFields.hide();
654 this.editFields.hide();
658 this.closeButton.hide();
655 this.closeButton.hide();
659 this.viewFields.show();
656 this.viewFields.show();
660 }
657 }
661 };
658 };
662
659
663 var ReviewersPanel = {
660 var ReviewersPanel = {
664 editButton: $('#open_edit_reviewers'),
661 editButton: $('#open_edit_reviewers'),
665 closeButton: $('#close_edit_reviewers'),
662 closeButton: $('#close_edit_reviewers'),
666 addButton: $('#add_reviewer'),
663 addButton: $('#add_reviewer'),
667 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
664 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
668
665
669 init: function() {
666 init: function() {
670 var self = this;
667 var self = this;
671 this.editButton.on('click', function(e) { self.edit(); });
668 this.editButton.on('click', function(e) { self.edit(); });
672 this.closeButton.on('click', function(e) { self.close(); });
669 this.closeButton.on('click', function(e) { self.close(); });
673 },
670 },
674
671
675 edit: function(event) {
672 edit: function(event) {
676 this.editButton.hide();
673 this.editButton.hide();
677 this.closeButton.show();
674 this.closeButton.show();
678 this.addButton.show();
675 this.addButton.show();
679 this.removeButtons.css('visibility', 'visible');
676 this.removeButtons.css('visibility', 'visible');
680 // review rules
677 // review rules
681 reviewersController.loadReviewRules(
678 reviewersController.loadReviewRules(
682 ${c.pull_request.reviewer_data_json | n});
679 ${c.pull_request.reviewer_data_json | n});
683 },
680 },
684
681
685 close: function(event) {
682 close: function(event) {
686 this.editButton.show();
683 this.editButton.show();
687 this.closeButton.hide();
684 this.closeButton.hide();
688 this.addButton.hide();
685 this.addButton.hide();
689 this.removeButtons.css('visibility', 'hidden');
686 this.removeButtons.css('visibility', 'hidden');
690 // hide review rules
687 // hide review rules
691 reviewersController.hideReviewRules()
688 reviewersController.hideReviewRules()
692 }
689 }
693 };
690 };
694
691
695 PRDetails.init();
692 PRDetails.init();
696 ReviewersPanel.init();
693 ReviewersPanel.init();
697
694
698 showOutdated = function(self){
695 showOutdated = function(self){
699 $('.comment-inline.comment-outdated').show();
696 $('.comment-inline.comment-outdated').show();
700 $('.filediff-outdated').show();
697 $('.filediff-outdated').show();
701 $('.showOutdatedComments').hide();
698 $('.showOutdatedComments').hide();
702 $('.hideOutdatedComments').show();
699 $('.hideOutdatedComments').show();
703 };
700 };
704
701
705 hideOutdated = function(self){
702 hideOutdated = function(self){
706 $('.comment-inline.comment-outdated').hide();
703 $('.comment-inline.comment-outdated').hide();
707 $('.filediff-outdated').hide();
704 $('.filediff-outdated').hide();
708 $('.hideOutdatedComments').hide();
705 $('.hideOutdatedComments').hide();
709 $('.showOutdatedComments').show();
706 $('.showOutdatedComments').show();
710 };
707 };
711
708
712 refreshMergeChecks = function(){
709 refreshMergeChecks = function(){
713 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
710 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
714 $('.pull-request-merge').css('opacity', 0.3);
711 $('.pull-request-merge').css('opacity', 0.3);
715 $('.action-buttons-extra').css('opacity', 0.3);
712 $('.action-buttons-extra').css('opacity', 0.3);
716
713
717 $('.pull-request-merge').load(
714 $('.pull-request-merge').load(
718 loadUrl, function() {
715 loadUrl, function() {
719 $('.pull-request-merge').css('opacity', 1);
716 $('.pull-request-merge').css('opacity', 1);
720
717
721 $('.action-buttons-extra').css('opacity', 1);
718 $('.action-buttons-extra').css('opacity', 1);
722 }
719 }
723 );
720 );
724 };
721 };
725
722
726 closePullRequest = function (status) {
723 closePullRequest = function (status) {
727 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
724 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
728 return false;
725 return false;
729 }
726 }
730 // inject closing flag
727 // inject closing flag
731 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
728 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
732 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
729 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
733 $(generalCommentForm.submitForm).submit();
730 $(generalCommentForm.submitForm).submit();
734 };
731 };
735
732
736 $('#show-outdated-comments').on('click', function(e){
733 $('#show-outdated-comments').on('click', function(e){
737 var button = $(this);
734 var button = $(this);
738 var outdated = $('.comment-outdated');
735 var outdated = $('.comment-outdated');
739
736
740 if (button.html() === "(Show)") {
737 if (button.html() === "(Show)") {
741 button.html("(Hide)");
738 button.html("(Hide)");
742 outdated.show();
739 outdated.show();
743 } else {
740 } else {
744 button.html("(Show)");
741 button.html("(Show)");
745 outdated.hide();
742 outdated.hide();
746 }
743 }
747 });
744 });
748
745
749 $('.show-inline-comments').on('change', function(e){
746 $('.show-inline-comments').on('change', function(e){
750 var show = 'none';
747 var show = 'none';
751 var target = e.currentTarget;
748 var target = e.currentTarget;
752 if(target.checked){
749 if(target.checked){
753 show = ''
750 show = ''
754 }
751 }
755 var boxid = $(target).attr('id_for');
752 var boxid = $(target).attr('id_for');
756 var comments = $('#{0} .inline-comments'.format(boxid));
753 var comments = $('#{0} .inline-comments'.format(boxid));
757 var fn_display = function(idx){
754 var fn_display = function(idx){
758 $(this).css('display', show);
755 $(this).css('display', show);
759 };
756 };
760 $(comments).each(fn_display);
757 $(comments).each(fn_display);
761 var btns = $('#{0} .inline-comments-button'.format(boxid));
758 var btns = $('#{0} .inline-comments-button'.format(boxid));
762 $(btns).each(fn_display);
759 $(btns).each(fn_display);
763 });
760 });
764
761
765 $('#merge_pull_request_form').submit(function() {
762 $('#merge_pull_request_form').submit(function() {
766 if (!$('#merge_pull_request').attr('disabled')) {
763 if (!$('#merge_pull_request').attr('disabled')) {
767 $('#merge_pull_request').attr('disabled', 'disabled');
764 $('#merge_pull_request').attr('disabled', 'disabled');
768 }
765 }
769 return true;
766 return true;
770 });
767 });
771
768
772 $('#edit_pull_request').on('click', function(e){
769 $('#edit_pull_request').on('click', function(e){
773 var title = $('#pr-title-input').val();
770 var title = $('#pr-title-input').val();
774 var description = codeMirrorInstance.getValue();
771 var description = codeMirrorInstance.getValue();
775 var renderer = $('#pr-renderer-input').val();
772 var renderer = $('#pr-renderer-input').val();
776 editPullRequest(
773 editPullRequest(
777 "${c.repo_name}", "${c.pull_request.pull_request_id}",
774 "${c.repo_name}", "${c.pull_request.pull_request_id}",
778 title, description, renderer);
775 title, description, renderer);
779 });
776 });
780
777
781 $('#update_pull_request').on('click', function(e){
778 $('#update_pull_request').on('click', function(e){
782 $(this).attr('disabled', 'disabled');
779 $(this).attr('disabled', 'disabled');
783 $(this).addClass('disabled');
780 $(this).addClass('disabled');
784 $(this).html(_gettext('Saving...'));
781 $(this).html(_gettext('Saving...'));
785 reviewersController.updateReviewers(
782 reviewersController.updateReviewers(
786 "${c.repo_name}", "${c.pull_request.pull_request_id}");
783 "${c.repo_name}", "${c.pull_request.pull_request_id}");
787 });
784 });
788
785
789
786
790 // fixing issue with caches on firefox
787 // fixing issue with caches on firefox
791 $('#update_commits').removeAttr("disabled");
788 $('#update_commits').removeAttr("disabled");
792
789
793 $('.show-inline-comments').on('click', function(e){
790 $('.show-inline-comments').on('click', function(e){
794 var boxid = $(this).attr('data-comment-id');
791 var boxid = $(this).attr('data-comment-id');
795 var button = $(this);
792 var button = $(this);
796
793
797 if(button.hasClass("comments-visible")) {
794 if(button.hasClass("comments-visible")) {
798 $('#{0} .inline-comments'.format(boxid)).each(function(index){
795 $('#{0} .inline-comments'.format(boxid)).each(function(index){
799 $(this).hide();
796 $(this).hide();
800 });
797 });
801 button.removeClass("comments-visible");
798 button.removeClass("comments-visible");
802 } else {
799 } else {
803 $('#{0} .inline-comments'.format(boxid)).each(function(index){
800 $('#{0} .inline-comments'.format(boxid)).each(function(index){
804 $(this).show();
801 $(this).show();
805 });
802 });
806 button.addClass("comments-visible");
803 button.addClass("comments-visible");
807 }
804 }
808 });
805 });
809
806
810 // register submit callback on commentForm form to track TODOs
807 // register submit callback on commentForm form to track TODOs
811 window.commentFormGlobalSubmitSuccessCallback = function(){
808 window.commentFormGlobalSubmitSuccessCallback = function(){
812 refreshMergeChecks();
809 refreshMergeChecks();
813 };
810 };
814
811
815 ReviewerAutoComplete('#user');
812 ReviewerAutoComplete('#user');
816
813
817 })
814 })
818
815
819 </script>
816 </script>
820 </div>
817 </div>
821
818
822 </%def>
819 </%def>
General Comments 0
You need to be logged in to leave comments. Login now