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