##// END OF EJS Templates
feat(ui): added ability to replace binary file through UI, added related tests. Fixes: RCCE-19
ilin.s -
r5274:6d0b768f default
parent child Browse files
Show More
@@ -1,1225 +1,1235 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 from rhodecode.apps._base import add_route_with_slash
19 19
20 20
21 21 def includeme(config):
22 22 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
23 23 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
24 24 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
25 25 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
26 26 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
27 27 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
28 28 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
29 29 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
30 30 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
31 31 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
32 32 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
33 33 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
34 34 from rhodecode.apps.repository.views.repo_files import RepoFilesView
35 35 from rhodecode.apps.repository.views.repo_forks import RepoForksView
36 36 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
37 37 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
38 38 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
39 39 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
40 40 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
41 41 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
42 42 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
43 43 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
44 44 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
45 45 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
46 46 from rhodecode.apps.repository.views.repo_strip import RepoStripView
47 47 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
48 48 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
49 49
50 50 # repo creating checks, special cases that aren't repo routes
51 51 config.add_route(
52 52 name='repo_creating',
53 53 pattern='/{repo_name:.*?[^/]}/repo_creating')
54 54 config.add_view(
55 55 RepoChecksView,
56 56 attr='repo_creating',
57 57 route_name='repo_creating', request_method='GET',
58 58 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
59 59
60 60 config.add_route(
61 61 name='repo_creating_check',
62 62 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
63 63 config.add_view(
64 64 RepoChecksView,
65 65 attr='repo_creating_check',
66 66 route_name='repo_creating_check', request_method='GET',
67 67 renderer='json_ext')
68 68
69 69 # Summary
70 70 # NOTE(marcink): one additional route is defined in very bottom, catch
71 71 # all pattern
72 72 config.add_route(
73 73 name='repo_summary_explicit',
74 74 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
75 75 config.add_view(
76 76 RepoSummaryView,
77 77 attr='summary',
78 78 route_name='repo_summary_explicit', request_method='GET',
79 79 renderer='rhodecode:templates/summary/summary.mako')
80 80
81 81 config.add_route(
82 82 name='repo_summary_commits',
83 83 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
84 84 config.add_view(
85 85 RepoSummaryView,
86 86 attr='summary_commits',
87 87 route_name='repo_summary_commits', request_method='GET',
88 88 renderer='rhodecode:templates/summary/summary_commits.mako')
89 89
90 90 # Commits
91 91 config.add_route(
92 92 name='repo_commit',
93 93 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
94 94 config.add_view(
95 95 RepoCommitsView,
96 96 attr='repo_commit_show',
97 97 route_name='repo_commit', request_method='GET',
98 98 renderer=None)
99 99
100 100 config.add_route(
101 101 name='repo_commit_children',
102 102 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
103 103 config.add_view(
104 104 RepoCommitsView,
105 105 attr='repo_commit_children',
106 106 route_name='repo_commit_children', request_method='GET',
107 107 renderer='json_ext', xhr=True)
108 108
109 109 config.add_route(
110 110 name='repo_commit_parents',
111 111 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
112 112 config.add_view(
113 113 RepoCommitsView,
114 114 attr='repo_commit_parents',
115 115 route_name='repo_commit_parents', request_method='GET',
116 116 renderer='json_ext')
117 117
118 118 config.add_route(
119 119 name='repo_commit_raw',
120 120 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
121 121 config.add_view(
122 122 RepoCommitsView,
123 123 attr='repo_commit_raw',
124 124 route_name='repo_commit_raw', request_method='GET',
125 125 renderer=None)
126 126
127 127 config.add_route(
128 128 name='repo_commit_patch',
129 129 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
130 130 config.add_view(
131 131 RepoCommitsView,
132 132 attr='repo_commit_patch',
133 133 route_name='repo_commit_patch', request_method='GET',
134 134 renderer=None)
135 135
136 136 config.add_route(
137 137 name='repo_commit_download',
138 138 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
139 139 config.add_view(
140 140 RepoCommitsView,
141 141 attr='repo_commit_download',
142 142 route_name='repo_commit_download', request_method='GET',
143 143 renderer=None)
144 144
145 145 config.add_route(
146 146 name='repo_commit_data',
147 147 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
148 148 config.add_view(
149 149 RepoCommitsView,
150 150 attr='repo_commit_data',
151 151 route_name='repo_commit_data', request_method='GET',
152 152 renderer='json_ext', xhr=True)
153 153
154 154 config.add_route(
155 155 name='repo_commit_comment_create',
156 156 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
157 157 config.add_view(
158 158 RepoCommitsView,
159 159 attr='repo_commit_comment_create',
160 160 route_name='repo_commit_comment_create', request_method='POST',
161 161 renderer='json_ext')
162 162
163 163 config.add_route(
164 164 name='repo_commit_comment_preview',
165 165 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
166 166 config.add_view(
167 167 RepoCommitsView,
168 168 attr='repo_commit_comment_preview',
169 169 route_name='repo_commit_comment_preview', request_method='POST',
170 170 renderer='string', xhr=True)
171 171
172 172 config.add_route(
173 173 name='repo_commit_comment_history_view',
174 174 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
175 175 config.add_view(
176 176 RepoCommitsView,
177 177 attr='repo_commit_comment_history_view',
178 178 route_name='repo_commit_comment_history_view', request_method='POST',
179 179 renderer='string', xhr=True)
180 180
181 181 config.add_route(
182 182 name='repo_commit_comment_attachment_upload',
183 183 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
184 184 config.add_view(
185 185 RepoCommitsView,
186 186 attr='repo_commit_comment_attachment_upload',
187 187 route_name='repo_commit_comment_attachment_upload', request_method='POST',
188 188 renderer='json_ext', xhr=True)
189 189
190 190 config.add_route(
191 191 name='repo_commit_comment_delete',
192 192 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
193 193 config.add_view(
194 194 RepoCommitsView,
195 195 attr='repo_commit_comment_delete',
196 196 route_name='repo_commit_comment_delete', request_method='POST',
197 197 renderer='json_ext')
198 198
199 199 config.add_route(
200 200 name='repo_commit_comment_edit',
201 201 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
202 202 config.add_view(
203 203 RepoCommitsView,
204 204 attr='repo_commit_comment_edit',
205 205 route_name='repo_commit_comment_edit', request_method='POST',
206 206 renderer='json_ext')
207 207
208 208 # still working url for backward compat.
209 209 config.add_route(
210 210 name='repo_commit_raw_deprecated',
211 211 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
212 212 config.add_view(
213 213 RepoCommitsView,
214 214 attr='repo_commit_raw',
215 215 route_name='repo_commit_raw_deprecated', request_method='GET',
216 216 renderer=None)
217 217
218 218 # Files
219 219 config.add_route(
220 220 name='repo_archivefile',
221 221 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
222 222 config.add_view(
223 223 RepoFilesView,
224 224 attr='repo_archivefile',
225 225 route_name='repo_archivefile', request_method='GET',
226 226 renderer=None)
227 227
228 228 config.add_route(
229 229 name='repo_files_diff',
230 230 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
231 231 config.add_view(
232 232 RepoFilesView,
233 233 attr='repo_files_diff',
234 234 route_name='repo_files_diff', request_method='GET',
235 235 renderer=None)
236 236
237 237 config.add_route( # legacy route to make old links work
238 238 name='repo_files_diff_2way_redirect',
239 239 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
240 240 config.add_view(
241 241 RepoFilesView,
242 242 attr='repo_files_diff_2way_redirect',
243 243 route_name='repo_files_diff_2way_redirect', request_method='GET',
244 244 renderer=None)
245 245
246 246 config.add_route(
247 247 name='repo_files',
248 248 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
249 249 config.add_view(
250 250 RepoFilesView,
251 251 attr='repo_files',
252 252 route_name='repo_files', request_method='GET',
253 253 renderer=None)
254 254
255 255 config.add_route(
256 256 name='repo_files:default_path',
257 257 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
258 258 config.add_view(
259 259 RepoFilesView,
260 260 attr='repo_files',
261 261 route_name='repo_files:default_path', request_method='GET',
262 262 renderer=None)
263 263
264 264 config.add_route(
265 265 name='repo_files:default_commit',
266 266 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
267 267 config.add_view(
268 268 RepoFilesView,
269 269 attr='repo_files',
270 270 route_name='repo_files:default_commit', request_method='GET',
271 271 renderer=None)
272 272
273 273 config.add_route(
274 274 name='repo_files:rendered',
275 275 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
276 276 config.add_view(
277 277 RepoFilesView,
278 278 attr='repo_files',
279 279 route_name='repo_files:rendered', request_method='GET',
280 280 renderer=None)
281 281
282 282 config.add_route(
283 283 name='repo_files:annotated',
284 284 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
285 285 config.add_view(
286 286 RepoFilesView,
287 287 attr='repo_files',
288 288 route_name='repo_files:annotated', request_method='GET',
289 289 renderer=None)
290 290
291 291 config.add_route(
292 292 name='repo_files:annotated_previous',
293 293 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
294 294 config.add_view(
295 295 RepoFilesView,
296 296 attr='repo_files_annotated_previous',
297 297 route_name='repo_files:annotated_previous', request_method='GET',
298 298 renderer=None)
299 299
300 300 config.add_route(
301 301 name='repo_nodetree_full',
302 302 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
303 303 config.add_view(
304 304 RepoFilesView,
305 305 attr='repo_nodetree_full',
306 306 route_name='repo_nodetree_full', request_method='GET',
307 307 renderer=None, xhr=True)
308 308
309 309 config.add_route(
310 310 name='repo_nodetree_full:default_path',
311 311 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
312 312 config.add_view(
313 313 RepoFilesView,
314 314 attr='repo_nodetree_full',
315 315 route_name='repo_nodetree_full:default_path', request_method='GET',
316 316 renderer=None, xhr=True)
317 317
318 318 config.add_route(
319 319 name='repo_files_nodelist',
320 320 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
321 321 config.add_view(
322 322 RepoFilesView,
323 323 attr='repo_nodelist',
324 324 route_name='repo_files_nodelist', request_method='GET',
325 325 renderer='json_ext', xhr=True)
326 326
327 327 config.add_route(
328 328 name='repo_file_raw',
329 329 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
330 330 config.add_view(
331 331 RepoFilesView,
332 332 attr='repo_file_raw',
333 333 route_name='repo_file_raw', request_method='GET',
334 334 renderer=None)
335 335
336 336 config.add_route(
337 337 name='repo_file_download',
338 338 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
339 339 config.add_view(
340 340 RepoFilesView,
341 341 attr='repo_file_download',
342 342 route_name='repo_file_download', request_method='GET',
343 343 renderer=None)
344 344
345 345 config.add_route( # backward compat to keep old links working
346 346 name='repo_file_download:legacy',
347 347 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
348 348 repo_route=True)
349 349 config.add_view(
350 350 RepoFilesView,
351 351 attr='repo_file_download',
352 352 route_name='repo_file_download:legacy', request_method='GET',
353 353 renderer=None)
354 354
355 355 config.add_route(
356 356 name='repo_file_history',
357 357 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
358 358 config.add_view(
359 359 RepoFilesView,
360 360 attr='repo_file_history',
361 361 route_name='repo_file_history', request_method='GET',
362 362 renderer='json_ext')
363 363
364 364 config.add_route(
365 365 name='repo_file_authors',
366 366 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
367 367 config.add_view(
368 368 RepoFilesView,
369 369 attr='repo_file_authors',
370 370 route_name='repo_file_authors', request_method='GET',
371 371 renderer='rhodecode:templates/files/file_authors_box.mako')
372 372
373 373 config.add_route(
374 374 name='repo_files_check_head',
375 375 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
376 376 repo_route=True)
377 377 config.add_view(
378 378 RepoFilesView,
379 379 attr='repo_files_check_head',
380 380 route_name='repo_files_check_head', request_method='POST',
381 381 renderer='json_ext', xhr=True)
382 382
383 383 config.add_route(
384 384 name='repo_files_remove_file',
385 385 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
386 386 repo_route=True)
387 387 config.add_view(
388 388 RepoFilesView,
389 389 attr='repo_files_remove_file',
390 390 route_name='repo_files_remove_file', request_method='GET',
391 391 renderer='rhodecode:templates/files/files_delete.mako')
392 392
393 393 config.add_route(
394 394 name='repo_files_delete_file',
395 395 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
396 396 repo_route=True)
397 397 config.add_view(
398 398 RepoFilesView,
399 399 attr='repo_files_delete_file',
400 400 route_name='repo_files_delete_file', request_method='POST',
401 401 renderer=None)
402 402
403 403 config.add_route(
404 404 name='repo_files_edit_file',
405 405 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
406 406 repo_route=True)
407 407 config.add_view(
408 408 RepoFilesView,
409 409 attr='repo_files_edit_file',
410 410 route_name='repo_files_edit_file', request_method='GET',
411 411 renderer='rhodecode:templates/files/files_edit.mako')
412 412
413 413 config.add_route(
414 414 name='repo_files_update_file',
415 415 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
416 416 repo_route=True)
417 417 config.add_view(
418 418 RepoFilesView,
419 419 attr='repo_files_update_file',
420 420 route_name='repo_files_update_file', request_method='POST',
421 421 renderer=None)
422 422
423 423 config.add_route(
424 424 name='repo_files_add_file',
425 425 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
426 426 repo_route=True)
427 427 config.add_view(
428 428 RepoFilesView,
429 429 attr='repo_files_add_file',
430 430 route_name='repo_files_add_file', request_method='GET',
431 431 renderer='rhodecode:templates/files/files_add.mako')
432 432
433 433 config.add_route(
434 434 name='repo_files_upload_file',
435 435 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
436 436 repo_route=True)
437 437 config.add_view(
438 438 RepoFilesView,
439 439 attr='repo_files_add_file',
440 440 route_name='repo_files_upload_file', request_method='GET',
441 441 renderer='rhodecode:templates/files/files_upload.mako')
442 442 config.add_view( # POST creates
443 443 RepoFilesView,
444 444 attr='repo_files_upload_file',
445 445 route_name='repo_files_upload_file', request_method='POST',
446 446 renderer='json_ext')
447 447
448 448 config.add_route(
449 name='repo_files_replace_binary',
450 pattern='/{repo_name:.*?[^/]}/replace_binary/{commit_id}/{f_path:.*}',
451 repo_route=True)
452 config.add_view(
453 RepoFilesView,
454 attr='repo_files_replace_file',
455 route_name='repo_files_replace_binary', request_method='POST',
456 renderer='json_ext')
457
458 config.add_route(
449 459 name='repo_files_create_file',
450 460 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
451 461 repo_route=True)
452 462 config.add_view( # POST creates
453 463 RepoFilesView,
454 464 attr='repo_files_create_file',
455 465 route_name='repo_files_create_file', request_method='POST',
456 466 renderer=None)
457 467
458 468 # Refs data
459 469 config.add_route(
460 470 name='repo_refs_data',
461 471 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
462 472 config.add_view(
463 473 RepoSummaryView,
464 474 attr='repo_refs_data',
465 475 route_name='repo_refs_data', request_method='GET',
466 476 renderer='json_ext')
467 477
468 478 config.add_route(
469 479 name='repo_refs_changelog_data',
470 480 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
471 481 config.add_view(
472 482 RepoSummaryView,
473 483 attr='repo_refs_changelog_data',
474 484 route_name='repo_refs_changelog_data', request_method='GET',
475 485 renderer='json_ext')
476 486
477 487 config.add_route(
478 488 name='repo_stats',
479 489 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
480 490 config.add_view(
481 491 RepoSummaryView,
482 492 attr='repo_stats',
483 493 route_name='repo_stats', request_method='GET',
484 494 renderer='json_ext')
485 495
486 496 # Commits
487 497 config.add_route(
488 498 name='repo_commits',
489 499 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
490 500 config.add_view(
491 501 RepoChangelogView,
492 502 attr='repo_changelog',
493 503 route_name='repo_commits', request_method='GET',
494 504 renderer='rhodecode:templates/commits/changelog.mako')
495 505 # old routes for backward compat
496 506 config.add_view(
497 507 RepoChangelogView,
498 508 attr='repo_changelog',
499 509 route_name='repo_changelog', request_method='GET',
500 510 renderer='rhodecode:templates/commits/changelog.mako')
501 511
502 512 config.add_route(
503 513 name='repo_commits_elements',
504 514 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
505 515 config.add_view(
506 516 RepoChangelogView,
507 517 attr='repo_commits_elements',
508 518 route_name='repo_commits_elements', request_method=('GET', 'POST'),
509 519 renderer='rhodecode:templates/commits/changelog_elements.mako',
510 520 xhr=True)
511 521
512 522 config.add_route(
513 523 name='repo_commits_elements_file',
514 524 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
515 525 config.add_view(
516 526 RepoChangelogView,
517 527 attr='repo_commits_elements',
518 528 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
519 529 renderer='rhodecode:templates/commits/changelog_elements.mako',
520 530 xhr=True)
521 531
522 532 config.add_route(
523 533 name='repo_commits_file',
524 534 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
525 535 config.add_view(
526 536 RepoChangelogView,
527 537 attr='repo_changelog',
528 538 route_name='repo_commits_file', request_method='GET',
529 539 renderer='rhodecode:templates/commits/changelog.mako')
530 540 # old routes for backward compat
531 541 config.add_view(
532 542 RepoChangelogView,
533 543 attr='repo_changelog',
534 544 route_name='repo_changelog_file', request_method='GET',
535 545 renderer='rhodecode:templates/commits/changelog.mako')
536 546
537 547 # Changelog (old deprecated name for commits page)
538 548 config.add_route(
539 549 name='repo_changelog',
540 550 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
541 551 config.add_route(
542 552 name='repo_changelog_file',
543 553 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
544 554
545 555 # Compare
546 556 config.add_route(
547 557 name='repo_compare_select',
548 558 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
549 559 config.add_view(
550 560 RepoCompareView,
551 561 attr='compare_select',
552 562 route_name='repo_compare_select', request_method='GET',
553 563 renderer='rhodecode:templates/compare/compare_diff.mako')
554 564
555 565 config.add_route(
556 566 name='repo_compare',
557 567 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
558 568 config.add_view(
559 569 RepoCompareView,
560 570 attr='compare',
561 571 route_name='repo_compare', request_method='GET',
562 572 renderer=None)
563 573
564 574 # Tags
565 575 config.add_route(
566 576 name='tags_home',
567 577 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
568 578 config.add_view(
569 579 RepoTagsView,
570 580 attr='tags',
571 581 route_name='tags_home', request_method='GET',
572 582 renderer='rhodecode:templates/tags/tags.mako')
573 583
574 584 # Branches
575 585 config.add_route(
576 586 name='branches_home',
577 587 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
578 588 config.add_view(
579 589 RepoBranchesView,
580 590 attr='branches',
581 591 route_name='branches_home', request_method='GET',
582 592 renderer='rhodecode:templates/branches/branches.mako')
583 593
584 594 # Bookmarks
585 595 config.add_route(
586 596 name='bookmarks_home',
587 597 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
588 598 config.add_view(
589 599 RepoBookmarksView,
590 600 attr='bookmarks',
591 601 route_name='bookmarks_home', request_method='GET',
592 602 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
593 603
594 604 # Forks
595 605 config.add_route(
596 606 name='repo_fork_new',
597 607 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
598 608 repo_forbid_when_archived=True,
599 609 repo_accepted_types=['hg', 'git'])
600 610 config.add_view(
601 611 RepoForksView,
602 612 attr='repo_fork_new',
603 613 route_name='repo_fork_new', request_method='GET',
604 614 renderer='rhodecode:templates/forks/forks.mako')
605 615
606 616 config.add_route(
607 617 name='repo_fork_create',
608 618 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
609 619 repo_forbid_when_archived=True,
610 620 repo_accepted_types=['hg', 'git'])
611 621 config.add_view(
612 622 RepoForksView,
613 623 attr='repo_fork_create',
614 624 route_name='repo_fork_create', request_method='POST',
615 625 renderer='rhodecode:templates/forks/fork.mako')
616 626
617 627 config.add_route(
618 628 name='repo_forks_show_all',
619 629 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
620 630 repo_accepted_types=['hg', 'git'])
621 631 config.add_view(
622 632 RepoForksView,
623 633 attr='repo_forks_show_all',
624 634 route_name='repo_forks_show_all', request_method='GET',
625 635 renderer='rhodecode:templates/forks/forks.mako')
626 636
627 637 config.add_route(
628 638 name='repo_forks_data',
629 639 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
630 640 repo_accepted_types=['hg', 'git'])
631 641 config.add_view(
632 642 RepoForksView,
633 643 attr='repo_forks_data',
634 644 route_name='repo_forks_data', request_method='GET',
635 645 renderer='json_ext', xhr=True)
636 646
637 647 # Pull Requests
638 648 config.add_route(
639 649 name='pullrequest_show',
640 650 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
641 651 repo_route=True)
642 652 config.add_view(
643 653 RepoPullRequestsView,
644 654 attr='pull_request_show',
645 655 route_name='pullrequest_show', request_method='GET',
646 656 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
647 657
648 658 config.add_route(
649 659 name='pullrequest_show_all',
650 660 pattern='/{repo_name:.*?[^/]}/pull-request',
651 661 repo_route=True, repo_accepted_types=['hg', 'git'])
652 662 config.add_view(
653 663 RepoPullRequestsView,
654 664 attr='pull_request_list',
655 665 route_name='pullrequest_show_all', request_method='GET',
656 666 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
657 667
658 668 config.add_route(
659 669 name='pullrequest_show_all_data',
660 670 pattern='/{repo_name:.*?[^/]}/pull-request-data',
661 671 repo_route=True, repo_accepted_types=['hg', 'git'])
662 672 config.add_view(
663 673 RepoPullRequestsView,
664 674 attr='pull_request_list_data',
665 675 route_name='pullrequest_show_all_data', request_method='GET',
666 676 renderer='json_ext', xhr=True)
667 677
668 678 config.add_route(
669 679 name='pullrequest_repo_refs',
670 680 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
671 681 repo_route=True)
672 682 config.add_view(
673 683 RepoPullRequestsView,
674 684 attr='pull_request_repo_refs',
675 685 route_name='pullrequest_repo_refs', request_method='GET',
676 686 renderer='json_ext', xhr=True)
677 687
678 688 config.add_route(
679 689 name='pullrequest_repo_targets',
680 690 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
681 691 repo_route=True)
682 692 config.add_view(
683 693 RepoPullRequestsView,
684 694 attr='pullrequest_repo_targets',
685 695 route_name='pullrequest_repo_targets', request_method='GET',
686 696 renderer='json_ext', xhr=True)
687 697
688 698 config.add_route(
689 699 name='pullrequest_new',
690 700 pattern='/{repo_name:.*?[^/]}/pull-request/new',
691 701 repo_route=True, repo_accepted_types=['hg', 'git'],
692 702 repo_forbid_when_archived=True)
693 703 config.add_view(
694 704 RepoPullRequestsView,
695 705 attr='pull_request_new',
696 706 route_name='pullrequest_new', request_method='GET',
697 707 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
698 708
699 709 config.add_route(
700 710 name='pullrequest_create',
701 711 pattern='/{repo_name:.*?[^/]}/pull-request/create',
702 712 repo_route=True, repo_accepted_types=['hg', 'git'],
703 713 repo_forbid_when_archived=True)
704 714 config.add_view(
705 715 RepoPullRequestsView,
706 716 attr='pull_request_create',
707 717 route_name='pullrequest_create', request_method='POST',
708 718 renderer=None)
709 719
710 720 config.add_route(
711 721 name='pullrequest_update',
712 722 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
713 723 repo_route=True, repo_forbid_when_archived=True)
714 724 config.add_view(
715 725 RepoPullRequestsView,
716 726 attr='pull_request_update',
717 727 route_name='pullrequest_update', request_method='POST',
718 728 renderer='json_ext')
719 729
720 730 config.add_route(
721 731 name='pullrequest_merge',
722 732 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
723 733 repo_route=True, repo_forbid_when_archived=True)
724 734 config.add_view(
725 735 RepoPullRequestsView,
726 736 attr='pull_request_merge',
727 737 route_name='pullrequest_merge', request_method='POST',
728 738 renderer='json_ext')
729 739
730 740 config.add_route(
731 741 name='pullrequest_delete',
732 742 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
733 743 repo_route=True, repo_forbid_when_archived=True)
734 744 config.add_view(
735 745 RepoPullRequestsView,
736 746 attr='pull_request_delete',
737 747 route_name='pullrequest_delete', request_method='POST',
738 748 renderer='json_ext')
739 749
740 750 config.add_route(
741 751 name='pullrequest_comment_create',
742 752 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
743 753 repo_route=True)
744 754 config.add_view(
745 755 RepoPullRequestsView,
746 756 attr='pull_request_comment_create',
747 757 route_name='pullrequest_comment_create', request_method='POST',
748 758 renderer='json_ext')
749 759
750 760 config.add_route(
751 761 name='pullrequest_comment_edit',
752 762 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
753 763 repo_route=True, repo_accepted_types=['hg', 'git'])
754 764 config.add_view(
755 765 RepoPullRequestsView,
756 766 attr='pull_request_comment_edit',
757 767 route_name='pullrequest_comment_edit', request_method='POST',
758 768 renderer='json_ext')
759 769
760 770 config.add_route(
761 771 name='pullrequest_comment_delete',
762 772 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
763 773 repo_route=True, repo_accepted_types=['hg', 'git'])
764 774 config.add_view(
765 775 RepoPullRequestsView,
766 776 attr='pull_request_comment_delete',
767 777 route_name='pullrequest_comment_delete', request_method='POST',
768 778 renderer='json_ext')
769 779
770 780 config.add_route(
771 781 name='pullrequest_comments',
772 782 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
773 783 repo_route=True)
774 784 config.add_view(
775 785 RepoPullRequestsView,
776 786 attr='pullrequest_comments',
777 787 route_name='pullrequest_comments', request_method='POST',
778 788 renderer='string_html', xhr=True)
779 789
780 790 config.add_route(
781 791 name='pullrequest_todos',
782 792 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
783 793 repo_route=True)
784 794 config.add_view(
785 795 RepoPullRequestsView,
786 796 attr='pullrequest_todos',
787 797 route_name='pullrequest_todos', request_method='POST',
788 798 renderer='string_html', xhr=True)
789 799
790 800 config.add_route(
791 801 name='pullrequest_drafts',
792 802 pattern=r'/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
793 803 repo_route=True)
794 804 config.add_view(
795 805 RepoPullRequestsView,
796 806 attr='pullrequest_drafts',
797 807 route_name='pullrequest_drafts', request_method='POST',
798 808 renderer='string_html', xhr=True)
799 809
800 810 # Artifacts, (EE feature)
801 811 config.add_route(
802 812 name='repo_artifacts_list',
803 813 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
804 814 config.add_view(
805 815 RepoArtifactsView,
806 816 attr='repo_artifacts',
807 817 route_name='repo_artifacts_list', request_method='GET',
808 818 renderer='rhodecode:templates/artifacts/artifact_list.mako')
809 819
810 820 # Settings
811 821 config.add_route(
812 822 name='edit_repo',
813 823 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
814 824 config.add_view(
815 825 RepoSettingsView,
816 826 attr='edit_settings',
817 827 route_name='edit_repo', request_method='GET',
818 828 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
819 829 # update is POST on edit_repo
820 830 config.add_view(
821 831 RepoSettingsView,
822 832 attr='edit_settings_update',
823 833 route_name='edit_repo', request_method='POST',
824 834 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
825 835
826 836 # Settings advanced
827 837 config.add_route(
828 838 name='edit_repo_advanced',
829 839 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
830 840 config.add_view(
831 841 RepoSettingsAdvancedView,
832 842 attr='edit_advanced',
833 843 route_name='edit_repo_advanced', request_method='GET',
834 844 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
835 845
836 846 config.add_route(
837 847 name='edit_repo_advanced_archive',
838 848 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
839 849 config.add_view(
840 850 RepoSettingsAdvancedView,
841 851 attr='edit_advanced_archive',
842 852 route_name='edit_repo_advanced_archive', request_method='POST',
843 853 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
844 854
845 855 config.add_route(
846 856 name='edit_repo_advanced_delete',
847 857 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
848 858 config.add_view(
849 859 RepoSettingsAdvancedView,
850 860 attr='edit_advanced_delete',
851 861 route_name='edit_repo_advanced_delete', request_method='POST',
852 862 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
853 863
854 864 config.add_route(
855 865 name='edit_repo_advanced_locking',
856 866 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
857 867 config.add_view(
858 868 RepoSettingsAdvancedView,
859 869 attr='edit_advanced_toggle_locking',
860 870 route_name='edit_repo_advanced_locking', request_method='POST',
861 871 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
862 872
863 873 config.add_route(
864 874 name='edit_repo_advanced_journal',
865 875 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
866 876 config.add_view(
867 877 RepoSettingsAdvancedView,
868 878 attr='edit_advanced_journal',
869 879 route_name='edit_repo_advanced_journal', request_method='POST',
870 880 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
871 881
872 882 config.add_route(
873 883 name='edit_repo_advanced_fork',
874 884 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
875 885 config.add_view(
876 886 RepoSettingsAdvancedView,
877 887 attr='edit_advanced_fork',
878 888 route_name='edit_repo_advanced_fork', request_method='POST',
879 889 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
880 890
881 891 config.add_route(
882 892 name='edit_repo_advanced_hooks',
883 893 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
884 894 config.add_view(
885 895 RepoSettingsAdvancedView,
886 896 attr='edit_advanced_install_hooks',
887 897 route_name='edit_repo_advanced_hooks', request_method='GET',
888 898 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
889 899
890 900 # Caches
891 901 config.add_route(
892 902 name='edit_repo_caches',
893 903 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
894 904 config.add_view(
895 905 RepoCachesView,
896 906 attr='repo_caches',
897 907 route_name='edit_repo_caches', request_method='GET',
898 908 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
899 909 config.add_view(
900 910 RepoCachesView,
901 911 attr='repo_caches_purge',
902 912 route_name='edit_repo_caches', request_method='POST')
903 913
904 914 # Permissions
905 915 config.add_route(
906 916 name='edit_repo_perms',
907 917 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
908 918 config.add_view(
909 919 RepoSettingsPermissionsView,
910 920 attr='edit_permissions',
911 921 route_name='edit_repo_perms', request_method='GET',
912 922 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
913 923 config.add_view(
914 924 RepoSettingsPermissionsView,
915 925 attr='edit_permissions_update',
916 926 route_name='edit_repo_perms', request_method='POST',
917 927 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
918 928
919 929 config.add_route(
920 930 name='edit_repo_perms_set_private',
921 931 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
922 932 config.add_view(
923 933 RepoSettingsPermissionsView,
924 934 attr='edit_permissions_set_private_repo',
925 935 route_name='edit_repo_perms_set_private', request_method='POST',
926 936 renderer='json_ext')
927 937
928 938 # Permissions Branch (EE feature)
929 939 config.add_route(
930 940 name='edit_repo_perms_branch',
931 941 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
932 942 config.add_view(
933 943 RepoSettingsBranchPermissionsView,
934 944 attr='branch_permissions',
935 945 route_name='edit_repo_perms_branch', request_method='GET',
936 946 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
937 947
938 948 config.add_route(
939 949 name='edit_repo_perms_branch_delete',
940 950 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
941 951 repo_route=True)
942 952 ## Only implemented in EE
943 953
944 954 # Maintenance
945 955 config.add_route(
946 956 name='edit_repo_maintenance',
947 957 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
948 958 config.add_view(
949 959 RepoMaintenanceView,
950 960 attr='repo_maintenance',
951 961 route_name='edit_repo_maintenance', request_method='GET',
952 962 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
953 963
954 964 config.add_route(
955 965 name='edit_repo_maintenance_execute',
956 966 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
957 967 config.add_view(
958 968 RepoMaintenanceView,
959 969 attr='repo_maintenance_execute',
960 970 route_name='edit_repo_maintenance_execute', request_method='GET',
961 971 renderer='json', xhr=True)
962 972
963 973 # Fields
964 974 config.add_route(
965 975 name='edit_repo_fields',
966 976 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
967 977 config.add_view(
968 978 RepoSettingsFieldsView,
969 979 attr='repo_field_edit',
970 980 route_name='edit_repo_fields', request_method='GET',
971 981 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
972 982
973 983 config.add_route(
974 984 name='edit_repo_fields_create',
975 985 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
976 986 config.add_view(
977 987 RepoSettingsFieldsView,
978 988 attr='repo_field_create',
979 989 route_name='edit_repo_fields_create', request_method='POST',
980 990 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
981 991
982 992 config.add_route(
983 993 name='edit_repo_fields_delete',
984 994 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
985 995 config.add_view(
986 996 RepoSettingsFieldsView,
987 997 attr='repo_field_delete',
988 998 route_name='edit_repo_fields_delete', request_method='POST',
989 999 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
990 1000
991 1001 # quick actions: locking
992 1002 config.add_route(
993 1003 name='repo_settings_quick_actions',
994 1004 pattern='/{repo_name:.*?[^/]}/settings/quick-action', repo_route=True)
995 1005 config.add_view(
996 1006 RepoSettingsView,
997 1007 attr='repo_settings_quick_actions',
998 1008 route_name='repo_settings_quick_actions', request_method='GET',
999 1009 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1000 1010
1001 1011 # Remote
1002 1012 config.add_route(
1003 1013 name='edit_repo_remote',
1004 1014 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1005 1015 config.add_view(
1006 1016 RepoSettingsRemoteView,
1007 1017 attr='repo_remote_edit_form',
1008 1018 route_name='edit_repo_remote', request_method='GET',
1009 1019 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1010 1020
1011 1021 config.add_route(
1012 1022 name='edit_repo_remote_pull',
1013 1023 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1014 1024 config.add_view(
1015 1025 RepoSettingsRemoteView,
1016 1026 attr='repo_remote_pull_changes',
1017 1027 route_name='edit_repo_remote_pull', request_method='POST',
1018 1028 renderer=None)
1019 1029
1020 1030 config.add_route(
1021 1031 name='edit_repo_remote_push',
1022 1032 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1023 1033
1024 1034 # Statistics
1025 1035 config.add_route(
1026 1036 name='edit_repo_statistics',
1027 1037 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1028 1038 config.add_view(
1029 1039 RepoSettingsView,
1030 1040 attr='edit_statistics_form',
1031 1041 route_name='edit_repo_statistics', request_method='GET',
1032 1042 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1033 1043
1034 1044 config.add_route(
1035 1045 name='edit_repo_statistics_reset',
1036 1046 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1037 1047 config.add_view(
1038 1048 RepoSettingsView,
1039 1049 attr='repo_statistics_reset',
1040 1050 route_name='edit_repo_statistics_reset', request_method='POST',
1041 1051 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1042 1052
1043 1053 # Issue trackers
1044 1054 config.add_route(
1045 1055 name='edit_repo_issuetracker',
1046 1056 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1047 1057 config.add_view(
1048 1058 RepoSettingsIssueTrackersView,
1049 1059 attr='repo_issuetracker',
1050 1060 route_name='edit_repo_issuetracker', request_method='GET',
1051 1061 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1052 1062
1053 1063 config.add_route(
1054 1064 name='edit_repo_issuetracker_test',
1055 1065 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1056 1066 config.add_view(
1057 1067 RepoSettingsIssueTrackersView,
1058 1068 attr='repo_issuetracker_test',
1059 1069 route_name='edit_repo_issuetracker_test', request_method='POST',
1060 1070 renderer='string', xhr=True)
1061 1071
1062 1072 config.add_route(
1063 1073 name='edit_repo_issuetracker_delete',
1064 1074 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1065 1075 config.add_view(
1066 1076 RepoSettingsIssueTrackersView,
1067 1077 attr='repo_issuetracker_delete',
1068 1078 route_name='edit_repo_issuetracker_delete', request_method='POST',
1069 1079 renderer='json_ext', xhr=True)
1070 1080
1071 1081 config.add_route(
1072 1082 name='edit_repo_issuetracker_update',
1073 1083 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1074 1084 config.add_view(
1075 1085 RepoSettingsIssueTrackersView,
1076 1086 attr='repo_issuetracker_update',
1077 1087 route_name='edit_repo_issuetracker_update', request_method='POST',
1078 1088 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1079 1089
1080 1090 # VCS Settings
1081 1091 config.add_route(
1082 1092 name='edit_repo_vcs',
1083 1093 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1084 1094 config.add_view(
1085 1095 RepoSettingsVcsView,
1086 1096 attr='repo_vcs_settings',
1087 1097 route_name='edit_repo_vcs', request_method='GET',
1088 1098 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1089 1099
1090 1100 config.add_route(
1091 1101 name='edit_repo_vcs_update',
1092 1102 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1093 1103 config.add_view(
1094 1104 RepoSettingsVcsView,
1095 1105 attr='repo_settings_vcs_update',
1096 1106 route_name='edit_repo_vcs_update', request_method='POST',
1097 1107 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1098 1108
1099 1109 # svn pattern
1100 1110 config.add_route(
1101 1111 name='edit_repo_vcs_svn_pattern_delete',
1102 1112 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1103 1113 config.add_view(
1104 1114 RepoSettingsVcsView,
1105 1115 attr='repo_settings_delete_svn_pattern',
1106 1116 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1107 1117 renderer='json_ext', xhr=True)
1108 1118
1109 1119 # Repo Review Rules (EE feature)
1110 1120 config.add_route(
1111 1121 name='repo_reviewers',
1112 1122 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1113 1123 config.add_view(
1114 1124 RepoReviewRulesView,
1115 1125 attr='repo_review_rules',
1116 1126 route_name='repo_reviewers', request_method='GET',
1117 1127 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1118 1128
1119 1129 config.add_route(
1120 1130 name='repo_default_reviewers_data',
1121 1131 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1122 1132 config.add_view(
1123 1133 RepoReviewRulesView,
1124 1134 attr='repo_default_reviewers_data',
1125 1135 route_name='repo_default_reviewers_data', request_method='GET',
1126 1136 renderer='json_ext')
1127 1137
1128 1138 # Repo Automation (EE feature)
1129 1139 config.add_route(
1130 1140 name='repo_automation',
1131 1141 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1132 1142 config.add_view(
1133 1143 RepoAutomationView,
1134 1144 attr='repo_automation',
1135 1145 route_name='repo_automation', request_method='GET',
1136 1146 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1137 1147
1138 1148 # Strip
1139 1149 config.add_route(
1140 1150 name='edit_repo_strip',
1141 1151 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1142 1152 config.add_view(
1143 1153 RepoStripView,
1144 1154 attr='strip',
1145 1155 route_name='edit_repo_strip', request_method='GET',
1146 1156 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1147 1157
1148 1158 config.add_route(
1149 1159 name='strip_check',
1150 1160 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1151 1161 config.add_view(
1152 1162 RepoStripView,
1153 1163 attr='strip_check',
1154 1164 route_name='strip_check', request_method='POST',
1155 1165 renderer='json', xhr=True)
1156 1166
1157 1167 config.add_route(
1158 1168 name='strip_execute',
1159 1169 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1160 1170 config.add_view(
1161 1171 RepoStripView,
1162 1172 attr='strip_execute',
1163 1173 route_name='strip_execute', request_method='POST',
1164 1174 renderer='json', xhr=True)
1165 1175
1166 1176 # Audit logs
1167 1177 config.add_route(
1168 1178 name='edit_repo_audit_logs',
1169 1179 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1170 1180 config.add_view(
1171 1181 AuditLogsView,
1172 1182 attr='repo_audit_logs',
1173 1183 route_name='edit_repo_audit_logs', request_method='GET',
1174 1184 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1175 1185
1176 1186 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1177 1187 config.add_route(
1178 1188 name='rss_feed_home',
1179 1189 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1180 1190 config.add_view(
1181 1191 RepoFeedView,
1182 1192 attr='rss',
1183 1193 route_name='rss_feed_home', request_method='GET', renderer=None)
1184 1194
1185 1195 config.add_route(
1186 1196 name='rss_feed_home_old',
1187 1197 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1188 1198 config.add_view(
1189 1199 RepoFeedView,
1190 1200 attr='rss',
1191 1201 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1192 1202
1193 1203 config.add_route(
1194 1204 name='atom_feed_home',
1195 1205 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1196 1206 config.add_view(
1197 1207 RepoFeedView,
1198 1208 attr='atom',
1199 1209 route_name='atom_feed_home', request_method='GET', renderer=None)
1200 1210
1201 1211 config.add_route(
1202 1212 name='atom_feed_home_old',
1203 1213 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1204 1214 config.add_view(
1205 1215 RepoFeedView,
1206 1216 attr='atom',
1207 1217 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1208 1218
1209 1219 # NOTE(marcink): needs to be at the end for catch-all
1210 1220 add_route_with_slash(
1211 1221 config,
1212 1222 name='repo_summary',
1213 1223 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1214 1224 config.add_view(
1215 1225 RepoSummaryView,
1216 1226 attr='summary',
1217 1227 route_name='repo_summary', request_method='GET',
1218 1228 renderer='rhodecode:templates/summary/summary.mako')
1219 1229
1220 1230 # TODO(marcink): there's no such route??
1221 1231 config.add_view(
1222 1232 RepoSummaryView,
1223 1233 attr='summary',
1224 1234 route_name='repo_summary_slash', request_method='GET',
1225 1235 renderer='rhodecode:templates/summary/summary.mako') No newline at end of file
@@ -1,1090 +1,1152 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21
22 22 import mock
23 23 import pytest
24 24 from collections import OrderedDict
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, get_archive_name, get_path_sha
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.str_utils import safe_str
31 31 from rhodecode.lib.vcs import nodes
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.model.db import Session, Repository
34 34
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 37 from rhodecode.tests.routes import route_path
38 38
39 39
40 40 fixture = Fixture()
41 41
42 42
43 43 def get_node_history(backend_type):
44 44 return {
45 45 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
46 46 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
47 47 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
48 48 }[backend_type]
49 49
50 50
51 51 def assert_files_in_response(response, files, params):
52 52 template = (
53 53 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
54 54 _assert_items_in_response(response, files, template, params)
55 55
56 56
57 57 def assert_dirs_in_response(response, dirs, params):
58 58 template = (
59 59 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
60 60 _assert_items_in_response(response, dirs, template, params)
61 61
62 62
63 63 def _assert_items_in_response(response, items, template, params):
64 64 for item in items:
65 65 item_params = {'name': item}
66 66 item_params.update(params)
67 67 response.mustcontain(template % item_params)
68 68
69 69
70 70 def assert_timeago_in_response(response, items, params):
71 71 for item in items:
72 72 response.mustcontain(h.age_component(params['date']))
73 73
74 74
75 75 @pytest.mark.usefixtures("app")
76 76 class TestFilesViews(object):
77 77
78 78 def test_show_files(self, backend):
79 79 response = self.app.get(
80 80 route_path('repo_files',
81 81 repo_name=backend.repo_name,
82 82 commit_id='tip', f_path='/'))
83 83 commit = backend.repo.get_commit()
84 84
85 85 params = {
86 86 'repo_name': backend.repo_name,
87 87 'commit_id': commit.raw_id,
88 88 'date': commit.date
89 89 }
90 90 assert_dirs_in_response(response, ['docs', 'vcs'], params)
91 91 files = [
92 92 '.gitignore',
93 93 '.hgignore',
94 94 '.hgtags',
95 95 # TODO: missing in Git
96 96 # '.travis.yml',
97 97 'MANIFEST.in',
98 98 'README.rst',
99 99 # TODO: File is missing in svn repository
100 100 # 'run_test_and_report.sh',
101 101 'setup.cfg',
102 102 'setup.py',
103 103 'test_and_report.sh',
104 104 'tox.ini',
105 105 ]
106 106 assert_files_in_response(response, files, params)
107 107 assert_timeago_in_response(response, files, params)
108 108
109 109 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
110 110 repo = backend_hg['subrepos']
111 111 response = self.app.get(
112 112 route_path('repo_files',
113 113 repo_name=repo.repo_name,
114 114 commit_id='tip', f_path='/'))
115 115 assert_response = response.assert_response()
116 116 assert_response.contains_one_link(
117 117 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
118 118
119 119 def test_show_files_links_submodules_with_absolute_url_subpaths(
120 120 self, backend_hg):
121 121 repo = backend_hg['subrepos']
122 122 response = self.app.get(
123 123 route_path('repo_files',
124 124 repo_name=repo.repo_name,
125 125 commit_id='tip', f_path='/'))
126 126 assert_response = response.assert_response()
127 127 assert_response.contains_one_link(
128 128 'subpaths-path @ 000000000000',
129 129 'http://sub-base.example.com/subpaths-path')
130 130
131 131 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
132 132 def test_files_menu(self, backend):
133 133 new_branch = "temp_branch_name"
134 134 commits = [
135 135 {'message': 'a'},
136 136 {'message': 'b', 'branch': new_branch}
137 137 ]
138 138 backend.create_repo(commits)
139 139 backend.repo.landing_rev = f"branch:{new_branch}"
140 140 Session().commit()
141 141
142 142 # get response based on tip and not new commit
143 143 response = self.app.get(
144 144 route_path('repo_files',
145 145 repo_name=backend.repo_name,
146 146 commit_id='tip', f_path='/'))
147 147
148 148 # make sure Files menu url is not tip but new commit
149 149 landing_rev = backend.repo.landing_ref_name
150 150 files_url = route_path('repo_files:default_path',
151 151 repo_name=backend.repo_name,
152 152 commit_id=landing_rev, params={'at': landing_rev})
153 153
154 154 assert landing_rev != 'tip'
155 155 response.mustcontain(f'<li class="active"><a class="menulink" href="{files_url}">')
156 156
157 157 def test_show_files_commit(self, backend):
158 158 commit = backend.repo.get_commit(commit_idx=32)
159 159
160 160 response = self.app.get(
161 161 route_path('repo_files',
162 162 repo_name=backend.repo_name,
163 163 commit_id=commit.raw_id, f_path='/'))
164 164
165 165 dirs = ['docs', 'tests']
166 166 files = ['README.rst']
167 167 params = {
168 168 'repo_name': backend.repo_name,
169 169 'commit_id': commit.raw_id,
170 170 }
171 171 assert_dirs_in_response(response, dirs, params)
172 172 assert_files_in_response(response, files, params)
173 173
174 174 def test_show_files_different_branch(self, backend):
175 175 branches = dict(
176 176 hg=(150, ['git']),
177 177 # TODO: Git test repository does not contain other branches
178 178 git=(633, ['master']),
179 179 # TODO: Branch support in Subversion
180 180 svn=(150, [])
181 181 )
182 182 idx, branches = branches[backend.alias]
183 183 commit = backend.repo.get_commit(commit_idx=idx)
184 184 response = self.app.get(
185 185 route_path('repo_files',
186 186 repo_name=backend.repo_name,
187 187 commit_id=commit.raw_id, f_path='/'))
188 188
189 189 assert_response = response.assert_response()
190 190 for branch in branches:
191 191 assert_response.element_contains('.tags .branchtag', branch)
192 192
193 193 def test_show_files_paging(self, backend):
194 194 repo = backend.repo
195 195 indexes = [73, 92, 109, 1, 0]
196 196 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
197 197 for rev in indexes]
198 198
199 199 for idx in idx_map:
200 200 response = self.app.get(
201 201 route_path('repo_files',
202 202 repo_name=backend.repo_name,
203 203 commit_id=idx[1], f_path='/'))
204 204
205 205 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
206 206
207 207 def test_file_source(self, backend):
208 208 commit = backend.repo.get_commit(commit_idx=167)
209 209 response = self.app.get(
210 210 route_path('repo_files',
211 211 repo_name=backend.repo_name,
212 212 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
213 213
214 214 msgbox = """<div class="commit">%s</div>"""
215 215 response.mustcontain(msgbox % (commit.message, ))
216 216
217 217 assert_response = response.assert_response()
218 218 if commit.branch:
219 219 assert_response.element_contains(
220 220 '.tags.tags-main .branchtag', commit.branch)
221 221 if commit.tags:
222 222 for tag in commit.tags:
223 223 assert_response.element_contains('.tags.tags-main .tagtag', tag)
224 224
225 225 def test_file_source_annotated(self, backend):
226 226 response = self.app.get(
227 227 route_path('repo_files:annotated',
228 228 repo_name=backend.repo_name,
229 229 commit_id='tip', f_path='vcs/nodes.py'))
230 230 expected_commits = {
231 231 'hg': 'r356',
232 232 'git': 'r345',
233 233 'svn': 'r208',
234 234 }
235 235 response.mustcontain(expected_commits[backend.alias])
236 236
237 237 def test_file_source_authors(self, backend):
238 238 response = self.app.get(
239 239 route_path('repo_file_authors',
240 240 repo_name=backend.repo_name,
241 241 commit_id='tip', f_path='vcs/nodes.py'))
242 242 expected_authors = {
243 243 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
244 244 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
245 245 'svn': ('marcin', 'lukasz'),
246 246 }
247 247
248 248 for author in expected_authors[backend.alias]:
249 249 response.mustcontain(author)
250 250
251 251 def test_file_source_authors_with_annotation(self, backend):
252 252 response = self.app.get(
253 253 route_path('repo_file_authors',
254 254 repo_name=backend.repo_name,
255 255 commit_id='tip', f_path='vcs/nodes.py',
256 256 params=dict(annotate=1)))
257 257 expected_authors = {
258 258 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
259 259 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
260 260 'svn': ('marcin', 'lukasz'),
261 261 }
262 262
263 263 for author in expected_authors[backend.alias]:
264 264 response.mustcontain(author)
265 265
266 266 def test_file_source_history(self, backend, xhr_header):
267 267 response = self.app.get(
268 268 route_path('repo_file_history',
269 269 repo_name=backend.repo_name,
270 270 commit_id='tip', f_path='vcs/nodes.py'),
271 271 extra_environ=xhr_header)
272 272 assert get_node_history(backend.alias) == json.loads(response.body)
273 273
274 274 def test_file_source_history_svn(self, backend_svn, xhr_header):
275 275 simple_repo = backend_svn['svn-simple-layout']
276 276 response = self.app.get(
277 277 route_path('repo_file_history',
278 278 repo_name=simple_repo.repo_name,
279 279 commit_id='tip', f_path='trunk/example.py'),
280 280 extra_environ=xhr_header)
281 281
282 282 expected_data = json.loads(
283 283 fixture.load_resource('svn_node_history_branches.json'))
284 284
285 285 assert expected_data == response.json
286 286
287 287 def test_file_source_history_with_annotation(self, backend, xhr_header):
288 288 response = self.app.get(
289 289 route_path('repo_file_history',
290 290 repo_name=backend.repo_name,
291 291 commit_id='tip', f_path='vcs/nodes.py',
292 292 params=dict(annotate=1)),
293 293
294 294 extra_environ=xhr_header)
295 295 assert get_node_history(backend.alias) == json.loads(response.body)
296 296
297 297 def test_tree_search_top_level(self, backend, xhr_header):
298 298 commit = backend.repo.get_commit(commit_idx=173)
299 299 response = self.app.get(
300 300 route_path('repo_files_nodelist',
301 301 repo_name=backend.repo_name,
302 302 commit_id=commit.raw_id, f_path='/'),
303 303 extra_environ=xhr_header)
304 304 assert 'nodes' in response.json
305 305 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
306 306
307 307 def test_tree_search_missing_xhr(self, backend):
308 308 self.app.get(
309 309 route_path('repo_files_nodelist',
310 310 repo_name=backend.repo_name,
311 311 commit_id='tip', f_path='/'),
312 312 status=404)
313 313
314 314 def test_tree_search_at_path(self, backend, xhr_header):
315 315 commit = backend.repo.get_commit(commit_idx=173)
316 316 response = self.app.get(
317 317 route_path('repo_files_nodelist',
318 318 repo_name=backend.repo_name,
319 319 commit_id=commit.raw_id, f_path='/docs'),
320 320 extra_environ=xhr_header)
321 321 assert 'nodes' in response.json
322 322 nodes = response.json['nodes']
323 323 assert {'name': 'docs/api', 'type': 'dir'} in nodes
324 324 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
325 325
326 326 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
327 327 commit = backend.repo.get_commit(commit_idx=173)
328 328 response = self.app.get(
329 329 route_path('repo_files_nodelist',
330 330 repo_name=backend.repo_name,
331 331 commit_id=commit.raw_id, f_path='/docs/api'),
332 332 extra_environ=xhr_header)
333 333 assert 'nodes' in response.json
334 334 nodes = response.json['nodes']
335 335 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
336 336
337 337 def test_tree_search_at_path_missing_xhr(self, backend):
338 338 self.app.get(
339 339 route_path('repo_files_nodelist',
340 340 repo_name=backend.repo_name,
341 341 commit_id='tip', f_path='/docs'),
342 342 status=404)
343 343
344 344 def test_nodetree(self, backend, xhr_header):
345 345 commit = backend.repo.get_commit(commit_idx=173)
346 346 response = self.app.get(
347 347 route_path('repo_nodetree_full',
348 348 repo_name=backend.repo_name,
349 349 commit_id=commit.raw_id, f_path='/'),
350 350 extra_environ=xhr_header)
351 351
352 352 assert_response = response.assert_response()
353 353
354 354 for attr in ['data-commit-id', 'data-date', 'data-author']:
355 355 elements = assert_response.get_elements('[{}]'.format(attr))
356 356 assert len(elements) > 1
357 357
358 358 for element in elements:
359 359 assert element.get(attr)
360 360
361 361 def test_nodetree_if_file(self, backend, xhr_header):
362 362 commit = backend.repo.get_commit(commit_idx=173)
363 363 response = self.app.get(
364 364 route_path('repo_nodetree_full',
365 365 repo_name=backend.repo_name,
366 366 commit_id=commit.raw_id, f_path='README.rst'),
367 367 extra_environ=xhr_header)
368 368 assert response.text == ''
369 369
370 370 def test_nodetree_wrong_path(self, backend, xhr_header):
371 371 commit = backend.repo.get_commit(commit_idx=173)
372 372 response = self.app.get(
373 373 route_path('repo_nodetree_full',
374 374 repo_name=backend.repo_name,
375 375 commit_id=commit.raw_id, f_path='/dont-exist'),
376 376 extra_environ=xhr_header)
377 377
378 378 err = 'error: There is no file nor ' \
379 379 'directory at the given path'
380 380 assert err in response.text
381 381
382 382 def test_nodetree_missing_xhr(self, backend):
383 383 self.app.get(
384 384 route_path('repo_nodetree_full',
385 385 repo_name=backend.repo_name,
386 386 commit_id='tip', f_path='/'),
387 387 status=404)
388 388
389 389
390 390 @pytest.mark.usefixtures("app", "autologin_user")
391 391 class TestRawFileHandling(object):
392 392
393 393 def test_download_file(self, backend):
394 394 commit = backend.repo.get_commit(commit_idx=173)
395 395 response = self.app.get(
396 396 route_path('repo_file_download',
397 397 repo_name=backend.repo_name,
398 398 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
399 399
400 400 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
401 401 assert response.content_type == "text/x-python"
402 402
403 403 def test_download_file_wrong_cs(self, backend):
404 404 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
405 405
406 406 response = self.app.get(
407 407 route_path('repo_file_download',
408 408 repo_name=backend.repo_name,
409 409 commit_id=raw_id, f_path='vcs/nodes.svg'),
410 410 status=404)
411 411
412 412 msg = """No such commit exists for this repository"""
413 413 response.mustcontain(msg)
414 414
415 415 def test_download_file_wrong_f_path(self, backend):
416 416 commit = backend.repo.get_commit(commit_idx=173)
417 417 f_path = 'vcs/ERRORnodes.py'
418 418
419 419 response = self.app.get(
420 420 route_path('repo_file_download',
421 421 repo_name=backend.repo_name,
422 422 commit_id=commit.raw_id, f_path=f_path),
423 423 status=404)
424 424
425 425 msg = (
426 426 "There is no file nor directory at the given path: "
427 427 "`%s` at commit %s" % (f_path, commit.short_id))
428 428 response.mustcontain(msg)
429 429
430 430 def test_file_raw(self, backend):
431 431 commit = backend.repo.get_commit(commit_idx=173)
432 432 response = self.app.get(
433 433 route_path('repo_file_raw',
434 434 repo_name=backend.repo_name,
435 435 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
436 436
437 437 assert response.content_type == "text/plain"
438 438
439 439 def test_file_raw_binary(self, backend):
440 440 commit = backend.repo.get_commit()
441 441 response = self.app.get(
442 442 route_path('repo_file_raw',
443 443 repo_name=backend.repo_name,
444 444 commit_id=commit.raw_id,
445 445 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
446 446
447 447 assert response.content_disposition == 'inline'
448 448
449 449 def test_raw_file_wrong_cs(self, backend):
450 450 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
451 451
452 452 response = self.app.get(
453 453 route_path('repo_file_raw',
454 454 repo_name=backend.repo_name,
455 455 commit_id=raw_id, f_path='vcs/nodes.svg'),
456 456 status=404)
457 457
458 458 msg = """No such commit exists for this repository"""
459 459 response.mustcontain(msg)
460 460
461 461 def test_raw_wrong_f_path(self, backend):
462 462 commit = backend.repo.get_commit(commit_idx=173)
463 463 f_path = 'vcs/ERRORnodes.py'
464 464 response = self.app.get(
465 465 route_path('repo_file_raw',
466 466 repo_name=backend.repo_name,
467 467 commit_id=commit.raw_id, f_path=f_path),
468 468 status=404)
469 469
470 470 msg = (
471 471 "There is no file nor directory at the given path: "
472 472 "`%s` at commit %s" % (f_path, commit.short_id))
473 473 response.mustcontain(msg)
474 474
475 475 def test_raw_svg_should_not_be_rendered(self, backend):
476 476 backend.create_repo()
477 477 backend.ensure_file(b"xss.svg")
478 478 response = self.app.get(
479 479 route_path('repo_file_raw',
480 480 repo_name=backend.repo_name,
481 481 commit_id='tip', f_path='xss.svg'),)
482 482 # If the content type is image/svg+xml then it allows to render HTML
483 483 # and malicious SVG.
484 484 assert response.content_type == "text/plain"
485 485
486 486
487 487 @pytest.mark.usefixtures("app")
488 488 class TestRepositoryArchival(object):
489 489
490 490 def test_archival(self, backend):
491 491 backend.enable_downloads()
492 492 commit = backend.repo.get_commit(commit_idx=173)
493 493
494 494 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
495 495 path_sha = get_path_sha('/')
496 496 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
497 497
498 498 fname = commit.raw_id + extension
499 499 response = self.app.get(
500 500 route_path('repo_archivefile',
501 501 repo_name=backend.repo_name,
502 502 fname=fname))
503 503
504 504 assert response.status == '200 OK'
505 505 headers = [
506 506 ('Content-Disposition', f'attachment; filename={filename}'),
507 507 ('Content-Type', content_type),
508 508 ]
509 509
510 510 for header in headers:
511 511 assert header in list(response.headers.items())
512 512
513 513 def test_archival_no_hash(self, backend):
514 514 backend.enable_downloads()
515 515 commit = backend.repo.get_commit(commit_idx=173)
516 516 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
517 517 path_sha = get_path_sha('/')
518 518 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
519 519
520 520 fname = commit.raw_id + extension
521 521 response = self.app.get(
522 522 route_path('repo_archivefile',
523 523 repo_name=backend.repo_name,
524 524 fname=fname, params={'with_hash': 0}))
525 525
526 526 assert response.status == '200 OK'
527 527 headers = [
528 528 ('Content-Disposition', f'attachment; filename={filename}'),
529 529 ('Content-Type', content_type),
530 530 ]
531 531
532 532 for header in headers:
533 533 assert header in list(response.headers.items())
534 534
535 535 def test_archival_at_path(self, backend):
536 536 backend.enable_downloads()
537 537 commit = backend.repo.get_commit(commit_idx=190)
538 538 at_path = 'vcs'
539 539
540 540 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
541 541 path_sha = get_path_sha(at_path)
542 542 filename = get_archive_name(backend.repo_id, backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
543 543
544 544 fname = commit.raw_id + extension
545 545 response = self.app.get(
546 546 route_path('repo_archivefile',
547 547 repo_name=backend.repo_name,
548 548 fname=fname, params={'at_path': at_path}))
549 549
550 550 assert response.status == '200 OK'
551 551 headers = [
552 552 ('Content-Disposition', f'attachment; filename={filename}'),
553 553 ('Content-Type', content_type),
554 554 ]
555 555
556 556 for header in headers:
557 557 assert header in list(response.headers.items())
558 558
559 559 @pytest.mark.parametrize('arch_ext',[
560 560 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
561 561 def test_archival_wrong_ext(self, backend, arch_ext):
562 562 backend.enable_downloads()
563 563 commit = backend.repo.get_commit(commit_idx=173)
564 564
565 565 fname = commit.raw_id + '.' + arch_ext
566 566
567 567 response = self.app.get(
568 568 route_path('repo_archivefile',
569 569 repo_name=backend.repo_name,
570 570 fname=fname))
571 571 response.mustcontain(
572 572 'Unknown archive type for: `{}`'.format(fname))
573 573
574 574 @pytest.mark.parametrize('commit_id', [
575 575 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
576 576 def test_archival_wrong_commit_id(self, backend, commit_id):
577 577 backend.enable_downloads()
578 578 fname = f'{commit_id}.zip'
579 579
580 580 response = self.app.get(
581 581 route_path('repo_archivefile',
582 582 repo_name=backend.repo_name,
583 583 fname=fname))
584 584 response.mustcontain('Unknown commit_id')
585 585
586 586
587 587 @pytest.mark.usefixtures("app")
588 588 class TestFilesDiff(object):
589 589
590 590 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
591 591 def test_file_full_diff(self, backend, diff):
592 592 commit1 = backend.repo.get_commit(commit_idx=-1)
593 593 commit2 = backend.repo.get_commit(commit_idx=-2)
594 594
595 595 response = self.app.get(
596 596 route_path('repo_files_diff',
597 597 repo_name=backend.repo_name,
598 598 f_path='README'),
599 599 params={
600 600 'diff1': commit2.raw_id,
601 601 'diff2': commit1.raw_id,
602 602 'fulldiff': '1',
603 603 'diff': diff,
604 604 })
605 605
606 606 if diff == 'diff':
607 607 # use redirect since this is OLD view redirecting to compare page
608 608 response = response.follow()
609 609
610 610 # It's a symlink to README.rst
611 611 response.mustcontain('README.rst')
612 612 response.mustcontain('No newline at end of file')
613 613
614 614 def test_file_binary_diff(self, backend):
615 615 commits = [
616 616 {'message': 'First commit'},
617 617 {'message': 'Commit with binary',
618 618 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
619 619 ]
620 620 repo = backend.create_repo(commits=commits)
621 621
622 622 response = self.app.get(
623 623 route_path('repo_files_diff',
624 624 repo_name=backend.repo_name,
625 625 f_path='file.bin'),
626 626 params={
627 627 'diff1': repo.get_commit(commit_idx=0).raw_id,
628 628 'diff2': repo.get_commit(commit_idx=1).raw_id,
629 629 'fulldiff': '1',
630 630 'diff': 'diff',
631 631 })
632 632 # use redirect since this is OLD view redirecting to compare page
633 633 response = response.follow()
634 634 response.mustcontain('Collapse 1 commit')
635 635 file_changes = (1, 0, 0)
636 636
637 637 compare_page = ComparePage(response)
638 638 compare_page.contains_change_summary(*file_changes)
639 639
640 640 if backend.alias == 'svn':
641 641 response.mustcontain('new file 10644')
642 642 # TODO(marcink): SVN doesn't yet detect binary changes
643 643 else:
644 644 response.mustcontain('new file 100644')
645 645 response.mustcontain('binary diff hidden')
646 646
647 647 def test_diff_2way(self, backend):
648 648 commit1 = backend.repo.get_commit(commit_idx=-1)
649 649 commit2 = backend.repo.get_commit(commit_idx=-2)
650 650 response = self.app.get(
651 651 route_path('repo_files_diff_2way_redirect',
652 652 repo_name=backend.repo_name,
653 653 f_path='README'),
654 654 params={
655 655 'diff1': commit2.raw_id,
656 656 'diff2': commit1.raw_id,
657 657 })
658 658 # use redirect since this is OLD view redirecting to compare page
659 659 response = response.follow()
660 660
661 661 # It's a symlink to README.rst
662 662 response.mustcontain('README.rst')
663 663 response.mustcontain('No newline at end of file')
664 664
665 665 def test_requires_one_commit_id(self, backend, autologin_user):
666 666 response = self.app.get(
667 667 route_path('repo_files_diff',
668 668 repo_name=backend.repo_name,
669 669 f_path='README.rst'),
670 670 status=400)
671 671 response.mustcontain(
672 672 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
673 673
674 674 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
675 675 repo = vcsbackend.repo
676 676 response = self.app.get(
677 677 route_path('repo_files_diff',
678 678 repo_name=repo.name,
679 679 f_path='does-not-exist-in-any-commit'),
680 680 params={
681 681 'diff1': repo[0].raw_id,
682 682 'diff2': repo[1].raw_id
683 683 })
684 684
685 685 response = response.follow()
686 686 response.mustcontain('No files')
687 687
688 688 def test_returns_redirect_if_file_not_changed(self, backend):
689 689 commit = backend.repo.get_commit(commit_idx=-1)
690 690 response = self.app.get(
691 691 route_path('repo_files_diff_2way_redirect',
692 692 repo_name=backend.repo_name,
693 693 f_path='README'),
694 694 params={
695 695 'diff1': commit.raw_id,
696 696 'diff2': commit.raw_id,
697 697 })
698 698
699 699 response = response.follow()
700 700 response.mustcontain('No files')
701 701 response.mustcontain('No commits in this compare')
702 702
703 703 def test_supports_diff_to_different_path_svn(self, backend_svn):
704 704 #TODO: check this case
705 705 return
706 706
707 707 repo = backend_svn['svn-simple-layout'].scm_instance()
708 708 commit_id_1 = '24'
709 709 commit_id_2 = '26'
710 710
711 711 response = self.app.get(
712 712 route_path('repo_files_diff',
713 713 repo_name=backend_svn.repo_name,
714 714 f_path='trunk/example.py'),
715 715 params={
716 716 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
717 717 'diff2': commit_id_2,
718 718 })
719 719
720 720 response = response.follow()
721 721 response.mustcontain(
722 722 # diff contains this
723 723 "Will print out a useful message on invocation.")
724 724
725 725 # Note: Expecting that we indicate the user what's being compared
726 726 response.mustcontain("trunk/example.py")
727 727 response.mustcontain("tags/v0.2/example.py")
728 728
729 729 def test_show_rev_redirects_to_svn_path(self, backend_svn):
730 730 #TODO: check this case
731 731 return
732 732
733 733 repo = backend_svn['svn-simple-layout'].scm_instance()
734 734 commit_id = repo[-1].raw_id
735 735
736 736 response = self.app.get(
737 737 route_path('repo_files_diff',
738 738 repo_name=backend_svn.repo_name,
739 739 f_path='trunk/example.py'),
740 740 params={
741 741 'diff1': 'branches/argparse/example.py@' + commit_id,
742 742 'diff2': commit_id,
743 743 },
744 744 status=302)
745 745 response = response.follow()
746 746 assert response.headers['Location'].endswith(
747 747 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
748 748
749 749 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
750 750 #TODO: check this case
751 751 return
752 752
753 753 repo = backend_svn['svn-simple-layout'].scm_instance()
754 754 commit_id = repo[-1].raw_id
755 755 response = self.app.get(
756 756 route_path('repo_files_diff',
757 757 repo_name=backend_svn.repo_name,
758 758 f_path='trunk/example.py'),
759 759 params={
760 760 'diff1': 'branches/argparse/example.py@' + commit_id,
761 761 'diff2': commit_id,
762 762 'show_rev': 'Show at Revision',
763 763 'annotate': 'true',
764 764 },
765 765 status=302)
766 766 response = response.follow()
767 767 assert response.headers['Location'].endswith(
768 768 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
769 769
770 770
771 771 @pytest.mark.usefixtures("app", "autologin_user")
772 772 class TestModifyFilesWithWebInterface(object):
773 773
774 774 def test_add_file_view(self, backend):
775 775 self.app.get(
776 776 route_path('repo_files_add_file',
777 777 repo_name=backend.repo_name,
778 778 commit_id='tip', f_path='/')
779 779 )
780 780
781 781 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
782 782 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
783 783 backend.create_repo()
784 784 filename = 'init.py'
785 785 response = self.app.post(
786 786 route_path('repo_files_create_file',
787 787 repo_name=backend.repo_name,
788 788 commit_id='tip', f_path='/'),
789 789 params={
790 790 'content': "",
791 791 'filename': filename,
792 792 'csrf_token': csrf_token,
793 793 },
794 794 status=302)
795 795 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
796 796 assert_session_flash(response, expected_msg)
797 797
798 798 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
799 799 commit_id = backend.repo.get_commit().raw_id
800 800 response = self.app.post(
801 801 route_path('repo_files_create_file',
802 802 repo_name=backend.repo_name,
803 803 commit_id=commit_id, f_path='/'),
804 804 params={
805 805 'content': "foo",
806 806 'csrf_token': csrf_token,
807 807 },
808 808 status=302)
809 809
810 810 assert_session_flash(response, 'No filename specified')
811 811
812 812 def test_add_file_into_repo_errors_and_no_commits(
813 813 self, backend, csrf_token):
814 814 repo = backend.create_repo()
815 815 # Create a file with no filename, it will display an error but
816 816 # the repo has no commits yet
817 817 response = self.app.post(
818 818 route_path('repo_files_create_file',
819 819 repo_name=repo.repo_name,
820 820 commit_id='tip', f_path='/'),
821 821 params={
822 822 'content': "foo",
823 823 'csrf_token': csrf_token,
824 824 },
825 825 status=302)
826 826
827 827 assert_session_flash(response, 'No filename specified')
828 828
829 829 # Not allowed, redirect to the summary
830 830 redirected = response.follow()
831 831 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
832 832
833 833 # As there are no commits, displays the summary page with the error of
834 834 # creating a file with no filename
835 835
836 836 assert redirected.request.path == summary_url
837 837
838 838 @pytest.mark.parametrize("filename, clean_filename", [
839 839 ('/abs/foo', 'abs/foo'),
840 840 ('../rel/foo', 'rel/foo'),
841 841 ('file/../foo/foo', 'file/foo/foo'),
842 842 ])
843 843 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
844 844 repo = backend.create_repo()
845 845 commit_id = repo.get_commit().raw_id
846 846
847 847 response = self.app.post(
848 848 route_path('repo_files_create_file',
849 849 repo_name=repo.repo_name,
850 850 commit_id=commit_id, f_path='/'),
851 851 params={
852 852 'content': "foo",
853 853 'filename': filename,
854 854 'csrf_token': csrf_token,
855 855 },
856 856 status=302)
857 857
858 858 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
859 859 assert_session_flash(response, expected_msg)
860 860
861 861 @pytest.mark.parametrize("cnt, filename, content", [
862 862 (1, 'foo.txt', "Content"),
863 863 (2, 'dir/foo.rst', "Content"),
864 864 (3, 'dir/foo-second.rst', "Content"),
865 865 (4, 'rel/dir/foo.bar', "Content"),
866 866 ])
867 867 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
868 868 repo = backend.create_repo()
869 869 commit_id = repo.get_commit().raw_id
870 870 response = self.app.post(
871 871 route_path('repo_files_create_file',
872 872 repo_name=repo.repo_name,
873 873 commit_id=commit_id, f_path='/'),
874 874 params={
875 875 'content': content,
876 876 'filename': filename,
877 877 'csrf_token': csrf_token,
878 878 },
879 879 status=302)
880 880
881 881 expected_msg = 'Successfully committed new file `{}`'.format(filename)
882 882 assert_session_flash(response, expected_msg)
883 883
884 884 def test_edit_file_view(self, backend):
885 885 response = self.app.get(
886 886 route_path('repo_files_edit_file',
887 887 repo_name=backend.repo_name,
888 888 commit_id=backend.default_head_id,
889 889 f_path='vcs/nodes.py'),
890 890 status=200)
891 891 response.mustcontain("Module holding everything related to vcs nodes.")
892 892
893 893 def test_edit_file_view_not_on_branch(self, backend):
894 894 repo = backend.create_repo()
895 895 backend.ensure_file(b"vcs/nodes.py")
896 896
897 897 response = self.app.get(
898 898 route_path('repo_files_edit_file',
899 899 repo_name=repo.repo_name,
900 900 commit_id='tip',
901 901 f_path='vcs/nodes.py'),
902 902 status=302)
903 903 assert_session_flash(
904 904 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
905 905
906 906 def test_edit_file_view_commit_changes(self, backend, csrf_token):
907 907 repo = backend.create_repo()
908 908 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
909 909
910 910 response = self.app.post(
911 911 route_path('repo_files_update_file',
912 912 repo_name=repo.repo_name,
913 913 commit_id=backend.default_head_id,
914 914 f_path='vcs/nodes.py'),
915 915 params={
916 916 'content': "print 'hello world'",
917 917 'message': 'I committed',
918 918 'filename': "vcs/nodes.py",
919 919 'csrf_token': csrf_token,
920 920 },
921 921 status=302)
922 922 assert_session_flash(
923 923 response, 'Successfully committed changes to file `vcs/nodes.py`')
924 924 tip = repo.get_commit(commit_idx=-1)
925 925 assert tip.message == 'I committed'
926 926
927 def test_replace_binary_file_view_commit_changes(self, backend, csrf_token):
928 repo = backend.create_repo()
929 backend.ensure_file(b"vcs/nodes.docx", content=b"PREVIOUS CONTENT'")
930
931 response = self.app.post(
932 route_path('repo_files_replace_binary',
933 repo_name=repo.repo_name,
934 commit_id=backend.default_head_id,
935 f_path='vcs/nodes.docx'),
936 params={
937 'message': 'I committed',
938 'csrf_token': csrf_token,
939 },
940 upload_files=[('files_upload', 'vcs/nodes.docx', b'SOME CONTENT')],
941 status=200)
942 assert_session_flash(
943 response, 'Successfully committed 1 new file')
944 tip = repo.get_commit(commit_idx=-1)
945 assert tip.message == 'I committed'
946
927 947 def test_edit_file_view_commit_changes_default_message(self, backend,
928 948 csrf_token):
929 949 repo = backend.create_repo()
930 950 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
931 951
932 952 commit_id = (
933 953 backend.default_branch_name or
934 954 backend.repo.scm_instance().commit_ids[-1])
935 955
936 956 response = self.app.post(
937 957 route_path('repo_files_update_file',
938 958 repo_name=repo.repo_name,
939 959 commit_id=commit_id,
940 960 f_path='vcs/nodes.py'),
941 961 params={
942 962 'content': "print 'hello world'",
943 963 'message': '',
944 964 'filename': "vcs/nodes.py",
945 965 'csrf_token': csrf_token,
946 966 },
947 967 status=302)
948 968 assert_session_flash(
949 969 response, 'Successfully committed changes to file `vcs/nodes.py`')
950 970 tip = repo.get_commit(commit_idx=-1)
951 971 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
952 972
973 def test_replace_binary_file_content_with_content_that_not_belong_to_original_type(self, backend, csrf_token):
974 repo = backend.create_repo()
975 backend.ensure_file(b"vcs/sheet.xlsx", content=b"PREVIOUS CONTENT'")
976
977 response = self.app.post(
978 route_path('repo_files_replace_binary',
979 repo_name=repo.repo_name,
980 commit_id=backend.default_head_id,
981 f_path='vcs/sheet.xlsx'),
982 params={
983 'message': 'I committed',
984 'csrf_token': csrf_token,
985 },
986 upload_files=[('files_upload', 'vcs/sheet.docx', b'SOME CONTENT')],
987 status=200)
988 assert response.json['error'] == "file extension of uploaded file doesn't match an original file's extension"
989
990 @pytest.mark.parametrize("replacement_files, expected_error", [
991 ([], 'missing files'),
992 (
993 [('files_upload', 'vcs/node1.docx', b'SOME CONTENT'),
994 ('files_upload', 'vcs/node2.docx', b'SOME CONTENT')],
995 'too many files for replacement'),
996 ])
997 def test_replace_binary_with_wrong_amount_of_content_sources(self, replacement_files, expected_error, backend,
998 csrf_token):
999 repo = backend.create_repo()
1000 backend.ensure_file(b"vcs/node.docx", content=b"PREVIOUS CONTENT'")
1001
1002 response = self.app.post(
1003 route_path('repo_files_replace_binary',
1004 repo_name=repo.repo_name,
1005 commit_id=backend.default_head_id,
1006 f_path='vcs/node.docx'),
1007 params={
1008 'message': 'I committed',
1009 'csrf_token': csrf_token,
1010 },
1011 upload_files=replacement_files,
1012 status=200)
1013 assert response.json['error'] == expected_error
1014
953 1015 def test_delete_file_view(self, backend):
954 1016 self.app.get(
955 1017 route_path('repo_files_remove_file',
956 1018 repo_name=backend.repo_name,
957 1019 commit_id=backend.default_head_id,
958 1020 f_path='vcs/nodes.py'),
959 1021 status=200)
960 1022
961 1023 def test_delete_file_view_not_on_branch(self, backend):
962 1024 repo = backend.create_repo()
963 1025 backend.ensure_file(b'vcs/nodes.py')
964 1026
965 1027 response = self.app.get(
966 1028 route_path('repo_files_remove_file',
967 1029 repo_name=repo.repo_name,
968 1030 commit_id='tip',
969 1031 f_path='vcs/nodes.py'),
970 1032 status=302)
971 1033 assert_session_flash(
972 1034 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
973 1035
974 1036 def test_delete_file_view_commit_changes(self, backend, csrf_token):
975 1037 repo = backend.create_repo()
976 1038 backend.ensure_file(b"vcs/nodes.py")
977 1039
978 1040 response = self.app.post(
979 1041 route_path('repo_files_delete_file',
980 1042 repo_name=repo.repo_name,
981 1043 commit_id=backend.default_head_id,
982 1044 f_path='vcs/nodes.py'),
983 1045 params={
984 1046 'message': 'i committed',
985 1047 'csrf_token': csrf_token,
986 1048 },
987 1049 status=302)
988 1050 assert_session_flash(
989 1051 response, 'Successfully deleted file `vcs/nodes.py`')
990 1052
991 1053
992 1054 @pytest.mark.usefixtures("app")
993 1055 class TestFilesViewOtherCases(object):
994 1056
995 1057 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
996 1058 self, backend_stub, autologin_regular_user, user_regular,
997 1059 user_util):
998 1060
999 1061 repo = backend_stub.create_repo()
1000 1062 user_util.grant_user_permission_to_repo(
1001 1063 repo, user_regular, 'repository.write')
1002 1064 response = self.app.get(
1003 1065 route_path('repo_files',
1004 1066 repo_name=repo.repo_name,
1005 1067 commit_id='tip', f_path='/'))
1006 1068
1007 1069 repo_file_add_url = route_path(
1008 1070 'repo_files_add_file',
1009 1071 repo_name=repo.repo_name,
1010 1072 commit_id=0, f_path='')
1011 1073 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1012 1074
1013 1075 repo_file_upload_url = route_path(
1014 1076 'repo_files_upload_file',
1015 1077 repo_name=repo.repo_name,
1016 1078 commit_id=0, f_path='')
1017 1079 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1018 1080
1019 1081 assert_session_flash(
1020 1082 response,
1021 1083 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1022 1084 )
1023 1085
1024 1086 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1025 1087 self, backend_stub, autologin_regular_user):
1026 1088 repo = backend_stub.create_repo()
1027 1089 # init session for anon user
1028 1090 route_path('repo_summary', repo_name=repo.repo_name)
1029 1091
1030 1092 repo_file_add_url = route_path(
1031 1093 'repo_files_add_file',
1032 1094 repo_name=repo.repo_name,
1033 1095 commit_id=0, f_path='')
1034 1096
1035 1097 response = self.app.get(
1036 1098 route_path('repo_files',
1037 1099 repo_name=repo.repo_name,
1038 1100 commit_id='tip', f_path='/'))
1039 1101
1040 1102 assert_session_flash(response, no_=repo_file_add_url)
1041 1103
1042 1104 @pytest.mark.parametrize('file_node', [
1043 1105 b'archive/file.zip',
1044 1106 b'diff/my-file.txt',
1045 1107 b'render.py',
1046 1108 b'render',
1047 1109 b'remove_file',
1048 1110 b'remove_file/to-delete.txt',
1049 1111 ])
1050 1112 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1051 1113 backend.create_repo()
1052 1114 backend.ensure_file(file_node)
1053 1115
1054 1116 self.app.get(
1055 1117 route_path('repo_files',
1056 1118 repo_name=backend.repo_name,
1057 1119 commit_id='tip', f_path=safe_str(file_node)),
1058 1120 status=200)
1059 1121
1060 1122
1061 1123 class TestAdjustFilePathForSvn(object):
1062 1124 """
1063 1125 SVN specific adjustments of node history in RepoFilesView.
1064 1126 """
1065 1127
1066 1128 def test_returns_path_relative_to_matched_reference(self):
1067 1129 repo = self._repo(branches=['trunk'])
1068 1130 self.assert_file_adjustment('trunk/file', 'file', repo)
1069 1131
1070 1132 def test_does_not_modify_file_if_no_reference_matches(self):
1071 1133 repo = self._repo(branches=['trunk'])
1072 1134 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1073 1135
1074 1136 def test_does_not_adjust_partial_directory_names(self):
1075 1137 repo = self._repo(branches=['trun'])
1076 1138 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1077 1139
1078 1140 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1079 1141 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1080 1142 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1081 1143
1082 1144 def assert_file_adjustment(self, f_path, expected, repo):
1083 1145 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1084 1146 assert result == expected
1085 1147
1086 1148 def _repo(self, branches=None):
1087 1149 repo = mock.Mock()
1088 1150 repo.branches = OrderedDict((name, '0') for name in branches or [])
1089 1151 repo.tags = {}
1090 1152 return repo
@@ -1,1586 +1,1708 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import os
22 22 import collections
23 23 import urllib.request
24 24 import urllib.parse
25 25 import urllib.error
26 26 import pathlib
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 import rhodecode
34 34 from rhodecode.apps._base import RepoAppView
35 35
36 36
37 37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.hash_utils import sha1_safe
40 40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
41 41 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
42 42 from rhodecode.lib.view_utils import parse_path_ref
43 43 from rhodecode.lib.exceptions import NonRelativePathError
44 44 from rhodecode.lib.codeblocks import (
45 45 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
46 46 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
47 47 from rhodecode.lib.type_utils import str2bool
48 48 from rhodecode.lib.str_utils import safe_str, safe_int
49 49 from rhodecode.lib.auth import (
50 50 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
51 51 from rhodecode.lib.vcs import path as vcspath
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit
53 53 from rhodecode.lib.vcs.conf import settings
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55 from rhodecode.lib.vcs.exceptions import (
56 56 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
57 57 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
58 58 NodeDoesNotExistError, CommitError, NodeError)
59 59
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.db import Repository
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
67 67 # original backward compat name of archive
68 68 clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_'))
69 69
70 70 # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip
71 71 # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
72 72 id_sha = sha1_safe(str(db_repo_id))[:4]
73 73 sub_repo = 'sub-1' if subrepos else 'sub-0'
74 74 commit = commit_sha if with_hash else 'archive'
75 75 path_marker = (path_sha if with_hash else '') or 'all'
76 76 archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}'
77 77
78 78 return archive_name
79 79
80 80
81 81 def get_path_sha(at_path):
82 82 return safe_str(sha1_safe(at_path)[:8])
83 83
84 84
85 85 def _get_archive_spec(fname):
86 86 log.debug('Detecting archive spec for: `%s`', fname)
87 87
88 88 fileformat = None
89 89 ext = None
90 90 content_type = None
91 91 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
92 92
93 93 if fname.endswith(extension):
94 94 fileformat = a_type
95 95 log.debug('archive is of type: %s', fileformat)
96 96 ext = extension
97 97 break
98 98
99 99 if not fileformat:
100 100 raise ValueError()
101 101
102 102 # left over part of whole fname is the commit
103 103 commit_id = fname[:-len(ext)]
104 104
105 105 return commit_id, ext, fileformat, content_type
106 106
107 107
108 108 class RepoFilesView(RepoAppView):
109 109
110 110 @staticmethod
111 111 def adjust_file_path_for_svn(f_path, repo):
112 112 """
113 113 Computes the relative path of `f_path`.
114 114
115 115 This is mainly based on prefix matching of the recognized tags and
116 116 branches in the underlying repository.
117 117 """
118 118 tags_and_branches = itertools.chain(
119 119 repo.branches.keys(),
120 120 repo.tags.keys())
121 121 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
122 122
123 123 for name in tags_and_branches:
124 124 if f_path.startswith(f'{name}/'):
125 125 f_path = vcspath.relpath(f_path, name)
126 126 break
127 127 return f_path
128 128
129 129 def load_default_context(self):
130 130 c = self._get_local_tmpl_context(include_app_defaults=True)
131 131 c.rhodecode_repo = self.rhodecode_vcs_repo
132 132 c.enable_downloads = self.db_repo.enable_downloads
133 133 return c
134 134
135 135 def _ensure_not_locked(self, commit_id='tip'):
136 136 _ = self.request.translate
137 137
138 138 repo = self.db_repo
139 139 if repo.enable_locking and repo.locked[0]:
140 140 h.flash(_('This repository has been locked by %s on %s')
141 141 % (h.person_by_id(repo.locked[0]),
142 142 h.format_date(h.time_to_datetime(repo.locked[1]))),
143 143 'warning')
144 144 files_url = h.route_path(
145 145 'repo_files:default_path',
146 146 repo_name=self.db_repo_name, commit_id=commit_id)
147 147 raise HTTPFound(files_url)
148 148
149 149 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
150 150 _ = self.request.translate
151 151
152 152 if not is_head:
153 153 message = _('Cannot modify file. '
154 154 'Given commit `{}` is not head of a branch.').format(commit_id)
155 155 h.flash(message, category='warning')
156 156
157 157 if json_mode:
158 158 return message
159 159
160 160 files_url = h.route_path(
161 161 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
162 162 f_path=f_path)
163 163 raise HTTPFound(files_url)
164 164
165 165 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
166 166 _ = self.request.translate
167 167
168 168 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
169 169 self.db_repo_name, branch_name)
170 170 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
171 171 message = _('Branch `{}` changes forbidden by rule {}.').format(
172 172 h.escape(branch_name), h.escape(rule))
173 173 h.flash(message, 'warning')
174 174
175 175 if json_mode:
176 176 return message
177 177
178 178 files_url = h.route_path(
179 179 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
180 180
181 181 raise HTTPFound(files_url)
182 182
183 183 def _get_commit_and_path(self):
184 184 default_commit_id = self.db_repo.landing_ref_name
185 185 default_f_path = '/'
186 186
187 187 commit_id = self.request.matchdict.get(
188 188 'commit_id', default_commit_id)
189 189 f_path = self._get_f_path(self.request.matchdict, default_f_path)
190 190 return commit_id, f_path
191 191
192 192 def _get_default_encoding(self, c):
193 193 enc_list = getattr(c, 'default_encodings', [])
194 194 return enc_list[0] if enc_list else 'UTF-8'
195 195
196 196 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
197 197 """
198 198 This is a safe way to get commit. If an error occurs it redirects to
199 199 tip with proper message
200 200
201 201 :param commit_id: id of commit to fetch
202 202 :param redirect_after: toggle redirection
203 203 """
204 204 _ = self.request.translate
205 205
206 206 try:
207 207 return self.rhodecode_vcs_repo.get_commit(commit_id)
208 208 except EmptyRepositoryError:
209 209 if not redirect_after:
210 210 return None
211 211
212 212 add_new = upload_new = ""
213 213 if h.HasRepoPermissionAny(
214 214 'repository.write', 'repository.admin')(self.db_repo_name):
215 215 _url = h.route_path(
216 216 'repo_files_add_file',
217 217 repo_name=self.db_repo_name, commit_id=0, f_path='')
218 218 add_new = h.link_to(
219 219 _('add a new file'), _url, class_="alert-link")
220 220
221 221 _url_upld = h.route_path(
222 222 'repo_files_upload_file',
223 223 repo_name=self.db_repo_name, commit_id=0, f_path='')
224 224 upload_new = h.link_to(
225 225 _('upload a new file'), _url_upld, class_="alert-link")
226 226
227 227 h.flash(h.literal(
228 228 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
229 229 raise HTTPFound(
230 230 h.route_path('repo_summary', repo_name=self.db_repo_name))
231 231
232 232 except (CommitDoesNotExistError, LookupError) as e:
233 233 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
234 234 h.flash(msg, category='error')
235 235 raise HTTPNotFound()
236 236 except RepositoryError as e:
237 237 h.flash(h.escape(safe_str(e)), category='error')
238 238 raise HTTPNotFound()
239 239
240 240 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
241 241 """
242 242 Returns file_node, if error occurs or given path is directory,
243 243 it'll redirect to top level path
244 244 """
245 245 _ = self.request.translate
246 246
247 247 try:
248 248 file_node = commit_obj.get_node(path, pre_load=pre_load)
249 249 if file_node.is_dir():
250 250 raise RepositoryError('The given path is a directory')
251 251 except CommitDoesNotExistError:
252 252 log.exception('No such commit exists for this repository')
253 253 h.flash(_('No such commit exists for this repository'), category='error')
254 254 raise HTTPNotFound()
255 255 except RepositoryError as e:
256 256 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
257 257 h.flash(h.escape(safe_str(e)), category='error')
258 258 raise HTTPNotFound()
259 259
260 260 return file_node
261 261
262 262 def _is_valid_head(self, commit_id, repo, landing_ref):
263 263 branch_name = sha_commit_id = ''
264 264 is_head = False
265 265 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
266 266
267 267 for _branch_name, branch_commit_id in repo.branches.items():
268 268 # simple case we pass in branch name, it's a HEAD
269 269 if commit_id == _branch_name:
270 270 is_head = True
271 271 branch_name = _branch_name
272 272 sha_commit_id = branch_commit_id
273 273 break
274 274 # case when we pass in full sha commit_id, which is a head
275 275 elif commit_id == branch_commit_id:
276 276 is_head = True
277 277 branch_name = _branch_name
278 278 sha_commit_id = branch_commit_id
279 279 break
280 280
281 281 if h.is_svn(repo) and not repo.is_empty():
282 282 # Note: Subversion only has one head.
283 283 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
284 284 is_head = True
285 285 return branch_name, sha_commit_id, is_head
286 286
287 287 # checked branches, means we only need to try to get the branch/commit_sha
288 288 if repo.is_empty():
289 289 is_head = True
290 290 branch_name = landing_ref
291 291 sha_commit_id = EmptyCommit().raw_id
292 292 else:
293 293 commit = repo.get_commit(commit_id=commit_id)
294 294 if commit:
295 295 branch_name = commit.branch
296 296 sha_commit_id = commit.raw_id
297 297
298 298 return branch_name, sha_commit_id, is_head
299 299
300 300 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
301 301
302 302 repo_id = self.db_repo.repo_id
303 303 force_recache = self.get_recache_flag()
304 304
305 305 cache_seconds = safe_int(
306 306 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
307 307 cache_on = not force_recache and cache_seconds > 0
308 308 log.debug(
309 309 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
310 310 'with caching: %s[TTL: %ss]' % (
311 311 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
312 312
313 313 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
314 314 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
315 315
316 316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
317 317 def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
318 318 log.debug('Generating cached file tree at for repo_id: %s, %s, %s',
319 319 _repo_id, _commit_id, _f_path)
320 320
321 321 c.full_load = _full_load
322 322 return render(
323 323 'rhodecode:templates/files/files_browser_tree.mako',
324 324 self._get_template_context(c), self.request, _at_rev)
325 325
326 326 return compute_file_tree(
327 327 self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
328 328
329 329 def create_pure_path(self, *parts):
330 330 # Split paths and sanitize them, removing any ../ etc
331 331 sanitized_path = [
332 332 x for x in pathlib.PurePath(*parts).parts
333 333 if x not in ['.', '..']]
334 334
335 335 pure_path = pathlib.PurePath(*sanitized_path)
336 336 return pure_path
337 337
338 338 def _is_lf_enabled(self, target_repo):
339 339 lf_enabled = False
340 340
341 341 lf_key_for_vcs_map = {
342 342 'hg': 'extensions_largefiles',
343 343 'git': 'vcs_git_lfs_enabled'
344 344 }
345 345
346 346 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
347 347
348 348 if lf_key_for_vcs:
349 349 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
350 350
351 351 return lf_enabled
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 def repo_archivefile(self):
357 357 # archive cache config
358 358 from rhodecode import CONFIG
359 359 _ = self.request.translate
360 360 self.load_default_context()
361 361 default_at_path = '/'
362 362 fname = self.request.matchdict['fname']
363 363 subrepos = self.request.GET.get('subrepos') == 'true'
364 364 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
365 365 at_path = self.request.GET.get('at_path') or default_at_path
366 366
367 367 if not self.db_repo.enable_downloads:
368 368 return Response(_('Downloads disabled'))
369 369
370 370 try:
371 371 commit_id, ext, fileformat, content_type = \
372 372 _get_archive_spec(fname)
373 373 except ValueError:
374 374 return Response(_('Unknown archive type for: `{}`').format(
375 375 h.escape(fname)))
376 376
377 377 try:
378 378 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
379 379 except CommitDoesNotExistError:
380 380 return Response(_('Unknown commit_id {}').format(
381 381 h.escape(commit_id)))
382 382 except EmptyRepositoryError:
383 383 return Response(_('Empty repository'))
384 384
385 385 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
386 386 if commit_id != commit.raw_id:
387 387 fname=f'{commit.raw_id}{ext}'
388 388 raise HTTPFound(self.request.current_route_path(fname=fname))
389 389
390 390 try:
391 391 at_path = commit.get_node(at_path).path or default_at_path
392 392 except Exception:
393 393 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
394 394
395 395 path_sha = get_path_sha(at_path)
396 396
397 397 # used for cache etc, consistent unique archive name
398 398 archive_name_key = get_archive_name(
399 399 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
400 400 path_sha=path_sha, with_hash=True)
401 401
402 402 if not with_hash:
403 403 path_sha = ''
404 404
405 405 # what end client gets served
406 406 response_archive_name = get_archive_name(
407 407 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
408 408 path_sha=path_sha, with_hash=with_hash)
409 409
410 410 # remove extension from our archive directory name
411 411 archive_dir_name = response_archive_name[:-len(ext)]
412 412
413 413 archive_cache_disable = self.request.GET.get('no_cache')
414 414
415 415 d_cache = get_archival_cache_store(config=CONFIG)
416 416
417 417 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
418 418 d_cache_conf = get_archival_config(config=CONFIG)
419 419
420 420 reentrant_lock_key = archive_name_key + '.lock'
421 421 with ReentrantLock(d_cache, reentrant_lock_key):
422 422 # This is also a cache key
423 423 use_cached_archive = False
424 424 if archive_name_key in d_cache and not archive_cache_disable:
425 425 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
426 426 use_cached_archive = True
427 427 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
428 428 archive_name_key, tag, reader.name)
429 429 else:
430 430 reader = None
431 431 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
432 432
433 433 # generate new archive, as previous was not found in the cache
434 434 if not reader:
435 435
436 436 try:
437 437 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
438 438 kind=fileformat, subrepos=subrepos,
439 439 archive_at_path=at_path, cache_config=d_cache_conf)
440 440 except ImproperArchiveTypeError:
441 441 return _('Unknown archive type')
442 442
443 443 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
444 444
445 445 if not reader:
446 446 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
447 447
448 448 def archive_iterator(_reader, block_size: int = 4096*512):
449 449 # 4096 * 64 = 64KB
450 450 while 1:
451 451 data = _reader.read(block_size)
452 452 if not data:
453 453 break
454 454 yield data
455 455
456 456 response = Response(app_iter=archive_iterator(reader))
457 457 response.content_disposition = f'attachment; filename={response_archive_name}'
458 458 response.content_type = str(content_type)
459 459
460 460 try:
461 461 return response
462 462 finally:
463 463 # store download action
464 464 audit_logger.store_web(
465 465 'repo.archive.download', action_data={
466 466 'user_agent': self.request.user_agent,
467 467 'archive_name': archive_name_key,
468 468 'archive_spec': fname,
469 469 'archive_cached': use_cached_archive},
470 470 user=self._rhodecode_user,
471 471 repo=self.db_repo,
472 472 commit=True
473 473 )
474 474
475 475 def _get_file_node(self, commit_id, f_path):
476 476 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
477 477 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
478 478 try:
479 479 node = commit.get_node(f_path)
480 480 if node.is_dir():
481 481 raise NodeError(f'{node} path is a {type(node)} not a file')
482 482 except NodeDoesNotExistError:
483 483 commit = EmptyCommit(
484 484 commit_id=commit_id,
485 485 idx=commit.idx,
486 486 repo=commit.repository,
487 487 alias=commit.repository.alias,
488 488 message=commit.message,
489 489 author=commit.author,
490 490 date=commit.date)
491 491 node = FileNode(safe_bytes(f_path), b'', commit=commit)
492 492 else:
493 493 commit = EmptyCommit(
494 494 repo=self.rhodecode_vcs_repo,
495 495 alias=self.rhodecode_vcs_repo.alias)
496 496 node = FileNode(safe_bytes(f_path), b'', commit=commit)
497 497 return node
498 498
499 499 @LoginRequired()
500 500 @HasRepoPermissionAnyDecorator(
501 501 'repository.read', 'repository.write', 'repository.admin')
502 502 def repo_files_diff(self):
503 503 c = self.load_default_context()
504 504 f_path = self._get_f_path(self.request.matchdict)
505 505 diff1 = self.request.GET.get('diff1', '')
506 506 diff2 = self.request.GET.get('diff2', '')
507 507
508 508 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
509 509
510 510 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
511 511 line_context = self.request.GET.get('context', 3)
512 512
513 513 if not any((diff1, diff2)):
514 514 h.flash(
515 515 'Need query parameter "diff1" or "diff2" to generate a diff.',
516 516 category='error')
517 517 raise HTTPBadRequest()
518 518
519 519 c.action = self.request.GET.get('diff')
520 520 if c.action not in ['download', 'raw']:
521 521 compare_url = h.route_path(
522 522 'repo_compare',
523 523 repo_name=self.db_repo_name,
524 524 source_ref_type='rev',
525 525 source_ref=diff1,
526 526 target_repo=self.db_repo_name,
527 527 target_ref_type='rev',
528 528 target_ref=diff2,
529 529 _query=dict(f_path=f_path))
530 530 # redirect to new view if we render diff
531 531 raise HTTPFound(compare_url)
532 532
533 533 try:
534 534 node1 = self._get_file_node(diff1, path1)
535 535 node2 = self._get_file_node(diff2, f_path)
536 536 except (RepositoryError, NodeError):
537 537 log.exception("Exception while trying to get node from repository")
538 538 raise HTTPFound(
539 539 h.route_path('repo_files', repo_name=self.db_repo_name,
540 540 commit_id='tip', f_path=f_path))
541 541
542 542 if all(isinstance(node.commit, EmptyCommit)
543 543 for node in (node1, node2)):
544 544 raise HTTPNotFound()
545 545
546 546 c.commit_1 = node1.commit
547 547 c.commit_2 = node2.commit
548 548
549 549 if c.action == 'download':
550 550 _diff = diffs.get_gitdiff(node1, node2,
551 551 ignore_whitespace=ignore_whitespace,
552 552 context=line_context)
553 553 # NOTE: this was using diff_format='gitdiff'
554 554 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
555 555
556 556 response = Response(self.path_filter.get_raw_patch(diff))
557 557 response.content_type = 'text/plain'
558 558 response.content_disposition = (
559 559 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
560 560 )
561 561 charset = self._get_default_encoding(c)
562 562 if charset:
563 563 response.charset = charset
564 564 return response
565 565
566 566 elif c.action == 'raw':
567 567 _diff = diffs.get_gitdiff(node1, node2,
568 568 ignore_whitespace=ignore_whitespace,
569 569 context=line_context)
570 570 # NOTE: this was using diff_format='gitdiff'
571 571 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
572 572
573 573 response = Response(self.path_filter.get_raw_patch(diff))
574 574 response.content_type = 'text/plain'
575 575 charset = self._get_default_encoding(c)
576 576 if charset:
577 577 response.charset = charset
578 578 return response
579 579
580 580 # in case we ever end up here
581 581 raise HTTPNotFound()
582 582
583 583 @LoginRequired()
584 584 @HasRepoPermissionAnyDecorator(
585 585 'repository.read', 'repository.write', 'repository.admin')
586 586 def repo_files_diff_2way_redirect(self):
587 587 """
588 588 Kept only to make OLD links work
589 589 """
590 590 f_path = self._get_f_path_unchecked(self.request.matchdict)
591 591 diff1 = self.request.GET.get('diff1', '')
592 592 diff2 = self.request.GET.get('diff2', '')
593 593
594 594 if not any((diff1, diff2)):
595 595 h.flash(
596 596 'Need query parameter "diff1" or "diff2" to generate a diff.',
597 597 category='error')
598 598 raise HTTPBadRequest()
599 599
600 600 compare_url = h.route_path(
601 601 'repo_compare',
602 602 repo_name=self.db_repo_name,
603 603 source_ref_type='rev',
604 604 source_ref=diff1,
605 605 target_ref_type='rev',
606 606 target_ref=diff2,
607 607 _query=dict(f_path=f_path, diffmode='sideside',
608 608 target_repo=self.db_repo_name,))
609 609 raise HTTPFound(compare_url)
610 610
611 611 @LoginRequired()
612 612 def repo_files_default_commit_redirect(self):
613 613 """
614 614 Special page that redirects to the landing page of files based on the default
615 615 commit for repository
616 616 """
617 617 c = self.load_default_context()
618 618 ref_name = c.rhodecode_db_repo.landing_ref_name
619 619 landing_url = h.repo_files_by_ref_url(
620 620 c.rhodecode_db_repo.repo_name,
621 621 c.rhodecode_db_repo.repo_type,
622 622 f_path='',
623 623 ref_name=ref_name,
624 624 commit_id='tip',
625 625 query=dict(at=ref_name)
626 626 )
627 627
628 628 raise HTTPFound(landing_url)
629 629
630 630 @LoginRequired()
631 631 @HasRepoPermissionAnyDecorator(
632 632 'repository.read', 'repository.write', 'repository.admin')
633 633 def repo_files(self):
634 634 c = self.load_default_context()
635 635
636 636 view_name = getattr(self.request.matched_route, 'name', None)
637 637
638 638 c.annotate = view_name == 'repo_files:annotated'
639 639 # default is false, but .rst/.md files later are auto rendered, we can
640 640 # overwrite auto rendering by setting this GET flag
641 641 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
642 642
643 643 commit_id, f_path = self._get_commit_and_path()
644 644
645 645 c.commit = self._get_commit_or_redirect(commit_id)
646 646 c.branch = self.request.GET.get('branch', None)
647 647 c.f_path = f_path
648 648 at_rev = self.request.GET.get('at')
649 649
650 650 # files or dirs
651 651 try:
652 652 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
653 653
654 654 c.file_author = True
655 655 c.file_tree = ''
656 656
657 657 # prev link
658 658 try:
659 659 prev_commit = c.commit.prev(c.branch)
660 660 c.prev_commit = prev_commit
661 661 c.url_prev = h.route_path(
662 662 'repo_files', repo_name=self.db_repo_name,
663 663 commit_id=prev_commit.raw_id, f_path=f_path)
664 664 if c.branch:
665 665 c.url_prev += '?branch=%s' % c.branch
666 666 except (CommitDoesNotExistError, VCSError):
667 667 c.url_prev = '#'
668 668 c.prev_commit = EmptyCommit()
669 669
670 670 # next link
671 671 try:
672 672 next_commit = c.commit.next(c.branch)
673 673 c.next_commit = next_commit
674 674 c.url_next = h.route_path(
675 675 'repo_files', repo_name=self.db_repo_name,
676 676 commit_id=next_commit.raw_id, f_path=f_path)
677 677 if c.branch:
678 678 c.url_next += '?branch=%s' % c.branch
679 679 except (CommitDoesNotExistError, VCSError):
680 680 c.url_next = '#'
681 681 c.next_commit = EmptyCommit()
682 682
683 683 # load file content
684 684 if c.file.is_file():
685 685
686 686 c.lf_node = {}
687 687
688 688 has_lf_enabled = self._is_lf_enabled(self.db_repo)
689 689 if has_lf_enabled:
690 690 c.lf_node = c.file.get_largefile_node()
691 691
692 692 c.file_source_page = 'true'
693 693 c.file_last_commit = c.file.last_commit
694 694
695 695 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
696 696
697 697 if not (c.file_size_too_big or c.file.is_binary):
698 698 if c.annotate: # annotation has precedence over renderer
699 699 c.annotated_lines = filenode_as_annotated_lines_tokens(
700 700 c.file
701 701 )
702 702 else:
703 703 c.renderer = (
704 704 c.renderer and h.renderer_from_filename(c.file.path)
705 705 )
706 706 if not c.renderer:
707 707 c.lines = filenode_as_lines_tokens(c.file)
708 708
709 709 _branch_name, _sha_commit_id, is_head = \
710 710 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
711 711 landing_ref=self.db_repo.landing_ref_name)
712 712 c.on_branch_head = is_head
713 713
714 714 branch = c.commit.branch if (
715 715 c.commit.branch and '/' not in c.commit.branch) else None
716 716 c.branch_or_raw_id = branch or c.commit.raw_id
717 717 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
718 718
719 719 author = c.file_last_commit.author
720 720 c.authors = [[
721 721 h.email(author),
722 722 h.person(author, 'username_or_name_or_email'),
723 723 1
724 724 ]]
725 725
726 726 else: # load tree content at path
727 727 c.file_source_page = 'false'
728 728 c.authors = []
729 729 # this loads a simple tree without metadata to speed things up
730 730 # later via ajax we call repo_nodetree_full and fetch whole
731 731 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
732 732
733 733 c.readme_data, c.readme_file = \
734 734 self._get_readme_data(self.db_repo, c.visual.default_renderer,
735 735 c.commit.raw_id, f_path)
736 736
737 737 except RepositoryError as e:
738 738 h.flash(h.escape(safe_str(e)), category='error')
739 739 raise HTTPNotFound()
740 740
741 741 if self.request.environ.get('HTTP_X_PJAX'):
742 742 html = render('rhodecode:templates/files/files_pjax.mako',
743 743 self._get_template_context(c), self.request)
744 744 else:
745 745 html = render('rhodecode:templates/files/files.mako',
746 746 self._get_template_context(c), self.request)
747 747 return Response(html)
748 748
749 749 @HasRepoPermissionAnyDecorator(
750 750 'repository.read', 'repository.write', 'repository.admin')
751 751 def repo_files_annotated_previous(self):
752 752 self.load_default_context()
753 753
754 754 commit_id, f_path = self._get_commit_and_path()
755 755 commit = self._get_commit_or_redirect(commit_id)
756 756 prev_commit_id = commit.raw_id
757 757 line_anchor = self.request.GET.get('line_anchor')
758 758 is_file = False
759 759 try:
760 760 _file = commit.get_node(f_path)
761 761 is_file = _file.is_file()
762 762 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
763 763 pass
764 764
765 765 if is_file:
766 766 history = commit.get_path_history(f_path)
767 767 prev_commit_id = history[1].raw_id \
768 768 if len(history) > 1 else prev_commit_id
769 769 prev_url = h.route_path(
770 770 'repo_files:annotated', repo_name=self.db_repo_name,
771 771 commit_id=prev_commit_id, f_path=f_path,
772 772 _anchor=f'L{line_anchor}')
773 773
774 774 raise HTTPFound(prev_url)
775 775
776 776 @LoginRequired()
777 777 @HasRepoPermissionAnyDecorator(
778 778 'repository.read', 'repository.write', 'repository.admin')
779 779 def repo_nodetree_full(self):
780 780 """
781 781 Returns rendered html of file tree that contains commit date,
782 782 author, commit_id for the specified combination of
783 783 repo, commit_id and file path
784 784 """
785 785 c = self.load_default_context()
786 786
787 787 commit_id, f_path = self._get_commit_and_path()
788 788 commit = self._get_commit_or_redirect(commit_id)
789 789 try:
790 790 dir_node = commit.get_node(f_path)
791 791 except RepositoryError as e:
792 792 return Response(f'error: {h.escape(safe_str(e))}')
793 793
794 794 if dir_node.is_file():
795 795 return Response('')
796 796
797 797 c.file = dir_node
798 798 c.commit = commit
799 799 at_rev = self.request.GET.get('at')
800 800
801 801 html = self._get_tree_at_commit(
802 802 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
803 803
804 804 return Response(html)
805 805
806 806 def _get_attachement_headers(self, f_path):
807 807 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
808 808 safe_path = f_name.replace('"', '\\"')
809 809 encoded_path = urllib.parse.quote(f_name)
810 810
811 811 headers = "attachment; " \
812 812 "filename=\"{}\"; " \
813 813 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
814 814
815 815 return safe_bytes(headers).decode('latin-1', errors='replace')
816 816
817 817 @LoginRequired()
818 818 @HasRepoPermissionAnyDecorator(
819 819 'repository.read', 'repository.write', 'repository.admin')
820 820 def repo_file_raw(self):
821 821 """
822 822 Action for show as raw, some mimetypes are "rendered",
823 823 those include images, icons.
824 824 """
825 825 c = self.load_default_context()
826 826
827 827 commit_id, f_path = self._get_commit_and_path()
828 828 commit = self._get_commit_or_redirect(commit_id)
829 829 file_node = self._get_filenode_or_redirect(commit, f_path)
830 830
831 831 raw_mimetype_mapping = {
832 832 # map original mimetype to a mimetype used for "show as raw"
833 833 # you can also provide a content-disposition to override the
834 834 # default "attachment" disposition.
835 835 # orig_type: (new_type, new_dispo)
836 836
837 837 # show images inline:
838 838 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
839 839 # for example render an SVG with javascript inside or even render
840 840 # HTML.
841 841 'image/x-icon': ('image/x-icon', 'inline'),
842 842 'image/png': ('image/png', 'inline'),
843 843 'image/gif': ('image/gif', 'inline'),
844 844 'image/jpeg': ('image/jpeg', 'inline'),
845 845 'application/pdf': ('application/pdf', 'inline'),
846 846 }
847 847
848 848 mimetype = file_node.mimetype
849 849 try:
850 850 mimetype, disposition = raw_mimetype_mapping[mimetype]
851 851 except KeyError:
852 852 # we don't know anything special about this, handle it safely
853 853 if file_node.is_binary:
854 854 # do same as download raw for binary files
855 855 mimetype, disposition = 'application/octet-stream', 'attachment'
856 856 else:
857 857 # do not just use the original mimetype, but force text/plain,
858 858 # otherwise it would serve text/html and that might be unsafe.
859 859 # Note: underlying vcs library fakes text/plain mimetype if the
860 860 # mimetype can not be determined and it thinks it is not
861 861 # binary.This might lead to erroneous text display in some
862 862 # cases, but helps in other cases, like with text files
863 863 # without extension.
864 864 mimetype, disposition = 'text/plain', 'inline'
865 865
866 866 if disposition == 'attachment':
867 867 disposition = self._get_attachement_headers(f_path)
868 868
869 869 stream_content = file_node.stream_bytes()
870 870
871 871 response = Response(app_iter=stream_content)
872 872 response.content_disposition = disposition
873 873 response.content_type = mimetype
874 874
875 875 charset = self._get_default_encoding(c)
876 876 if charset:
877 877 response.charset = charset
878 878
879 879 return response
880 880
881 881 @LoginRequired()
882 882 @HasRepoPermissionAnyDecorator(
883 883 'repository.read', 'repository.write', 'repository.admin')
884 884 def repo_file_download(self):
885 885 c = self.load_default_context()
886 886
887 887 commit_id, f_path = self._get_commit_and_path()
888 888 commit = self._get_commit_or_redirect(commit_id)
889 889 file_node = self._get_filenode_or_redirect(commit, f_path)
890 890
891 891 if self.request.GET.get('lf'):
892 892 # only if lf get flag is passed, we download this file
893 893 # as LFS/Largefile
894 894 lf_node = file_node.get_largefile_node()
895 895 if lf_node:
896 896 # overwrite our pointer with the REAL large-file
897 897 file_node = lf_node
898 898
899 899 disposition = self._get_attachement_headers(f_path)
900 900
901 901 stream_content = file_node.stream_bytes()
902 902
903 903 response = Response(app_iter=stream_content)
904 904 response.content_disposition = disposition
905 905 response.content_type = file_node.mimetype
906 906
907 907 charset = self._get_default_encoding(c)
908 908 if charset:
909 909 response.charset = charset
910 910
911 911 return response
912 912
913 913 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
914 914
915 915 cache_seconds = safe_int(
916 916 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
917 917 cache_on = cache_seconds > 0
918 918 log.debug(
919 919 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
920 920 'with caching: %s[TTL: %ss]' % (
921 921 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
922 922
923 923 cache_namespace_uid = f'repo.{repo_id}'
924 924 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
925 925
926 926 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
927 927 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
928 928 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
929 929 _repo_id, commit_id, f_path)
930 930 try:
931 931 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
932 932 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
933 933 log.exception(safe_str(e))
934 934 h.flash(h.escape(safe_str(e)), category='error')
935 935 raise HTTPFound(h.route_path(
936 936 'repo_files', repo_name=self.db_repo_name,
937 937 commit_id='tip', f_path='/'))
938 938
939 939 return _d + _f
940 940
941 941 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
942 942 commit_id, f_path)
943 943 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
944 944
945 945 @LoginRequired()
946 946 @HasRepoPermissionAnyDecorator(
947 947 'repository.read', 'repository.write', 'repository.admin')
948 948 def repo_nodelist(self):
949 949 self.load_default_context()
950 950
951 951 commit_id, f_path = self._get_commit_and_path()
952 952 commit = self._get_commit_or_redirect(commit_id)
953 953
954 954 metadata = self._get_nodelist_at_commit(
955 955 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
956 956 return {'nodes': [x for x in metadata]}
957 957
958 958 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
959 959 items = []
960 960 for name, commit_id in branches_or_tags.items():
961 961 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
962 962 items.append((sym_ref, name, ref_type))
963 963 return items
964 964
965 965 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
966 966 return commit_id
967 967
968 968 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
969 969 return commit_id
970 970
971 971 # NOTE(dan): old code we used in "diff" mode compare
972 972 new_f_path = vcspath.join(name, f_path)
973 973 return f'{new_f_path}@{commit_id}'
974 974
975 975 def _get_node_history(self, commit_obj, f_path, commits=None):
976 976 """
977 977 get commit history for given node
978 978
979 979 :param commit_obj: commit to calculate history
980 980 :param f_path: path for node to calculate history for
981 981 :param commits: if passed don't calculate history and take
982 982 commits defined in this list
983 983 """
984 984 _ = self.request.translate
985 985
986 986 # calculate history based on tip
987 987 tip = self.rhodecode_vcs_repo.get_commit()
988 988 if commits is None:
989 989 pre_load = ["author", "branch"]
990 990 try:
991 991 commits = tip.get_path_history(f_path, pre_load=pre_load)
992 992 except (NodeDoesNotExistError, CommitError):
993 993 # this node is not present at tip!
994 994 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
995 995
996 996 history = []
997 997 commits_group = ([], _("Changesets"))
998 998 for commit in commits:
999 999 branch = ' (%s)' % commit.branch if commit.branch else ''
1000 1000 n_desc = f'r{commit.idx}:{commit.short_id}{branch}'
1001 1001 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1002 1002 history.append(commits_group)
1003 1003
1004 1004 symbolic_reference = self._symbolic_reference
1005 1005
1006 1006 if self.rhodecode_vcs_repo.alias == 'svn':
1007 1007 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1008 1008 f_path, self.rhodecode_vcs_repo)
1009 1009 if adjusted_f_path != f_path:
1010 1010 log.debug(
1011 1011 'Recognized svn tag or branch in file "%s", using svn '
1012 1012 'specific symbolic references', f_path)
1013 1013 f_path = adjusted_f_path
1014 1014 symbolic_reference = self._symbolic_reference_svn
1015 1015
1016 1016 branches = self._create_references(
1017 1017 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1018 1018 branches_group = (branches, _("Branches"))
1019 1019
1020 1020 tags = self._create_references(
1021 1021 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1022 1022 tags_group = (tags, _("Tags"))
1023 1023
1024 1024 history.append(branches_group)
1025 1025 history.append(tags_group)
1026 1026
1027 1027 return history, commits
1028 1028
1029 1029 @LoginRequired()
1030 1030 @HasRepoPermissionAnyDecorator(
1031 1031 'repository.read', 'repository.write', 'repository.admin')
1032 1032 def repo_file_history(self):
1033 1033 self.load_default_context()
1034 1034
1035 1035 commit_id, f_path = self._get_commit_and_path()
1036 1036 commit = self._get_commit_or_redirect(commit_id)
1037 1037 file_node = self._get_filenode_or_redirect(commit, f_path)
1038 1038
1039 1039 if file_node.is_file():
1040 1040 file_history, _hist = self._get_node_history(commit, f_path)
1041 1041
1042 1042 res = []
1043 1043 for section_items, section in file_history:
1044 1044 items = []
1045 1045 for obj_id, obj_text, obj_type in section_items:
1046 1046 at_rev = ''
1047 1047 if obj_type in ['branch', 'bookmark', 'tag']:
1048 1048 at_rev = obj_text
1049 1049 entry = {
1050 1050 'id': obj_id,
1051 1051 'text': obj_text,
1052 1052 'type': obj_type,
1053 1053 'at_rev': at_rev
1054 1054 }
1055 1055
1056 1056 items.append(entry)
1057 1057
1058 1058 res.append({
1059 1059 'text': section,
1060 1060 'children': items
1061 1061 })
1062 1062
1063 1063 data = {
1064 1064 'more': False,
1065 1065 'results': res
1066 1066 }
1067 1067 return data
1068 1068
1069 1069 log.warning('Cannot fetch history for directory')
1070 1070 raise HTTPBadRequest()
1071 1071
1072 1072 @LoginRequired()
1073 1073 @HasRepoPermissionAnyDecorator(
1074 1074 'repository.read', 'repository.write', 'repository.admin')
1075 1075 def repo_file_authors(self):
1076 1076 c = self.load_default_context()
1077 1077
1078 1078 commit_id, f_path = self._get_commit_and_path()
1079 1079 commit = self._get_commit_or_redirect(commit_id)
1080 1080 file_node = self._get_filenode_or_redirect(commit, f_path)
1081 1081
1082 1082 if not file_node.is_file():
1083 1083 raise HTTPBadRequest()
1084 1084
1085 1085 c.file_last_commit = file_node.last_commit
1086 1086 if self.request.GET.get('annotate') == '1':
1087 1087 # use _hist from annotation if annotation mode is on
1088 1088 commit_ids = {x[1] for x in file_node.annotate}
1089 1089 _hist = (
1090 1090 self.rhodecode_vcs_repo.get_commit(commit_id)
1091 1091 for commit_id in commit_ids)
1092 1092 else:
1093 1093 _f_history, _hist = self._get_node_history(commit, f_path)
1094 1094 c.file_author = False
1095 1095
1096 1096 unique = collections.OrderedDict()
1097 1097 for commit in _hist:
1098 1098 author = commit.author
1099 1099 if author not in unique:
1100 1100 unique[commit.author] = [
1101 1101 h.email(author),
1102 1102 h.person(author, 'username_or_name_or_email'),
1103 1103 1 # counter
1104 1104 ]
1105 1105
1106 1106 else:
1107 1107 # increase counter
1108 1108 unique[commit.author][2] += 1
1109 1109
1110 1110 c.authors = [val for val in unique.values()]
1111 1111
1112 1112 return self._get_template_context(c)
1113 1113
1114 1114 @LoginRequired()
1115 1115 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1116 1116 def repo_files_check_head(self):
1117 1117 self.load_default_context()
1118 1118
1119 1119 commit_id, f_path = self._get_commit_and_path()
1120 1120 _branch_name, _sha_commit_id, is_head = \
1121 1121 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1122 1122 landing_ref=self.db_repo.landing_ref_name)
1123 1123
1124 1124 new_path = self.request.POST.get('path')
1125 1125 operation = self.request.POST.get('operation')
1126 1126 path_exist = ''
1127 1127
1128 1128 if new_path and operation in ['create', 'upload']:
1129 1129 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1130 1130 try:
1131 1131 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1132 1132 # NOTE(dan): construct whole path without leading /
1133 1133 file_node = commit_obj.get_node(new_f_path)
1134 1134 if file_node is not None:
1135 1135 path_exist = new_f_path
1136 1136 except EmptyRepositoryError:
1137 1137 pass
1138 1138 except Exception:
1139 1139 pass
1140 1140
1141 1141 return {
1142 1142 'branch': _branch_name,
1143 1143 'sha': _sha_commit_id,
1144 1144 'is_head': is_head,
1145 1145 'path_exists': path_exist
1146 1146 }
1147 1147
1148 1148 @LoginRequired()
1149 1149 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1150 1150 def repo_files_remove_file(self):
1151 1151 _ = self.request.translate
1152 1152 c = self.load_default_context()
1153 1153 commit_id, f_path = self._get_commit_and_path()
1154 1154
1155 1155 self._ensure_not_locked()
1156 1156 _branch_name, _sha_commit_id, is_head = \
1157 1157 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1158 1158 landing_ref=self.db_repo.landing_ref_name)
1159 1159
1160 1160 self.forbid_non_head(is_head, f_path)
1161 1161 self.check_branch_permission(_branch_name)
1162 1162
1163 1163 c.commit = self._get_commit_or_redirect(commit_id)
1164 1164 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1165 1165
1166 1166 c.default_message = _(
1167 1167 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1168 1168 c.f_path = f_path
1169 1169
1170 1170 return self._get_template_context(c)
1171 1171
1172 1172 @LoginRequired()
1173 1173 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1174 1174 @CSRFRequired()
1175 1175 def repo_files_delete_file(self):
1176 1176 _ = self.request.translate
1177 1177
1178 1178 c = self.load_default_context()
1179 1179 commit_id, f_path = self._get_commit_and_path()
1180 1180
1181 1181 self._ensure_not_locked()
1182 1182 _branch_name, _sha_commit_id, is_head = \
1183 1183 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1184 1184 landing_ref=self.db_repo.landing_ref_name)
1185 1185
1186 1186 self.forbid_non_head(is_head, f_path)
1187 1187 self.check_branch_permission(_branch_name)
1188 1188
1189 1189 c.commit = self._get_commit_or_redirect(commit_id)
1190 1190 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1191 1191
1192 1192 c.default_message = _(
1193 1193 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1194 1194 c.f_path = f_path
1195 1195 node_path = f_path
1196 1196 author = self._rhodecode_db_user.full_contact
1197 1197 message = self.request.POST.get('message') or c.default_message
1198 1198 try:
1199 1199 nodes = {
1200 1200 safe_bytes(node_path): {
1201 1201 'content': b''
1202 1202 }
1203 1203 }
1204 1204 ScmModel().delete_nodes(
1205 1205 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1206 1206 message=message,
1207 1207 nodes=nodes,
1208 1208 parent_commit=c.commit,
1209 1209 author=author,
1210 1210 )
1211 1211
1212 1212 h.flash(
1213 1213 _('Successfully deleted file `{}`').format(
1214 1214 h.escape(f_path)), category='success')
1215 1215 except Exception:
1216 1216 log.exception('Error during commit operation')
1217 1217 h.flash(_('Error occurred during commit'), category='error')
1218 1218 raise HTTPFound(
1219 1219 h.route_path('repo_commit', repo_name=self.db_repo_name,
1220 1220 commit_id='tip'))
1221 1221
1222 1222 @LoginRequired()
1223 1223 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1224 1224 def repo_files_edit_file(self):
1225 1225 _ = self.request.translate
1226 1226 c = self.load_default_context()
1227 1227 commit_id, f_path = self._get_commit_and_path()
1228 1228
1229 1229 self._ensure_not_locked()
1230 1230 _branch_name, _sha_commit_id, is_head = \
1231 1231 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1232 1232 landing_ref=self.db_repo.landing_ref_name)
1233 1233
1234 1234 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1235 1235 self.check_branch_permission(_branch_name, commit_id=commit_id)
1236 1236
1237 1237 c.commit = self._get_commit_or_redirect(commit_id)
1238 1238 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1239 1239
1240 1240 if c.file.is_binary:
1241 1241 files_url = h.route_path(
1242 1242 'repo_files',
1243 1243 repo_name=self.db_repo_name,
1244 1244 commit_id=c.commit.raw_id, f_path=f_path)
1245 1245 raise HTTPFound(files_url)
1246 1246
1247 1247 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1248 1248 c.f_path = f_path
1249 1249
1250 1250 return self._get_template_context(c)
1251 1251
1252 1252 @LoginRequired()
1253 1253 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1254 1254 @CSRFRequired()
1255 1255 def repo_files_update_file(self):
1256 1256 _ = self.request.translate
1257 1257 c = self.load_default_context()
1258 1258 commit_id, f_path = self._get_commit_and_path()
1259 1259
1260 1260 self._ensure_not_locked()
1261 1261
1262 1262 c.commit = self._get_commit_or_redirect(commit_id)
1263 1263 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1264 1264
1265 1265 if c.file.is_binary:
1266 1266 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1267 1267 commit_id=c.commit.raw_id, f_path=f_path))
1268 1268
1269 1269 _branch_name, _sha_commit_id, is_head = \
1270 1270 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1271 1271 landing_ref=self.db_repo.landing_ref_name)
1272 1272
1273 1273 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1274 1274 self.check_branch_permission(_branch_name, commit_id=commit_id)
1275 1275
1276 1276 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1277 1277 c.f_path = f_path
1278 1278
1279 1279 old_content = c.file.str_content
1280 1280 sl = old_content.splitlines(1)
1281 1281 first_line = sl[0] if sl else ''
1282 1282
1283 1283 r_post = self.request.POST
1284 1284 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1285 1285 line_ending_mode = detect_mode(first_line, 0)
1286 1286 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1287 1287
1288 1288 message = r_post.get('message') or c.default_message
1289 1289
1290 1290 org_node_path = c.file.str_path
1291 1291 filename = r_post['filename']
1292 1292
1293 1293 root_path = c.file.dir_path
1294 1294 pure_path = self.create_pure_path(root_path, filename)
1295 1295 node_path = pure_path.as_posix()
1296 1296
1297 1297 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1298 1298 commit_id=commit_id)
1299 1299 if content == old_content and node_path == org_node_path:
1300 1300 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1301 1301 category='warning')
1302 1302 raise HTTPFound(default_redirect_url)
1303 1303
1304 1304 try:
1305 1305 mapping = {
1306 1306 c.file.bytes_path: {
1307 1307 'org_filename': org_node_path,
1308 1308 'filename': safe_bytes(node_path),
1309 1309 'content': safe_bytes(content),
1310 1310 'lexer': '',
1311 1311 'op': 'mod',
1312 1312 'mode': c.file.mode
1313 1313 }
1314 1314 }
1315 1315
1316 1316 commit = ScmModel().update_nodes(
1317 1317 user=self._rhodecode_db_user.user_id,
1318 1318 repo=self.db_repo,
1319 1319 message=message,
1320 1320 nodes=mapping,
1321 1321 parent_commit=c.commit,
1322 1322 )
1323 1323
1324 1324 h.flash(_('Successfully committed changes to file `{}`').format(
1325 1325 h.escape(f_path)), category='success')
1326 1326 default_redirect_url = h.route_path(
1327 1327 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1328 1328
1329 1329 except Exception:
1330 1330 log.exception('Error occurred during commit')
1331 1331 h.flash(_('Error occurred during commit'), category='error')
1332 1332
1333 1333 raise HTTPFound(default_redirect_url)
1334 1334
1335 1335 @LoginRequired()
1336 1336 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1337 1337 def repo_files_add_file(self):
1338 1338 _ = self.request.translate
1339 1339 c = self.load_default_context()
1340 1340 commit_id, f_path = self._get_commit_and_path()
1341 1341
1342 1342 self._ensure_not_locked()
1343 1343
1344 # Check if we need to use this page to upload binary
1345 upload_binary = str2bool(self.request.params.get('upload_binary', False))
1346
1344 1347 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1345 1348 if c.commit is None:
1346 1349 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1347 1350
1348 1351 if self.rhodecode_vcs_repo.is_empty():
1349 1352 # for empty repository we cannot check for current branch, we rely on
1350 1353 # c.commit.branch instead
1351 1354 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1352 1355 else:
1353 1356 _branch_name, _sha_commit_id, is_head = \
1354 1357 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1355 1358 landing_ref=self.db_repo.landing_ref_name)
1356 1359
1357 1360 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1358 1361 self.check_branch_permission(_branch_name, commit_id=commit_id)
1359 1362
1360 c.default_message = (_('Added file via RhodeCode Enterprise'))
1363 c.default_message = (_('Added file via RhodeCode Enterprise')) \
1364 if not upload_binary else (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1361 1365 c.f_path = f_path.lstrip('/') # ensure not relative path
1366 c.replace_binary = upload_binary
1362 1367
1363 1368 return self._get_template_context(c)
1364 1369
1365 1370 @LoginRequired()
1366 1371 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1367 1372 @CSRFRequired()
1368 1373 def repo_files_create_file(self):
1369 1374 _ = self.request.translate
1370 1375 c = self.load_default_context()
1371 1376 commit_id, f_path = self._get_commit_and_path()
1372 1377
1373 1378 self._ensure_not_locked()
1374 1379
1375 1380 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1376 1381 if c.commit is None:
1377 1382 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1378 1383
1379 1384 # calculate redirect URL
1380 1385 if self.rhodecode_vcs_repo.is_empty():
1381 1386 default_redirect_url = h.route_path(
1382 1387 'repo_summary', repo_name=self.db_repo_name)
1383 1388 else:
1384 1389 default_redirect_url = h.route_path(
1385 1390 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1386 1391
1387 1392 if self.rhodecode_vcs_repo.is_empty():
1388 1393 # for empty repository we cannot check for current branch, we rely on
1389 1394 # c.commit.branch instead
1390 1395 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1391 1396 else:
1392 1397 _branch_name, _sha_commit_id, is_head = \
1393 1398 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1394 1399 landing_ref=self.db_repo.landing_ref_name)
1395 1400
1396 1401 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1397 1402 self.check_branch_permission(_branch_name, commit_id=commit_id)
1398 1403
1399 1404 c.default_message = (_('Added file via RhodeCode Enterprise'))
1400 1405 c.f_path = f_path
1401 1406
1402 1407 r_post = self.request.POST
1403 1408 message = r_post.get('message') or c.default_message
1404 1409 filename = r_post.get('filename')
1405 1410 unix_mode = 0
1406 1411
1407 1412 if not filename:
1408 1413 # If there's no commit, redirect to repo summary
1409 1414 if type(c.commit) is EmptyCommit:
1410 1415 redirect_url = h.route_path(
1411 1416 'repo_summary', repo_name=self.db_repo_name)
1412 1417 else:
1413 1418 redirect_url = default_redirect_url
1414 1419 h.flash(_('No filename specified'), category='warning')
1415 1420 raise HTTPFound(redirect_url)
1416 1421
1417 1422 root_path = f_path
1418 1423 pure_path = self.create_pure_path(root_path, filename)
1419 1424 node_path = pure_path.as_posix().lstrip('/')
1420 1425
1421 1426 author = self._rhodecode_db_user.full_contact
1422 1427 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1423 1428 nodes = {
1424 1429 safe_bytes(node_path): {
1425 1430 'content': safe_bytes(content)
1426 1431 }
1427 1432 }
1428 1433
1429 1434 try:
1430 1435
1431 1436 commit = ScmModel().create_nodes(
1432 1437 user=self._rhodecode_db_user.user_id,
1433 1438 repo=self.db_repo,
1434 1439 message=message,
1435 1440 nodes=nodes,
1436 1441 parent_commit=c.commit,
1437 1442 author=author,
1438 1443 )
1439 1444
1440 1445 h.flash(_('Successfully committed new file `{}`').format(
1441 1446 h.escape(node_path)), category='success')
1442 1447
1443 1448 default_redirect_url = h.route_path(
1444 1449 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1445 1450
1446 1451 except NonRelativePathError:
1447 1452 log.exception('Non Relative path found')
1448 1453 h.flash(_('The location specified must be a relative path and must not '
1449 1454 'contain .. in the path'), category='warning')
1450 1455 raise HTTPFound(default_redirect_url)
1451 1456 except (NodeError, NodeAlreadyExistsError) as e:
1452 1457 h.flash(h.escape(safe_str(e)), category='error')
1453 1458 except Exception:
1454 1459 log.exception('Error occurred during commit')
1455 1460 h.flash(_('Error occurred during commit'), category='error')
1456 1461
1457 1462 raise HTTPFound(default_redirect_url)
1458 1463
1459 1464 @LoginRequired()
1460 1465 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1461 1466 @CSRFRequired()
1462 1467 def repo_files_upload_file(self):
1463 1468 _ = self.request.translate
1464 1469 c = self.load_default_context()
1465 1470 commit_id, f_path = self._get_commit_and_path()
1466 1471
1467 1472 self._ensure_not_locked()
1468 1473
1469 1474 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1470 1475 if c.commit is None:
1471 1476 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1472 1477
1473 1478 # calculate redirect URL
1474 1479 if self.rhodecode_vcs_repo.is_empty():
1475 1480 default_redirect_url = h.route_path(
1476 1481 'repo_summary', repo_name=self.db_repo_name)
1477 1482 else:
1478 1483 default_redirect_url = h.route_path(
1479 1484 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1480 1485
1481 1486 if self.rhodecode_vcs_repo.is_empty():
1482 1487 # for empty repository we cannot check for current branch, we rely on
1483 1488 # c.commit.branch instead
1484 1489 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1485 1490 else:
1486 1491 _branch_name, _sha_commit_id, is_head = \
1487 1492 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1488 1493 landing_ref=self.db_repo.landing_ref_name)
1489 1494
1490 1495 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1491 1496 if error:
1492 1497 return {
1493 1498 'error': error,
1494 1499 'redirect_url': default_redirect_url
1495 1500 }
1496 1501 error = self.check_branch_permission(_branch_name, json_mode=True)
1497 1502 if error:
1498 1503 return {
1499 1504 'error': error,
1500 1505 'redirect_url': default_redirect_url
1501 1506 }
1502 1507
1503 1508 c.default_message = (_('Added file via RhodeCode Enterprise'))
1504 1509 c.f_path = f_path
1505 1510
1506 1511 r_post = self.request.POST
1507 1512
1508 1513 message = c.default_message
1509 1514 user_message = r_post.getall('message')
1510 1515 if isinstance(user_message, list) and user_message:
1511 1516 # we take the first from duplicated results if it's not empty
1512 1517 message = user_message[0] if user_message[0] else message
1513 1518
1514 1519 nodes = {}
1515 1520
1516 1521 for file_obj in r_post.getall('files_upload') or []:
1517 1522 content = file_obj.file
1518 1523 filename = file_obj.filename
1519 1524
1520 1525 root_path = f_path
1521 1526 pure_path = self.create_pure_path(root_path, filename)
1522 1527 node_path = pure_path.as_posix().lstrip('/')
1523 1528
1524 1529 nodes[safe_bytes(node_path)] = {
1525 1530 'content': content
1526 1531 }
1527 1532
1528 1533 if not nodes:
1529 1534 error = 'missing files'
1530 1535 return {
1531 1536 'error': error,
1532 1537 'redirect_url': default_redirect_url
1533 1538 }
1534 1539
1535 1540 author = self._rhodecode_db_user.full_contact
1536 1541
1537 1542 try:
1538 1543 commit = ScmModel().create_nodes(
1539 1544 user=self._rhodecode_db_user.user_id,
1540 1545 repo=self.db_repo,
1541 1546 message=message,
1542 1547 nodes=nodes,
1543 1548 parent_commit=c.commit,
1544 1549 author=author,
1545 1550 )
1546 1551 if len(nodes) == 1:
1547 1552 flash_message = _('Successfully committed {} new files').format(len(nodes))
1548 1553 else:
1549 1554 flash_message = _('Successfully committed 1 new file')
1550 1555
1551 1556 h.flash(flash_message, category='success')
1552 1557
1553 1558 default_redirect_url = h.route_path(
1554 1559 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1555 1560
1556 1561 except NonRelativePathError:
1557 1562 log.exception('Non Relative path found')
1558 1563 error = _('The location specified must be a relative path and must not '
1559 1564 'contain .. in the path')
1560 1565 h.flash(error, category='warning')
1561 1566
1562 1567 return {
1563 1568 'error': error,
1564 1569 'redirect_url': default_redirect_url
1565 1570 }
1566 1571 except (NodeError, NodeAlreadyExistsError) as e:
1567 1572 error = h.escape(e)
1568 1573 h.flash(error, category='error')
1569 1574
1570 1575 return {
1571 1576 'error': error,
1572 1577 'redirect_url': default_redirect_url
1573 1578 }
1574 1579 except Exception:
1575 1580 log.exception('Error occurred during commit')
1576 1581 error = _('Error occurred during commit')
1577 1582 h.flash(error, category='error')
1578 1583 return {
1579 1584 'error': error,
1580 1585 'redirect_url': default_redirect_url
1581 1586 }
1582 1587
1583 1588 return {
1584 1589 'error': None,
1585 1590 'redirect_url': default_redirect_url
1586 1591 }
1592
1593 @LoginRequired()
1594 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1595 @CSRFRequired()
1596 def repo_files_replace_file(self):
1597 _ = self.request.translate
1598 c = self.load_default_context()
1599 commit_id, f_path = self._get_commit_and_path()
1600
1601 self._ensure_not_locked()
1602
1603 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1604 if c.commit is None:
1605 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1606
1607 if self.rhodecode_vcs_repo.is_empty():
1608 default_redirect_url = h.route_path(
1609 'repo_summary', repo_name=self.db_repo_name)
1610 else:
1611 default_redirect_url = h.route_path(
1612 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1613
1614 if self.rhodecode_vcs_repo.is_empty():
1615 # for empty repository we cannot check for current branch, we rely on
1616 # c.commit.branch instead
1617 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1618 else:
1619 _branch_name, _sha_commit_id, is_head = \
1620 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1621 landing_ref=self.db_repo.landing_ref_name)
1622
1623 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1624 if error:
1625 return {
1626 'error': error,
1627 'redirect_url': default_redirect_url
1628 }
1629 error = self.check_branch_permission(_branch_name, json_mode=True)
1630 if error:
1631 return {
1632 'error': error,
1633 'redirect_url': default_redirect_url
1634 }
1635
1636 c.default_message = (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1637 c.f_path = f_path
1638
1639 r_post = self.request.POST
1640
1641 message = c.default_message
1642 user_message = r_post.getall('message')
1643 if isinstance(user_message, list) and user_message:
1644 # we take the first from duplicated results if it's not empty
1645 message = user_message[0] if user_message[0] else message
1646
1647 data_for_replacement = r_post.getall('files_upload') or []
1648 if (objects_count := len(data_for_replacement)) > 1:
1649 return {
1650 'error': 'too many files for replacement',
1651 'redirect_url': default_redirect_url
1652 }
1653 elif not objects_count:
1654 return {
1655 'error': 'missing files',
1656 'redirect_url': default_redirect_url
1657 }
1658
1659 content = data_for_replacement[0].file
1660 retrieved_filename = data_for_replacement[0].filename
1661
1662 if retrieved_filename.split('.')[-1] != f_path.split('.')[-1]:
1663 return {
1664 'error': 'file extension of uploaded file doesn\'t match an original file\'s extension',
1665 'redirect_url': default_redirect_url
1666 }
1667
1668 author = self._rhodecode_db_user.full_contact
1669
1670 try:
1671 commit = ScmModel().update_binary_node(
1672 user=self._rhodecode_db_user.user_id,
1673 repo=self.db_repo,
1674 message=message,
1675 node={
1676 'content': content,
1677 'file_path': f_path.encode(),
1678 },
1679 parent_commit=c.commit,
1680 author=author,
1681 )
1682
1683 h.flash(_('Successfully committed 1 new file'), category='success')
1684
1685 default_redirect_url = h.route_path(
1686 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1687
1688 except (NodeError, NodeAlreadyExistsError) as e:
1689 error = h.escape(e)
1690 h.flash(error, category='error')
1691
1692 return {
1693 'error': error,
1694 'redirect_url': default_redirect_url
1695 }
1696 except Exception:
1697 log.exception('Error occurred during commit')
1698 error = _('Error occurred during commit')
1699 h.flash(error, category='error')
1700 return {
1701 'error': error,
1702 'redirect_url': default_redirect_url
1703 }
1704
1705 return {
1706 'error': None,
1707 'redirect_url': default_redirect_url
1708 }
@@ -1,1044 +1,1063 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Scm model for RhodeCode
21 21 """
22 22
23 23 import os.path
24 24 import traceback
25 25 import logging
26 26 import io
27 27
28 28 from sqlalchemy import func
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 import rhodecode
32 32 from rhodecode.lib.str_utils import safe_bytes
33 33 from rhodecode.lib.vcs import get_backend
34 34 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
35 35 from rhodecode.lib.vcs.nodes import FileNode
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib import helpers as h, rc_cache
38 38 from rhodecode.lib.auth import (
39 39 HasRepoPermissionAny, HasRepoGroupPermissionAny,
40 40 HasUserGroupPermissionAny)
41 41 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
42 42 from rhodecode.lib import hooks_utils
43 43 from rhodecode.lib.utils import (
44 44 get_filesystem_repos, make_db_config)
45 45 from rhodecode.lib.str_utils import safe_str
46 46 from rhodecode.lib.system_info import get_system_info
47 47 from rhodecode.model import BaseModel
48 48 from rhodecode.model.db import (
49 49 or_, false, null,
50 50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 51 PullRequest, FileStore)
52 52 from rhodecode.model.settings import VcsSettingsModel
53 53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<{}('id:{}')>".format(self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<{}('id:{}')>".format(self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class SimpleCachedRepoList(object):
75 75 """
76 76 Lighter version of of iteration of repos without the scm initialisation,
77 77 and with cache usage
78 78 """
79 79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 80 self.db_repo_list = db_repo_list
81 81 self.repos_path = repos_path
82 82 self.order_by = order_by
83 83 self.reversed = (order_by or '').startswith('-')
84 84 if not perm_set:
85 85 perm_set = ['repository.read', 'repository.write',
86 86 'repository.admin']
87 87 self.perm_set = perm_set
88 88
89 89 def __len__(self):
90 90 return len(self.db_repo_list)
91 91
92 92 def __repr__(self):
93 93 return '<{} ({})>'.format(self.__class__.__name__, self.__len__())
94 94
95 95 def __iter__(self):
96 96 for dbr in self.db_repo_list:
97 97 # check permission at this level
98 98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 99 dbr.repo_name, 'SimpleCachedRepoList check')
100 100 if not has_perm:
101 101 continue
102 102
103 103 tmp_d = {
104 104 'name': dbr.repo_name,
105 105 'dbrepo': dbr.get_dict(),
106 106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 107 }
108 108 yield tmp_d
109 109
110 110
111 111 class _PermCheckIterator(object):
112 112
113 113 def __init__(
114 114 self, obj_list, obj_attr, perm_set, perm_checker,
115 115 extra_kwargs=None):
116 116 """
117 117 Creates iterator from given list of objects, additionally
118 118 checking permission for them from perm_set var
119 119
120 120 :param obj_list: list of db objects
121 121 :param obj_attr: attribute of object to pass into perm_checker
122 122 :param perm_set: list of permissions to check
123 123 :param perm_checker: callable to check permissions against
124 124 """
125 125 self.obj_list = obj_list
126 126 self.obj_attr = obj_attr
127 127 self.perm_set = perm_set
128 128 self.perm_checker = perm_checker(*self.perm_set)
129 129 self.extra_kwargs = extra_kwargs or {}
130 130
131 131 def __len__(self):
132 132 return len(self.obj_list)
133 133
134 134 def __repr__(self):
135 135 return '<{} ({})>'.format(self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 for db_obj in self.obj_list:
139 139 # check permission at this level
140 140 # NOTE(marcink): the __dict__.get() is ~4x faster then getattr()
141 141 name = db_obj.__dict__.get(self.obj_attr, None)
142 142 if not self.perm_checker(name, self.__class__.__name__, **self.extra_kwargs):
143 143 continue
144 144
145 145 yield db_obj
146 146
147 147
148 148 class RepoList(_PermCheckIterator):
149 149
150 150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 151 if not perm_set:
152 152 perm_set = ['repository.read', 'repository.write', 'repository.admin']
153 153
154 154 super().__init__(
155 155 obj_list=db_repo_list,
156 156 obj_attr='_repo_name', perm_set=perm_set,
157 157 perm_checker=HasRepoPermissionAny,
158 158 extra_kwargs=extra_kwargs)
159 159
160 160
161 161 class RepoGroupList(_PermCheckIterator):
162 162
163 163 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
164 164 if not perm_set:
165 165 perm_set = ['group.read', 'group.write', 'group.admin']
166 166
167 167 super().__init__(
168 168 obj_list=db_repo_group_list,
169 169 obj_attr='_group_name', perm_set=perm_set,
170 170 perm_checker=HasRepoGroupPermissionAny,
171 171 extra_kwargs=extra_kwargs)
172 172
173 173
174 174 class UserGroupList(_PermCheckIterator):
175 175
176 176 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
177 177 if not perm_set:
178 178 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
179 179
180 180 super().__init__(
181 181 obj_list=db_user_group_list,
182 182 obj_attr='users_group_name', perm_set=perm_set,
183 183 perm_checker=HasUserGroupPermissionAny,
184 184 extra_kwargs=extra_kwargs)
185 185
186 186
187 187 class ScmModel(BaseModel):
188 188 """
189 189 Generic Scm Model
190 190 """
191 191
192 192 @LazyProperty
193 193 def repos_path(self):
194 194 """
195 195 Gets the repositories root path from database
196 196 """
197 197
198 198 settings_model = VcsSettingsModel(sa=self.sa)
199 199 return settings_model.get_repos_location()
200 200
201 201 def repo_scan(self, repos_path=None):
202 202 """
203 203 Listing of repositories in given path. This path should not be a
204 204 repository itself. Return a dictionary of repository objects
205 205
206 206 :param repos_path: path to directory containing repositories
207 207 """
208 208
209 209 if repos_path is None:
210 210 repos_path = self.repos_path
211 211
212 212 log.info('scanning for repositories in %s', repos_path)
213 213
214 214 config = make_db_config()
215 215 config.set('extensions', 'largefiles', '')
216 216 repos = {}
217 217
218 218 for name, path in get_filesystem_repos(repos_path, recursive=True):
219 219 # name need to be decomposed and put back together using the /
220 220 # since this is internal storage separator for rhodecode
221 221 name = Repository.normalize_repo_name(name)
222 222
223 223 try:
224 224 if name in repos:
225 225 raise RepositoryError('Duplicate repository name %s '
226 226 'found in %s' % (name, path))
227 227 elif path[0] in rhodecode.BACKENDS:
228 228 backend = get_backend(path[0])
229 229 repos[name] = backend(path[1], config=config,
230 230 with_wire={"cache": False})
231 231 except OSError:
232 232 continue
233 233 except RepositoryError:
234 234 log.exception('Failed to create a repo')
235 235 continue
236 236
237 237 log.debug('found %s paths with repositories', len(repos))
238 238 return repos
239 239
240 240 def get_repos(self, all_repos=None, sort_key=None):
241 241 """
242 242 Get all repositories from db and for each repo create it's
243 243 backend instance and fill that backed with information from database
244 244
245 245 :param all_repos: list of repository names as strings
246 246 give specific repositories list, good for filtering
247 247
248 248 :param sort_key: initial sorting of repositories
249 249 """
250 250 if all_repos is None:
251 251 all_repos = self.sa.query(Repository)\
252 252 .filter(Repository.group_id == null())\
253 253 .order_by(func.lower(Repository.repo_name)).all()
254 254 repo_iter = SimpleCachedRepoList(
255 255 all_repos, repos_path=self.repos_path, order_by=sort_key)
256 256 return repo_iter
257 257
258 @staticmethod
259 def get_parent_commits(parent_commit, scm_instance):
260 if not parent_commit:
261 parent_commit = EmptyCommit(alias=scm_instance.alias)
262
263 if isinstance(parent_commit, EmptyCommit):
264 # EmptyCommit means we're editing empty repository
265 parents = None
266 else:
267 parents = [parent_commit]
268 return parent_commit, parents
269
270 def initialize_inmemory_vars(self, user, repo, message, author):
271 """
272 Initialize node specific objects for further usage
273 """
274 user = self._get_user(user)
275 scm_instance = repo.scm_instance(cache=False)
276 message = safe_str(message)
277 commiter = user.full_contact
278 author = safe_str(author) if author else commiter
279 imc = scm_instance.in_memory_commit
280
281 return user, scm_instance, message, commiter, author, imc
282
258 283 def get_repo_groups(self, all_groups=None):
259 284 if all_groups is None:
260 285 all_groups = RepoGroup.query()\
261 286 .filter(RepoGroup.group_parent_id == null()).all()
262 287 return [x for x in RepoGroupList(all_groups)]
263 288
264 289 def mark_for_invalidation(self, repo_name, delete=False):
265 290 """
266 291 Mark caches of this repo invalid in the database. `delete` flag
267 292 removes the cache entries
268 293
269 294 :param repo_name: the repo_name for which caches should be marked
270 295 invalid, or deleted
271 296 :param delete: delete the entry keys instead of setting bool
272 297 flag on them, and also purge caches used by the dogpile
273 298 """
274 299 repo = Repository.get_by_repo_name(repo_name)
275 300
276 301 if repo:
277 302 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
278 303 repo_id=repo.repo_id)
279 304 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
280 305
281 306 repo_id = repo.repo_id
282 307 config = repo._config
283 308 config.set('extensions', 'largefiles', '')
284 309 repo.update_commit_cache(config=config, cs_cache=None)
285 310 if delete:
286 311 cache_namespace_uid = f'cache_repo.{repo_id}'
287 312 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
288 313
289 314 def toggle_following_repo(self, follow_repo_id, user_id):
290 315
291 316 f = self.sa.query(UserFollowing)\
292 317 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
293 318 .filter(UserFollowing.user_id == user_id).scalar()
294 319
295 320 if f is not None:
296 321 try:
297 322 self.sa.delete(f)
298 323 return
299 324 except Exception:
300 325 log.error(traceback.format_exc())
301 326 raise
302 327
303 328 try:
304 329 f = UserFollowing()
305 330 f.user_id = user_id
306 331 f.follows_repo_id = follow_repo_id
307 332 self.sa.add(f)
308 333 except Exception:
309 334 log.error(traceback.format_exc())
310 335 raise
311 336
312 337 def toggle_following_user(self, follow_user_id, user_id):
313 338 f = self.sa.query(UserFollowing)\
314 339 .filter(UserFollowing.follows_user_id == follow_user_id)\
315 340 .filter(UserFollowing.user_id == user_id).scalar()
316 341
317 342 if f is not None:
318 343 try:
319 344 self.sa.delete(f)
320 345 return
321 346 except Exception:
322 347 log.error(traceback.format_exc())
323 348 raise
324 349
325 350 try:
326 351 f = UserFollowing()
327 352 f.user_id = user_id
328 353 f.follows_user_id = follow_user_id
329 354 self.sa.add(f)
330 355 except Exception:
331 356 log.error(traceback.format_exc())
332 357 raise
333 358
334 359 def is_following_repo(self, repo_name, user_id, cache=False):
335 360 r = self.sa.query(Repository)\
336 361 .filter(Repository.repo_name == repo_name).scalar()
337 362
338 363 f = self.sa.query(UserFollowing)\
339 364 .filter(UserFollowing.follows_repository == r)\
340 365 .filter(UserFollowing.user_id == user_id).scalar()
341 366
342 367 return f is not None
343 368
344 369 def is_following_user(self, username, user_id, cache=False):
345 370 u = User.get_by_username(username)
346 371
347 372 f = self.sa.query(UserFollowing)\
348 373 .filter(UserFollowing.follows_user == u)\
349 374 .filter(UserFollowing.user_id == user_id).scalar()
350 375
351 376 return f is not None
352 377
353 378 def get_followers(self, repo):
354 379 repo = self._get_repo(repo)
355 380
356 381 return self.sa.query(UserFollowing)\
357 382 .filter(UserFollowing.follows_repository == repo).count()
358 383
359 384 def get_forks(self, repo):
360 385 repo = self._get_repo(repo)
361 386 return self.sa.query(Repository)\
362 387 .filter(Repository.fork == repo).count()
363 388
364 389 def get_pull_requests(self, repo):
365 390 repo = self._get_repo(repo)
366 391 return self.sa.query(PullRequest)\
367 392 .filter(PullRequest.target_repo == repo)\
368 393 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
369 394
370 395 def get_artifacts(self, repo):
371 396 repo = self._get_repo(repo)
372 397 return self.sa.query(FileStore)\
373 398 .filter(FileStore.repo == repo)\
374 399 .filter(or_(FileStore.hidden == null(), FileStore.hidden == false())).count()
375 400
376 401 def mark_as_fork(self, repo, fork, user):
377 402 repo = self._get_repo(repo)
378 403 fork = self._get_repo(fork)
379 404 if fork and repo.repo_id == fork.repo_id:
380 405 raise Exception("Cannot set repository as fork of itself")
381 406
382 407 if fork and repo.repo_type != fork.repo_type:
383 408 raise RepositoryError(
384 409 "Cannot set repository as fork of repository with other type")
385 410
386 411 repo.fork = fork
387 412 self.sa.add(repo)
388 413 return repo
389 414
390 415 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True, **kwargs):
391 416 dbrepo = self._get_repo(repo)
392 417 remote_uri = remote_uri or dbrepo.clone_uri
393 418 if not remote_uri:
394 419 raise Exception("This repository doesn't have a clone uri")
395 420
396 421 repo = dbrepo.scm_instance(cache=False)
397 422 repo.config.clear_section('hooks')
398 423
399 424 try:
400 425 # NOTE(marcink): add extra validation so we skip invalid urls
401 426 # this is due this tasks can be executed via scheduler without
402 427 # proper validation of remote_uri
403 428 if validate_uri:
404 429 config = make_db_config(clear_session=False)
405 430 url_validator(remote_uri, dbrepo.repo_type, config)
406 431 except InvalidCloneUrl:
407 432 raise
408 433
409 434 repo_name = dbrepo.repo_name
410 435 try:
411 436 # TODO: we need to make sure those operations call proper hooks !
412 437 repo.fetch(remote_uri, **kwargs)
413 438
414 439 self.mark_for_invalidation(repo_name)
415 440 except Exception:
416 441 log.error(traceback.format_exc())
417 442 raise
418 443
419 444 def push_changes(self, repo, username, remote_uri=None, validate_uri=True, **kwargs):
420 445 dbrepo = self._get_repo(repo)
421 446 remote_uri = remote_uri or dbrepo.push_uri
422 447 if not remote_uri:
423 448 raise Exception("This repository doesn't have a clone uri")
424 449
425 450 repo = dbrepo.scm_instance(cache=False)
426 451 repo.config.clear_section('hooks')
427 452
428 453 try:
429 454 # NOTE(marcink): add extra validation so we skip invalid urls
430 455 # this is due this tasks can be executed via scheduler without
431 456 # proper validation of remote_uri
432 457 if validate_uri:
433 458 config = make_db_config(clear_session=False)
434 459 url_validator(remote_uri, dbrepo.repo_type, config)
435 460 except InvalidCloneUrl:
436 461 raise
437 462
438 463 try:
439 464 repo.push(remote_uri, **kwargs)
440 465 except Exception:
441 466 log.error(traceback.format_exc())
442 467 raise
443 468
444 469 def commit_change(self, repo, repo_name, commit, user, author, message,
445 470 content: bytes, f_path: bytes, branch: str = None):
446 471 """
447 472 Commits changes
448 473 """
449 474 user = self._get_user(user)
450 475
451 476 # message and author needs to be unicode
452 477 # proper backend should then translate that into required type
453 478 message = safe_str(message)
454 479 author = safe_str(author)
455 480 imc = repo.in_memory_commit
456 481 imc.change(FileNode(f_path, content, mode=commit.get_file_mode(f_path)))
457 482 try:
458 483 # TODO: handle pre-push action !
459 484 tip = imc.commit(
460 485 message=message, author=author, parents=[commit],
461 486 branch=branch or commit.branch)
462 487 except Exception as e:
463 488 log.error(traceback.format_exc())
464 489 raise IMCCommitError(str(e))
465 490 finally:
466 491 # always clear caches, if commit fails we want fresh object also
467 492 self.mark_for_invalidation(repo_name)
468 493
469 494 # We trigger the post-push action
470 495 hooks_utils.trigger_post_push_hook(
471 496 username=user.username, action='push_local', hook_type='post_push',
472 497 repo_name=repo_name, repo_type=repo.alias, commit_ids=[tip.raw_id])
473 498 return tip
474 499
475 500 def _sanitize_path(self, f_path: bytes):
476 501 if f_path.startswith(b'/') or f_path.startswith(b'./') or b'../' in f_path:
477 502 raise NonRelativePathError(b'%b is not an relative path' % f_path)
478 503 if f_path:
479 504 f_path = os.path.normpath(f_path)
480 505 return f_path
481 506
482 507 def get_dirnode_metadata(self, request, commit, dir_node):
483 508 if not dir_node.is_dir():
484 509 return []
485 510
486 511 data = []
487 512 for node in dir_node:
488 513 if not node.is_file():
489 514 # we skip file-nodes
490 515 continue
491 516
492 517 last_commit = node.last_commit
493 518 last_commit_date = last_commit.date
494 519 data.append({
495 520 'name': node.name,
496 521 'size': h.format_byte_size_binary(node.size),
497 522 'modified_at': h.format_date(last_commit_date),
498 523 'modified_ts': last_commit_date.isoformat(),
499 524 'revision': last_commit.revision,
500 525 'short_id': last_commit.short_id,
501 526 'message': h.escape(last_commit.message),
502 527 'author': h.escape(last_commit.author),
503 528 'user_profile': h.gravatar_with_user(
504 529 request, last_commit.author),
505 530 })
506 531
507 532 return data
508 533
509 534 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
510 535 extended_info=False, content=False, max_file_bytes=None):
511 536 """
512 537 recursive walk in root dir and return a set of all path in that dir
513 538 based on repository walk function
514 539
515 540 :param repo_name: name of repository
516 541 :param commit_id: commit id for which to list nodes
517 542 :param root_path: root path to list
518 543 :param flat: return as a list, if False returns a dict with description
519 544 :param extended_info: show additional info such as md5, binary, size etc
520 545 :param content: add nodes content to the return data
521 546 :param max_file_bytes: will not return file contents over this limit
522 547
523 548 """
524 549 _files = list()
525 550 _dirs = list()
526 551
527 552 try:
528 553 _repo = self._get_repo(repo_name)
529 554 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
530 555 root_path = root_path.lstrip('/')
531 556
532 557 # get RootNode, inject pre-load options before walking
533 558 top_node = commit.get_node(root_path)
534 559 extended_info_pre_load = []
535 560 if extended_info:
536 561 extended_info_pre_load += ['md5']
537 562 top_node.default_pre_load = ['is_binary', 'size'] + extended_info_pre_load
538 563
539 564 for __, dirs, files in commit.walk(top_node):
540 565
541 566 for f in files:
542 567 _content = None
543 568 _data = f_name = f.str_path
544 569
545 570 if not flat:
546 571 _data = {
547 572 "name": h.escape(f_name),
548 573 "type": "file",
549 574 }
550 575 if extended_info:
551 576 _data.update({
552 577 "md5": f.md5,
553 578 "binary": f.is_binary,
554 579 "size": f.size,
555 580 "extension": f.extension,
556 581 "mimetype": f.mimetype,
557 582 "lines": f.lines()[0]
558 583 })
559 584
560 585 if content:
561 586 over_size_limit = (max_file_bytes is not None
562 587 and f.size > max_file_bytes)
563 588 full_content = None
564 589 if not f.is_binary and not over_size_limit:
565 590 full_content = f.str_content
566 591
567 592 _data.update({
568 593 "content": full_content,
569 594 })
570 595 _files.append(_data)
571 596
572 597 for d in dirs:
573 598 _data = d_name = d.str_path
574 599 if not flat:
575 600 _data = {
576 601 "name": h.escape(d_name),
577 602 "type": "dir",
578 603 }
579 604 if extended_info:
580 605 _data.update({
581 606 "md5": "",
582 607 "binary": False,
583 608 "size": 0,
584 609 "extension": "",
585 610 })
586 611 if content:
587 612 _data.update({
588 613 "content": None
589 614 })
590 615 _dirs.append(_data)
591 616 except RepositoryError:
592 617 log.exception("Exception in get_nodes")
593 618 raise
594 619
595 620 return _dirs, _files
596 621
597 622 def get_quick_filter_nodes(self, repo_name, commit_id, root_path='/'):
598 623 """
599 624 Generate files for quick filter in files view
600 625 """
601 626
602 627 _files = list()
603 628 _dirs = list()
604 629 try:
605 630 _repo = self._get_repo(repo_name)
606 631 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
607 632 root_path = root_path.lstrip('/')
608 633
609 634 top_node = commit.get_node(root_path)
610 635 top_node.default_pre_load = []
611 636
612 637 for __, dirs, files in commit.walk(top_node):
613 638 for f in files:
614 639
615 640 _data = {
616 641 "name": h.escape(f.str_path),
617 642 "type": "file",
618 643 }
619 644
620 645 _files.append(_data)
621 646
622 647 for d in dirs:
623 648
624 649 _data = {
625 650 "name": h.escape(d.str_path),
626 651 "type": "dir",
627 652 }
628 653
629 654 _dirs.append(_data)
630 655 except RepositoryError:
631 656 log.exception("Exception in get_quick_filter_nodes")
632 657 raise
633 658
634 659 return _dirs, _files
635 660
636 661 def get_node(self, repo_name, commit_id, file_path,
637 662 extended_info=False, content=False, max_file_bytes=None, cache=True):
638 663 """
639 664 retrieve single node from commit
640 665 """
641 666
642 667 try:
643 668
644 669 _repo = self._get_repo(repo_name)
645 670 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
646 671
647 672 file_node = commit.get_node(file_path)
648 673 if file_node.is_dir():
649 674 raise RepositoryError('The given path is a directory')
650 675
651 676 _content = None
652 677 f_name = file_node.str_path
653 678
654 679 file_data = {
655 680 "name": h.escape(f_name),
656 681 "type": "file",
657 682 }
658 683
659 684 if extended_info:
660 685 file_data.update({
661 686 "extension": file_node.extension,
662 687 "mimetype": file_node.mimetype,
663 688 })
664 689
665 690 if cache:
666 691 md5 = file_node.md5
667 692 is_binary = file_node.is_binary
668 693 size = file_node.size
669 694 else:
670 695 is_binary, md5, size, _content = file_node.metadata_uncached()
671 696
672 697 file_data.update({
673 698 "md5": md5,
674 699 "binary": is_binary,
675 700 "size": size,
676 701 })
677 702
678 703 if content and cache:
679 704 # get content + cache
680 705 size = file_node.size
681 706 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
682 707 full_content = None
683 708 all_lines = 0
684 709 if not file_node.is_binary and not over_size_limit:
685 710 full_content = safe_str(file_node.content)
686 711 all_lines, empty_lines = file_node.count_lines(full_content)
687 712
688 713 file_data.update({
689 714 "content": full_content,
690 715 "lines": all_lines
691 716 })
692 717 elif content:
693 718 # get content *without* cache
694 719 if _content is None:
695 720 is_binary, md5, size, _content = file_node.metadata_uncached()
696 721
697 722 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
698 723 full_content = None
699 724 all_lines = 0
700 725 if not is_binary and not over_size_limit:
701 726 full_content = safe_str(_content)
702 727 all_lines, empty_lines = file_node.count_lines(full_content)
703 728
704 729 file_data.update({
705 730 "content": full_content,
706 731 "lines": all_lines
707 732 })
708 733
709 734 except RepositoryError:
710 735 log.exception("Exception in get_node")
711 736 raise
712 737
713 738 return file_data
714 739
715 740 def get_fts_data(self, repo_name, commit_id, root_path='/'):
716 741 """
717 742 Fetch node tree for usage in full text search
718 743 """
719 744
720 745 tree_info = list()
721 746
722 747 try:
723 748 _repo = self._get_repo(repo_name)
724 749 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
725 750 root_path = root_path.lstrip('/')
726 751 top_node = commit.get_node(root_path)
727 752 top_node.default_pre_load = []
728 753
729 754 for __, dirs, files in commit.walk(top_node):
730 755
731 756 for f in files:
732 757 is_binary, md5, size, _content = f.metadata_uncached()
733 758 _data = {
734 759 "name": f.str_path,
735 760 "md5": md5,
736 761 "extension": f.extension,
737 762 "binary": is_binary,
738 763 "size": size
739 764 }
740 765
741 766 tree_info.append(_data)
742 767
743 768 except RepositoryError:
744 769 log.exception("Exception in get_nodes")
745 770 raise
746 771
747 772 return tree_info
748 773
749 774 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
750 775 author=None, trigger_push_hook=True):
751 776 """
752 777 Commits given multiple nodes into repo
753 778
754 779 :param user: RhodeCode User object or user_id, the commiter
755 780 :param repo: RhodeCode Repository object
756 781 :param message: commit message
757 782 :param nodes: mapping {filename:{'content':content},...}
758 783 :param parent_commit: parent commit, can be empty than it's
759 784 initial commit
760 785 :param author: author of commit, cna be different that commiter
761 786 only for git
762 787 :param trigger_push_hook: trigger push hooks
763 788
764 789 :returns: new committed commit
765 790 """
766
767 user = self._get_user(user)
768 scm_instance = repo.scm_instance(cache=False)
769
770 message = safe_str(message)
771 commiter = user.full_contact
772 author = safe_str(author) if author else commiter
791 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
792 user, repo, message, author)
773 793
774 imc = scm_instance.in_memory_commit
775
776 if not parent_commit:
777 parent_commit = EmptyCommit(alias=scm_instance.alias)
778
779 if isinstance(parent_commit, EmptyCommit):
780 # EmptyCommit means we're editing empty repository
781 parents = None
782 else:
783 parents = [parent_commit]
794 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
784 795
785 796 upload_file_types = (io.BytesIO, io.BufferedRandom)
786 797 processed_nodes = []
787 798 for filename, content_dict in nodes.items():
788 799 if not isinstance(filename, bytes):
789 800 raise ValueError(f'filename key in nodes needs to be bytes , or {upload_file_types}')
790 801 content = content_dict['content']
791 802 if not isinstance(content, upload_file_types + (bytes,)):
792 803 raise ValueError('content key value in nodes needs to be bytes')
793 804
794 805 for f_path in nodes:
795 806 f_path = self._sanitize_path(f_path)
796 807 content = nodes[f_path]['content']
797 808
798 809 # decoding here will force that we have proper encoded values
799 810 # in any other case this will throw exceptions and deny commit
800 811
801 812 if isinstance(content, bytes):
802 813 pass
803 814 elif isinstance(content, upload_file_types):
804 815 content = content.read()
805 816 else:
806 817 raise Exception(f'Content is of unrecognized type {type(content)}, expected {upload_file_types}')
807 818 processed_nodes.append((f_path, content))
808 819
809 820 # add multiple nodes
810 821 for path, content in processed_nodes:
811 822 imc.add(FileNode(path, content=content))
812 823
813 824 # TODO: handle pre push scenario
814 825 tip = imc.commit(message=message,
815 826 author=author,
816 827 parents=parents,
817 828 branch=parent_commit.branch)
818 829
819 830 self.mark_for_invalidation(repo.repo_name)
820 831 if trigger_push_hook:
821 832 hooks_utils.trigger_post_push_hook(
822 833 username=user.username, action='push_local',
823 834 repo_name=repo.repo_name, repo_type=scm_instance.alias,
824 835 hook_type='post_push',
825 836 commit_ids=[tip.raw_id])
826 837 return tip
827 838
828 839 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
829 840 author=None, trigger_push_hook=True):
830 user = self._get_user(user)
831 scm_instance = repo.scm_instance(cache=False)
832
833 message = safe_str(message)
834 commiter = user.full_contact
835 author = safe_str(author) if author else commiter
836
837 imc = scm_instance.in_memory_commit
841 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
842 user, repo, message, author)
838 843
839 if not parent_commit:
840 parent_commit = EmptyCommit(alias=scm_instance.alias)
841
842 if isinstance(parent_commit, EmptyCommit):
843 # EmptyCommit means we we're editing empty repository
844 parents = None
845 else:
846 parents = [parent_commit]
844 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
847 845
848 846 # add multiple nodes
849 847 for _filename, data in nodes.items():
850 848 # new filename, can be renamed from the old one, also sanitaze
851 849 # the path for any hack around relative paths like ../../ etc.
852 850 filename = self._sanitize_path(data['filename'])
853 851 old_filename = self._sanitize_path(_filename)
854 852 content = data['content']
855 853 file_mode = data.get('mode')
856 854 filenode = FileNode(old_filename, content=content, mode=file_mode)
857 855 op = data['op']
858 856 if op == 'add':
859 857 imc.add(filenode)
860 858 elif op == 'del':
861 859 imc.remove(filenode)
862 860 elif op == 'mod':
863 861 if filename != old_filename:
864 862 # TODO: handle renames more efficient, needs vcs lib changes
865 863 imc.remove(filenode)
866 864 imc.add(FileNode(filename, content=content, mode=file_mode))
867 865 else:
868 866 imc.change(filenode)
869 867
870 868 try:
871 869 # TODO: handle pre push scenario commit changes
872 870 tip = imc.commit(message=message,
873 871 author=author,
874 872 parents=parents,
875 873 branch=parent_commit.branch)
876 874 except NodeNotChangedError:
877 875 raise
878 876 except Exception as e:
879 877 log.exception("Unexpected exception during call to imc.commit")
880 878 raise IMCCommitError(str(e))
881 879 finally:
882 880 # always clear caches, if commit fails we want fresh object also
883 881 self.mark_for_invalidation(repo.repo_name)
884 882
885 883 if trigger_push_hook:
886 884 hooks_utils.trigger_post_push_hook(
887 885 username=user.username, action='push_local', hook_type='post_push',
888 886 repo_name=repo.repo_name, repo_type=scm_instance.alias,
889 887 commit_ids=[tip.raw_id])
890 888
891 889 return tip
892 890
891 def update_binary_node(self, user, repo, message, node, parent_commit=None, author=None):
892 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
893 user, repo, message, author)
894
895 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
896
897 file_path = node.get('file_path')
898 if isinstance(raw_content := node.get('content'), (io.BytesIO, io.BufferedRandom)):
899 content = raw_content.read()
900 else:
901 raise Exception("Wrong content was provided")
902 file_node = FileNode(file_path, content=content)
903 imc.change(file_node)
904
905 try:
906 tip = imc.commit(message=message,
907 author=author,
908 parents=parents,
909 branch=parent_commit.branch)
910 except NodeNotChangedError:
911 raise
912 except Exception as e:
913 log.exception("Unexpected exception during call to imc.commit")
914 raise IMCCommitError(str(e))
915 finally:
916 self.mark_for_invalidation(repo.repo_name)
917
918 hooks_utils.trigger_post_push_hook(
919 username=user.username, action='push_local', hook_type='post_push',
920 repo_name=repo.repo_name, repo_type=scm_instance.alias,
921 commit_ids=[tip.raw_id])
922 return tip
923
893 924 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
894 925 author=None, trigger_push_hook=True):
895 926 """
896 927 Deletes given multiple nodes into `repo`
897 928
898 929 :param user: RhodeCode User object or user_id, the committer
899 930 :param repo: RhodeCode Repository object
900 931 :param message: commit message
901 932 :param nodes: mapping {filename:{'content':content},...}
902 933 :param parent_commit: parent commit, can be empty than it's initial
903 934 commit
904 935 :param author: author of commit, cna be different that commiter only
905 936 for git
906 937 :param trigger_push_hook: trigger push hooks
907 938
908 939 :returns: new commit after deletion
909 940 """
910 941
911 user = self._get_user(user)
912 scm_instance = repo.scm_instance(cache=False)
942 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
943 user, repo, message, author)
913 944
914 945 processed_nodes = []
915 946 for f_path in nodes:
916 947 f_path = self._sanitize_path(f_path)
917 948 # content can be empty but for compatibility it allows same dicts
918 949 # structure as add_nodes
919 950 content = nodes[f_path].get('content')
920 951 processed_nodes.append((safe_bytes(f_path), content))
921 952
922 message = safe_str(message)
923 commiter = user.full_contact
924 author = safe_str(author) if author else commiter
925
926 imc = scm_instance.in_memory_commit
953 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
927 954
928 if not parent_commit:
929 parent_commit = EmptyCommit(alias=scm_instance.alias)
930
931 if isinstance(parent_commit, EmptyCommit):
932 # EmptyCommit means we we're editing empty repository
933 parents = None
934 else:
935 parents = [parent_commit]
936 955 # add multiple nodes
937 956 for path, content in processed_nodes:
938 957 imc.remove(FileNode(path, content=content))
939 958
940 959 # TODO: handle pre push scenario
941 960 tip = imc.commit(message=message,
942 961 author=author,
943 962 parents=parents,
944 963 branch=parent_commit.branch)
945 964
946 965 self.mark_for_invalidation(repo.repo_name)
947 966 if trigger_push_hook:
948 967 hooks_utils.trigger_post_push_hook(
949 968 username=user.username, action='push_local', hook_type='post_push',
950 969 repo_name=repo.repo_name, repo_type=scm_instance.alias,
951 970 commit_ids=[tip.raw_id])
952 971 return tip
953 972
954 973 def strip(self, repo, commit_id, branch):
955 974 scm_instance = repo.scm_instance(cache=False)
956 975 scm_instance.config.clear_section('hooks')
957 976 scm_instance.strip(commit_id, branch)
958 977 self.mark_for_invalidation(repo.repo_name)
959 978
960 979 def get_unread_journal(self):
961 980 return self.sa.query(UserLog).count()
962 981
963 982 @classmethod
964 983 def backend_landing_ref(cls, repo_type):
965 984 """
966 985 Return a default landing ref based on a repository type.
967 986 """
968 987
969 988 landing_ref = {
970 989 'hg': ('branch:default', 'default'),
971 990 'git': ('branch:master', 'master'),
972 991 'svn': ('rev:tip', 'latest tip'),
973 992 'default': ('rev:tip', 'latest tip'),
974 993 }
975 994
976 995 return landing_ref.get(repo_type) or landing_ref['default']
977 996
978 997 def get_repo_landing_revs(self, translator, repo=None):
979 998 """
980 999 Generates select option with tags branches and bookmarks (for hg only)
981 1000 grouped by type
982 1001
983 1002 :param repo:
984 1003 """
985 1004 from rhodecode.lib.vcs.backends.git import GitRepository
986 1005
987 1006 _ = translator
988 1007 repo = self._get_repo(repo)
989 1008
990 1009 if repo:
991 1010 repo_type = repo.repo_type
992 1011 else:
993 1012 repo_type = 'default'
994 1013
995 1014 default_landing_ref, landing_ref_lbl = self.backend_landing_ref(repo_type)
996 1015
997 1016 default_ref_options = [
998 1017 [default_landing_ref, landing_ref_lbl]
999 1018 ]
1000 1019 default_choices = [
1001 1020 default_landing_ref
1002 1021 ]
1003 1022
1004 1023 if not repo:
1005 1024 # presented at NEW repo creation
1006 1025 return default_choices, default_ref_options
1007 1026
1008 1027 repo = repo.scm_instance()
1009 1028
1010 1029 ref_options = [(default_landing_ref, landing_ref_lbl)]
1011 1030 choices = [default_landing_ref]
1012 1031
1013 1032 # branches
1014 1033 branch_group = [(f'branch:{safe_str(b)}', safe_str(b)) for b in repo.branches]
1015 1034 if not branch_group:
1016 1035 # new repo, or without maybe a branch?
1017 1036 branch_group = default_ref_options
1018 1037
1019 1038 branches_group = (branch_group, _("Branches"))
1020 1039 ref_options.append(branches_group)
1021 1040 choices.extend([x[0] for x in branches_group[0]])
1022 1041
1023 1042 # bookmarks for HG
1024 1043 if repo.alias == 'hg':
1025 1044 bookmarks_group = (
1026 1045 [(f'book:{safe_str(b)}', safe_str(b))
1027 1046 for b in repo.bookmarks],
1028 1047 _("Bookmarks"))
1029 1048 ref_options.append(bookmarks_group)
1030 1049 choices.extend([x[0] for x in bookmarks_group[0]])
1031 1050
1032 1051 # tags
1033 1052 tags_group = (
1034 1053 [(f'tag:{safe_str(t)}', safe_str(t))
1035 1054 for t in repo.tags],
1036 1055 _("Tags"))
1037 1056 ref_options.append(tags_group)
1038 1057 choices.extend([x[0] for x in tags_group[0]])
1039 1058
1040 1059 return choices, ref_options
1041 1060
1042 1061 def get_server_info(self, environ=None):
1043 1062 server_info = get_system_info(environ)
1044 1063 return server_info
@@ -1,189 +1,189 b''
1 1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
2 2
3 3 <%
4 4 at_ref = request.GET.get('at')
5 5 if at_ref:
6 6 query={'at': at_ref}
7 7 default_commit_id = at_ref or c.rhodecode_db_repo.landing_ref_name
8 8 else:
9 9 query=None
10 10 default_commit_id = c.commit.raw_id
11 11 %>
12 12
13 13 <div id="codeblock" class="browserblock">
14 14 <div class="browser-header">
15 15 <div class="browser-nav">
16 16 <div class="pull-left">
17 17 ## loads the history for a file
18 18 ${h.hidden('file_refs_filter')}
19 19 </div>
20 20
21 21 <div class="pull-right">
22 22
23 23 ## Download
24 24 % if c.lf_node:
25 25 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
26 26 ${_('Download largefile')}
27 27 </a>
28 28 % else:
29 29 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
30 30 ${_('Download file')}
31 31 </a>
32 32 % endif
33 33
34 34 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
35 35 ## on branch head, can edit files
36 36 %if c.on_branch_head and c.branch_or_raw_id:
37 37 ## binary files are delete only
38 38 % if c.file.is_binary:
39 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
39 ${h.link_to(_('Replace'), h.route_path('repo_files_upload_file', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query={'upload_binary': 'true'}), class_="btn btn-default active tooltip", title=_('You can replace content of your binary file'))}
40 40 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query),class_="btn btn-danger")}
41 41 % else:
42 42 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
43 43 ${_('Edit on branch: ')}<code>${c.branch_name}</code>
44 44 </a>
45 45
46 46 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
47 47 ${_('Delete')}
48 48 </a>
49 49 % endif
50 50 ## not on head, forbid all
51 51 % else:
52 52 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
53 53 ${h.link_to(_('Delete'), '#Delete', class_="btn btn-default btn-danger disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
54 54 % endif
55 55 %endif
56 56
57 57 </div>
58 58 </div>
59 59 <div id="file_history_container"></div>
60 60
61 61 </div>
62 62 </div>
63 63
64 64 <div class="codeblock">
65 65 <div class=" codeblock-header">
66 66 <div class="file-filename">
67 67 <i class="icon-file"></i> ${c.file.name}
68 68 </div>
69 69
70 70 <div class="file-stats">
71 71
72 72 <div class="stats-info">
73 73 <span class="stats-first-item">
74 74 % if c.file_size_too_big:
75 75 0 ${(_('lines'))}
76 76 % else:
77 77 ${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}
78 78 % endif
79 79 </span>
80 80
81 81 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
82 82 % if c.lf_node:
83 83 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
84 84 % endif
85 85 <span>
86 86 | ${c.file.mimetype}
87 87 </span>
88 88
89 89 % if not c.file_size_too_big:
90 90 <span> |
91 91 ${h.get_lexer_for_filenode(c.file).__class__.__name__}
92 92 </span>
93 93 % endif
94 94
95 95 </div>
96 96 </div>
97 97 </div>
98 98
99 99 <div class="path clear-fix">
100 100 <div class="pull-left">
101 101 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'))}
102 102 </div>
103 103
104 104 <div class="pull-right stats">
105 105 <a id="file_history_overview" href="#loadHistory">
106 106 ${_('History')}
107 107 </a>
108 108 |
109 109 %if c.annotate:
110 110 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
111 111 %else:
112 112 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
113 113 %endif
114 114 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
115 115 % if not c.file.is_binary:
116 116 |<a href="#copySource" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${c.file.content}">${_('Copy content')}</a>
117 117 |<a href="#copyPermaLink" onclick="return false;" class="no-grey clipboard-action" data-clipboard-text="${h.route_url('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">${_('Copy permalink')}</a>
118 118 % endif
119 119
120 120 </div>
121 121 <div class="clear-fix"></div>
122 122 </div>
123 123
124 124 <div class="code-body clear-fix ">
125 125 %if c.file.is_binary:
126 126 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
127 127 % if rendered_binary:
128 128 <div class="text-center">
129 129 ${rendered_binary}
130 130 </div>
131 131 % else:
132 132 <div>
133 133 ${_('Binary file ({})').format(c.file.mimetype)}
134 134 </div>
135 135 % endif
136 136 %else:
137 137 % if c.file_size_too_big:
138 138 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
139 139 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
140 140 % else:
141 141 %if c.renderer and not c.annotate:
142 142 ## pick relative url based on renderer
143 143 <%
144 144 relative_urls = {
145 145 'raw': h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
146 146 'standard': h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
147 147 }
148 148 %>
149 149 ${h.render(c.file.str_content, renderer=c.renderer, relative_urls=relative_urls)}
150 150 %else:
151 151 <table class="cb codehilite">
152 152 %if c.annotate:
153 153 <% color_hasher = h.color_hasher() %>
154 154 %for annotation, lines in c.annotated_lines:
155 155 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
156 156 %endfor
157 157 %else:
158 158 %for line_num, tokens in enumerate(c.lines, 1):
159 159 ${sourceblock.render_line(line_num, tokens)}
160 160 %endfor
161 161 %endif
162 162 </table>
163 163 %endif
164 164 % endif
165 165 %endif
166 166 </div>
167 167
168 168 </div>
169 169
170 170 <script type="text/javascript">
171 171 % if request.GET.get('mark'):
172 172
173 173 $(function(){
174 174 $(".codehilite").mark(
175 175 "${request.GET.get('mark')}",
176 176 {
177 177 "className": 'match',
178 178 "accuracy": "complementary",
179 179 "ignorePunctuation": ":._(){}[]!'+=".split(""),
180 180 "each": function(el) {
181 181 // and also highlight lines !
182 182 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
183 183 }
184 184 }
185 185 );
186 186
187 187 });
188 188 % endif
189 189 </script>
@@ -1,211 +1,222 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Files Upload').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()"></%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='files')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21
22 22 <div class="box">
23 23 ## Template for uploads
24 24 <div style="display: none" id="tpl-dropzone">
25 25 <div class="dz-preview dz-file-preview">
26 26 <div class="dz-details">
27 27
28 28 <div class="dz-filename">
29 29 <span data-dz-name></span>
30 30 </div>
31 31 <div class="dz-filename-size">
32 32 <span class="dz-size" data-dz-size></span>
33 33
34 34 </div>
35 35
36 36 <div class="dz-sending" style="display: none">${_('Uploading...')}</div>
37 37 <div class="dz-response" style="display: none">
38 38 ${_('Uploaded')} 100%
39 39 </div>
40 40
41 41 </div>
42 42 <div class="dz-progress">
43 43 <span class="dz-upload" data-dz-uploadprogress></span>
44 44 </div>
45 45
46 46 <div class="dz-error-message">
47 47 </div>
48 48 </div>
49 49 </div>
50 50
51 51 <div class="edit-file-title">
52 % if c.replace_binary:
53 <span class="title-heading">${_('Replace content of')} <b>${c.f_path}</b> @ <code>${h.show_id(c.commit)}</code></span>
54 % else:
52 55 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
56 % endif
53 57 % if c.commit.branch:
54 58 <span class="tag branchtag">
55 59 <i class="icon-branch"></i> ${c.commit.branch}
56 60 </span>
57 61 % endif
58 62 </div>
59 63
64 % if not c.replace_binary:
60 65 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
62 66 <div class="edit-file-fieldset">
63 67 <div class="path-items">
64 68 <ul class="tooltip" title="Repository path to store uploaded files. To change it, navigate to different path and click upload from there.">
65 69 <li class="breadcrumb-path">
66 70 <div>
67 71 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
68 72 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a>${('/' if c.f_path else '')}
69 73 </div>
70 74 </li>
71 75 <li class="location-path">
72 76
73 77 </li>
74 78 </ul>
75 79 </div>
76 80
77 81 </div>
82 % else:
83 <% form_url = h.route_path('repo_files_replace_binary', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
84 % endif
78 85
79 86 <div class="upload-form table">
80 87 <div>
81 88
82 89 <div class="dropzone-wrapper" id="file-uploader" style="border: none; padding: 40px 0">
83 90 <div class="dropzone-pure">
84 91 <div class="dz-message">
85 92 <i class="icon-upload" style="font-size:36px"></i></br>
93 % if not c.replace_binary:
86 94 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
95 % else:
96 ${_("Drag'n Drop file here or")} <span class="link">${_('Choose your file')}</span>.<br>
97 % endif
87 98 </div>
88 99 </div>
89 100
90 101 </div>
91 102 </div>
92 103
93 104 </div>
94 105
95 106 <div class="upload-form edit-file-fieldset">
96 107 <div class="fieldset">
97 108 <div class="message">
98 109 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
99 110 </div>
100 111 </div>
101 112 <div class="pull-left">
102 113 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
103 114 </div>
104 115 </div>
105 116 ##${h.end_form()}
106 117
107 118 <div class="file-upload-transaction-wrapper" style="display: none">
108 119 <div class="file-upload-transaction">
109 120 <h3>${_('Committing...')}</h3>
110 121 <p>${_('Please wait while the files are being uploaded')}</p>
111 122 <p class="error" style="display: none">
112 123
113 124 </p>
114 125 <i class="icon-spin animate-spin"></i>
115 126 <p></p>
116 127 </div>
117 128 </div>
118 129
119 130 </div>
120 131
121 132 <script type="text/javascript">
122 133
123 134 $(document).ready(function () {
124 135
125 136 //see: https://www.dropzonejs.com/#configuration
126 137 myDropzone = new Dropzone("div#file-uploader", {
127 138 url: "${form_url}",
128 139 headers: {"X-CSRF-Token": CSRF_TOKEN},
129 140 paramName: function () {
130 141 return "files_upload"
131 142 }, // The name that will be used to transfer the file
132 143 parallelUploads: 20,
133 144 maxFiles: 20,
134 145 uploadMultiple: true,
135 146 //chunking: true, // use chunking transfer, not supported at the moment
136 147 //maxFilesize: 2, // in MBs
137 148 autoProcessQueue: false, // if false queue will not be processed automatically.
138 149 createImageThumbnails: false,
139 150 previewTemplate: document.querySelector('#tpl-dropzone').innerHTML,
140 151 accept: function (file, done) {
141 152 done();
142 153 },
143 154 init: function () {
144 155 this.on("addedfile", function (file) {
145 156
146 157 });
147 158
148 159 this.on("sending", function (file, xhr, formData) {
149 160 formData.append("message", $('#commit').val());
150 161 $(file.previewElement).find('.dz-sending').show();
151 162 });
152 163
153 164 this.on("success", function (file, response) {
154 165 $(file.previewElement).find('.dz-sending').hide();
155 166 $(file.previewElement).find('.dz-response').show();
156 167
157 168 if (response.error !== null) {
158 169 $('.file-upload-transaction-wrapper .error').html('ERROR: {0}'.format(response.error));
159 170 $('.file-upload-transaction-wrapper .error').show();
160 171 $('.file-upload-transaction-wrapper i').hide()
161 172 }
162 173
163 174 var redirect_url = response.redirect_url || '/';
164 175 window.location = redirect_url
165 176
166 177 });
167 178
168 179 this.on("error", function (file, errorMessage, xhr) {
169 180 var error = null;
170 181
171 182 if (xhr !== undefined){
172 183 var httpStatus = xhr.status + " " + xhr.statusText;
173 184 if (xhr !== undefined && xhr.status >= 500) {
174 185 error = httpStatus;
175 186 }
176 187 }
177 188
178 189 if (error === null) {
179 190 error = errorMessage.error || errorMessage || httpStatus;
180 191 }
181 192
182 193 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
183 194 });
184 195 }
185 196 });
186 197
187 198 $('#commit_btn').on('click', function(e) {
188 199 e.preventDefault();
189 200 var button = $(this);
190 201 if (button.hasClass('clicked')) {
191 202 button.attr('disabled', true);
192 203 } else {
193 204 button.addClass('clicked');
194 205 }
195 206
196 207 var files = myDropzone.getQueuedFiles();
197 208 if (files.length === 0) {
198 209 alert("Missing files");
199 210 e.preventDefault();
200 211 }
201 212
202 213 $('.upload-form').hide();
203 214 $('.file-upload-transaction-wrapper').show();
204 215 myDropzone.processQueue();
205 216
206 217 });
207 218
208 219 });
209 220
210 221 </script>
211 222 </%def>
@@ -1,319 +1,320 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 def get_url_defs():
21 21 from rhodecode.apps._base import ADMIN_PREFIX
22 22
23 23 return {
24 24 "home": "/",
25 25 "main_page_repos_data": "/_home_repos",
26 26 "main_page_repo_groups_data": "/_home_repo_groups",
27 27 "repo_group_home": "/{repo_group_name}",
28 28 "user_autocomplete_data": "/_users",
29 29 "user_group_autocomplete_data": "/_user_groups",
30 30 "repo_list_data": "/_repos",
31 31 "goto_switcher_data": "/_goto_data",
32 32 "admin_home": ADMIN_PREFIX + "",
33 33 "admin_audit_logs": ADMIN_PREFIX + "/audit_logs",
34 34 "admin_defaults_repositories": ADMIN_PREFIX + "/defaults/repositories",
35 35 "admin_defaults_repositories_update": ADMIN_PREFIX
36 36 + "/defaults/repositories/update",
37 37 "search": ADMIN_PREFIX + "/search",
38 38 "search_repo": "/{repo_name}/search",
39 39 "my_account_auth_tokens": ADMIN_PREFIX + "/my_account/auth_tokens",
40 40 "my_account_auth_tokens_add": ADMIN_PREFIX + "/my_account/auth_tokens/new",
41 41 "my_account_auth_tokens_delete": ADMIN_PREFIX
42 42 + "/my_account/auth_tokens/delete",
43 43 "repos": ADMIN_PREFIX + "/repos",
44 44 "repos_data": ADMIN_PREFIX + "/repos_data",
45 45 "repo_groups": ADMIN_PREFIX + "/repo_groups",
46 46 "repo_groups_data": ADMIN_PREFIX + "/repo_groups_data",
47 47 "user_groups": ADMIN_PREFIX + "/user_groups",
48 48 "user_groups_data": ADMIN_PREFIX + "/user_groups_data",
49 49 "user_profile": "/_profiles/{username}",
50 50 "profile_user_group": "/_profile_user_group/{user_group_name}",
51 51 "repo_summary": "/{repo_name}",
52 52 "repo_creating_check": "/{repo_name}/repo_creating_check",
53 53 "edit_repo": "/{repo_name}/settings",
54 54 "edit_repo_vcs": "/{repo_name}/settings/vcs",
55 55 "edit_repo_vcs_update": "/{repo_name}/settings/vcs/update",
56 56 "edit_repo_vcs_svn_pattern_delete": "/{repo_name}/settings/vcs/svn_pattern/delete",
57 57 "repo_archivefile": "/{repo_name}/archive/{fname}",
58 58 "repo_files_diff": "/{repo_name}/diff/{f_path}",
59 59 "repo_files_diff_2way_redirect": "/{repo_name}/diff-2way/{f_path}",
60 60 "repo_files": "/{repo_name}/files/{commit_id}/{f_path}",
61 61 "repo_files:default_path": "/{repo_name}/files/{commit_id}/",
62 62 "repo_files:default_commit": "/{repo_name}/files",
63 63 "repo_files:rendered": "/{repo_name}/render/{commit_id}/{f_path}",
64 64 "repo_files:annotated": "/{repo_name}/annotate/{commit_id}/{f_path}",
65 65 "repo_files:annotated_previous": "/{repo_name}/annotate-previous/{commit_id}/{f_path}",
66 66 "repo_files_nodelist": "/{repo_name}/nodelist/{commit_id}/{f_path}",
67 67 "repo_file_raw": "/{repo_name}/raw/{commit_id}/{f_path}",
68 68 "repo_file_download": "/{repo_name}/download/{commit_id}/{f_path}",
69 69 "repo_file_history": "/{repo_name}/history/{commit_id}/{f_path}",
70 70 "repo_file_authors": "/{repo_name}/authors/{commit_id}/{f_path}",
71 71 "repo_files_remove_file": "/{repo_name}/remove_file/{commit_id}/{f_path}",
72 72 "repo_files_delete_file": "/{repo_name}/delete_file/{commit_id}/{f_path}",
73 73 "repo_files_edit_file": "/{repo_name}/edit_file/{commit_id}/{f_path}",
74 74 "repo_files_update_file": "/{repo_name}/update_file/{commit_id}/{f_path}",
75 75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
76 76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
77 77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
78 79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
79 80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
80 81 "journal": ADMIN_PREFIX + "/journal",
81 82 "journal_rss": ADMIN_PREFIX + "/journal/rss",
82 83 "journal_atom": ADMIN_PREFIX + "/journal/atom",
83 84 "journal_public": ADMIN_PREFIX + "/public_journal",
84 85 "journal_public_atom": ADMIN_PREFIX + "/public_journal/atom",
85 86 "journal_public_atom_old": ADMIN_PREFIX + "/public_journal_atom",
86 87 "journal_public_rss": ADMIN_PREFIX + "/public_journal/rss",
87 88 "journal_public_rss_old": ADMIN_PREFIX + "/public_journal_rss",
88 89 "toggle_following": ADMIN_PREFIX + "/toggle_following",
89 90 "upload_file": "/_file_store/upload",
90 91 "download_file": "/_file_store/download/{fid}",
91 92 "download_file_by_token": "/_file_store/token-download/{_auth_token}/{fid}",
92 93 "gists_show": ADMIN_PREFIX + "/gists",
93 94 "gists_new": ADMIN_PREFIX + "/gists/new",
94 95 "gists_create": ADMIN_PREFIX + "/gists/create",
95 96 "gist_show": ADMIN_PREFIX + "/gists/{gist_id}",
96 97 "gist_delete": ADMIN_PREFIX + "/gists/{gist_id}/delete",
97 98 "gist_edit": ADMIN_PREFIX + "/gists/{gist_id}/edit",
98 99 "gist_edit_check_revision": ADMIN_PREFIX
99 100 + "/gists/{gist_id}/edit/check_revision",
100 101 "gist_update": ADMIN_PREFIX + "/gists/{gist_id}/update",
101 102 "gist_show_rev": ADMIN_PREFIX + "/gists/{gist_id}/rev/{revision}",
102 103 "gist_show_formatted": ADMIN_PREFIX
103 104 + "/gists/{gist_id}/rev/{revision}/{format}",
104 105 "gist_show_formatted_path": ADMIN_PREFIX
105 106 + "/gists/{gist_id}/rev/{revision}/{format}/{f_path}",
106 107 "login": ADMIN_PREFIX + "/login",
107 108 "logout": ADMIN_PREFIX + "/logout",
108 109 "register": ADMIN_PREFIX + "/register",
109 110 "reset_password": ADMIN_PREFIX + "/password_reset",
110 111 "reset_password_confirmation": ADMIN_PREFIX + "/password_reset_confirmation",
111 112 "admin_permissions_application": ADMIN_PREFIX + "/permissions/application",
112 113 "admin_permissions_application_update": ADMIN_PREFIX
113 114 + "/permissions/application/update",
114 115 "repo_commit_raw": "/{repo_name}/changeset-diff/{commit_id}",
115 116 "user_group_members_data": ADMIN_PREFIX
116 117 + "/user_groups/{user_group_id}/members",
117 118 "user_groups_new": ADMIN_PREFIX + "/user_groups/new",
118 119 "user_groups_create": ADMIN_PREFIX + "/user_groups/create",
119 120 "edit_user_group": ADMIN_PREFIX + "/user_groups/{user_group_id}/edit",
120 121 "edit_user_group_advanced_sync": ADMIN_PREFIX
121 122 + "/user_groups/{user_group_id}/edit/advanced/sync",
122 123 "edit_user_group_global_perms_update": ADMIN_PREFIX
123 124 + "/user_groups/{user_group_id}/edit/global_permissions/update",
124 125 "user_groups_update": ADMIN_PREFIX + "/user_groups/{user_group_id}/update",
125 126 "user_groups_delete": ADMIN_PREFIX + "/user_groups/{user_group_id}/delete",
126 127 "edit_user_group_perms": ADMIN_PREFIX
127 128 + "/user_groups/{user_group_id}/edit/permissions",
128 129 "edit_user_group_perms_update": ADMIN_PREFIX
129 130 + "/user_groups/{user_group_id}/edit/permissions/update",
130 131 "edit_repo_group": "/{repo_group_name}/_edit",
131 132 "edit_repo_group_perms": "/{repo_group_name:}/_settings/permissions",
132 133 "edit_repo_group_perms_update": "/{repo_group_name}/_settings/permissions/update",
133 134 "edit_repo_group_advanced": "/{repo_group_name}/_settings/advanced",
134 135 "edit_repo_group_advanced_delete": "/{repo_group_name}/_settings/advanced/delete",
135 136 "edit_user_ssh_keys": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys",
136 137 "edit_user_ssh_keys_generate_keypair": ADMIN_PREFIX
137 138 + "/users/{user_id}/edit/ssh_keys/generate",
138 139 "edit_user_ssh_keys_add": ADMIN_PREFIX + "/users/{user_id}/edit/ssh_keys/new",
139 140 "edit_user_ssh_keys_delete": ADMIN_PREFIX
140 141 + "/users/{user_id}/edit/ssh_keys/delete",
141 142 "users": ADMIN_PREFIX + "/users",
142 143 "users_data": ADMIN_PREFIX + "/users_data",
143 144 "users_create": ADMIN_PREFIX + "/users/create",
144 145 "users_new": ADMIN_PREFIX + "/users/new",
145 146 "user_edit": ADMIN_PREFIX + "/users/{user_id}/edit",
146 147 "user_edit_advanced": ADMIN_PREFIX + "/users/{user_id}/edit/advanced",
147 148 "user_edit_global_perms": ADMIN_PREFIX
148 149 + "/users/{user_id}/edit/global_permissions",
149 150 "user_edit_global_perms_update": ADMIN_PREFIX
150 151 + "/users/{user_id}/edit/global_permissions/update",
151 152 "user_update": ADMIN_PREFIX + "/users/{user_id}/update",
152 153 "user_delete": ADMIN_PREFIX + "/users/{user_id}/delete",
153 154 "user_create_personal_repo_group": ADMIN_PREFIX
154 155 + "/users/{user_id}/create_repo_group",
155 156 "edit_user_auth_tokens": ADMIN_PREFIX + "/users/{user_id}/edit/auth_tokens",
156 157 "edit_user_auth_tokens_add": ADMIN_PREFIX
157 158 + "/users/{user_id}/edit/auth_tokens/new",
158 159 "edit_user_auth_tokens_delete": ADMIN_PREFIX
159 160 + "/users/{user_id}/edit/auth_tokens/delete",
160 161 "edit_user_emails": ADMIN_PREFIX + "/users/{user_id}/edit/emails",
161 162 "edit_user_emails_add": ADMIN_PREFIX + "/users/{user_id}/edit/emails/new",
162 163 "edit_user_emails_delete": ADMIN_PREFIX + "/users/{user_id}/edit/emails/delete",
163 164 "edit_user_ips": ADMIN_PREFIX + "/users/{user_id}/edit/ips",
164 165 "edit_user_ips_add": ADMIN_PREFIX + "/users/{user_id}/edit/ips/new",
165 166 "edit_user_ips_delete": ADMIN_PREFIX + "/users/{user_id}/edit/ips/delete",
166 167 "edit_user_perms_summary": ADMIN_PREFIX
167 168 + "/users/{user_id}/edit/permissions_summary",
168 169 "edit_user_perms_summary_json": ADMIN_PREFIX
169 170 + "/users/{user_id}/edit/permissions_summary/json",
170 171 "edit_user_audit_logs": ADMIN_PREFIX + "/users/{user_id}/edit/audit",
171 172 "edit_user_audit_logs_download": ADMIN_PREFIX
172 173 + "/users/{user_id}/edit/audit/download",
173 174 "admin_settings": ADMIN_PREFIX + "/settings",
174 175 "admin_settings_update": ADMIN_PREFIX + "/settings/update",
175 176 "admin_settings_global": ADMIN_PREFIX + "/settings/global",
176 177 "admin_settings_global_update": ADMIN_PREFIX + "/settings/global/update",
177 178 "admin_settings_vcs": ADMIN_PREFIX + "/settings/vcs",
178 179 "admin_settings_vcs_update": ADMIN_PREFIX + "/settings/vcs/update",
179 180 "admin_settings_vcs_svn_pattern_delete": ADMIN_PREFIX
180 181 + "/settings/vcs/svn_pattern_delete",
181 182 "admin_settings_mapping": ADMIN_PREFIX + "/settings/mapping",
182 183 "admin_settings_mapping_update": ADMIN_PREFIX + "/settings/mapping/update",
183 184 "admin_settings_visual": ADMIN_PREFIX + "/settings/visual",
184 185 "admin_settings_visual_update": ADMIN_PREFIX + "/settings/visual/update",
185 186 "admin_settings_issuetracker": ADMIN_PREFIX + "/settings/issue-tracker",
186 187 "admin_settings_issuetracker_update": ADMIN_PREFIX
187 188 + "/settings/issue-tracker/update",
188 189 "admin_settings_issuetracker_test": ADMIN_PREFIX
189 190 + "/settings/issue-tracker/test",
190 191 "admin_settings_issuetracker_delete": ADMIN_PREFIX
191 192 + "/settings/issue-tracker/delete",
192 193 "admin_settings_email": ADMIN_PREFIX + "/settings/email",
193 194 "admin_settings_email_update": ADMIN_PREFIX + "/settings/email/update",
194 195 "admin_settings_hooks": ADMIN_PREFIX + "/settings/hooks",
195 196 "admin_settings_hooks_update": ADMIN_PREFIX + "/settings/hooks/update",
196 197 "admin_settings_hooks_delete": ADMIN_PREFIX + "/settings/hooks/delete",
197 198 "admin_settings_search": ADMIN_PREFIX + "/settings/search",
198 199 "admin_settings_labs": ADMIN_PREFIX + "/settings/labs",
199 200 "admin_settings_labs_update": ADMIN_PREFIX + "/settings/labs/update",
200 201 "admin_settings_sessions": ADMIN_PREFIX + "/settings/sessions",
201 202 "admin_settings_sessions_cleanup": ADMIN_PREFIX + "/settings/sessions/cleanup",
202 203 "admin_settings_system": ADMIN_PREFIX + "/settings/system",
203 204 "admin_settings_system_update": ADMIN_PREFIX + "/settings/system/updates",
204 205 "admin_settings_open_source": ADMIN_PREFIX + "/settings/open_source",
205 206 "repo_group_new": ADMIN_PREFIX + "/repo_group/new",
206 207 "repo_group_create": ADMIN_PREFIX + "/repo_group/create",
207 208 "repo_new": ADMIN_PREFIX + "/repos/new",
208 209 "repo_create": ADMIN_PREFIX + "/repos/create",
209 210 "admin_permissions_global": ADMIN_PREFIX + "/permissions/global",
210 211 "admin_permissions_global_update": ADMIN_PREFIX + "/permissions/global/update",
211 212 "admin_permissions_object": ADMIN_PREFIX + "/permissions/object",
212 213 "admin_permissions_object_update": ADMIN_PREFIX + "/permissions/object/update",
213 214 "admin_permissions_ips": ADMIN_PREFIX + "/permissions/ips",
214 215 "admin_permissions_overview": ADMIN_PREFIX + "/permissions/overview",
215 216 "admin_permissions_ssh_keys": ADMIN_PREFIX + "/permissions/ssh_keys",
216 217 "admin_permissions_ssh_keys_data": ADMIN_PREFIX + "/permissions/ssh_keys/data",
217 218 "admin_permissions_ssh_keys_update": ADMIN_PREFIX
218 219 + "/permissions/ssh_keys/update",
219 220 "pullrequest_show": "/{repo_name}/pull-request/{pull_request_id}",
220 221 "pull_requests_global": ADMIN_PREFIX + "/pull-request/{pull_request_id}",
221 222 "pull_requests_global_0": ADMIN_PREFIX + "/pull_requests/{pull_request_id}",
222 223 "pull_requests_global_1": ADMIN_PREFIX + "/pull-requests/{pull_request_id}",
223 224 "notifications_show_all": ADMIN_PREFIX + "/notifications",
224 225 "notifications_mark_all_read": ADMIN_PREFIX + "/notifications_mark_all_read",
225 226 "notifications_show": ADMIN_PREFIX + "/notifications/{notification_id}",
226 227 "notifications_update": ADMIN_PREFIX
227 228 + "/notifications/{notification_id}/update",
228 229 "notifications_delete": ADMIN_PREFIX
229 230 + "/notifications/{notification_id}/delete",
230 231 "my_account": ADMIN_PREFIX + "/my_account/profile",
231 232 "my_account_edit": ADMIN_PREFIX + "/my_account/edit",
232 233 "my_account_update": ADMIN_PREFIX + "/my_account/update",
233 234 "my_account_pullrequests": ADMIN_PREFIX + "/my_account/pull_requests",
234 235 "my_account_pullrequests_data": ADMIN_PREFIX + "/my_account/pull_requests/data",
235 236 "my_account_emails": ADMIN_PREFIX + "/my_account/emails",
236 237 "my_account_emails_add": ADMIN_PREFIX + "/my_account/emails/new",
237 238 "my_account_emails_delete": ADMIN_PREFIX + "/my_account/emails/delete",
238 239 "my_account_password": ADMIN_PREFIX + "/my_account/password",
239 240 "my_account_password_update": ADMIN_PREFIX + "/my_account/password/update",
240 241 "my_account_repos": ADMIN_PREFIX + "/my_account/repos",
241 242 "my_account_watched": ADMIN_PREFIX + "/my_account/watched",
242 243 "my_account_perms": ADMIN_PREFIX + "/my_account/perms",
243 244 "my_account_notifications": ADMIN_PREFIX + "/my_account/notifications",
244 245 "my_account_ssh_keys": ADMIN_PREFIX + "/my_account/ssh_keys",
245 246 "my_account_ssh_keys_generate": ADMIN_PREFIX + "/my_account/ssh_keys/generate",
246 247 "my_account_ssh_keys_add": ADMIN_PREFIX + "/my_account/ssh_keys/new",
247 248 "my_account_ssh_keys_delete": ADMIN_PREFIX + "/my_account/ssh_keys/delete",
248 249 "pullrequest_show_all": "/{repo_name}/pull-request",
249 250 "pullrequest_show_all_data": "/{repo_name}/pull-request-data",
250 251 "bookmarks_home": "/{repo_name}/bookmarks",
251 252 "branches_home": "/{repo_name}/branches",
252 253 "tags_home": "/{repo_name}/tags",
253 254 "repo_changelog": "/{repo_name}/changelog",
254 255 "repo_commits": "/{repo_name}/commits",
255 256 "repo_commits_file": "/{repo_name}/commits/{commit_id}/{f_path}",
256 257 "repo_commits_elements": "/{repo_name}/commits_elements",
257 258 "repo_commit": "/{repo_name}/changeset/{commit_id}",
258 259 "repo_commit_comment_create": "/{repo_name}/changeset/{commit_id}/comment/create",
259 260 "repo_commit_comment_preview": "/{repo_name}/changeset/{commit_id}/comment/preview",
260 261 "repo_commit_comment_delete": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete",
261 262 "repo_commit_comment_edit": "/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit",
262 263 "repo_commit_children": "/{repo_name}/changeset_children/{commit_id}",
263 264 "repo_commit_parents": "/{repo_name}/changeset_parents/{commit_id}",
264 265 "repo_commit_patch": "/{repo_name}/changeset-patch/{commit_id}",
265 266 "repo_commit_download": "/{repo_name}/changeset-download/{commit_id}",
266 267 "repo_commit_data": "/{repo_name}/changeset-data/{commit_id}",
267 268 "repo_compare": "/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}",
268 269 "repo_compare_select": "/{repo_name}/compare",
269 270 "rss_feed_home": "/{repo_name}/feed-rss",
270 271 "atom_feed_home": "/{repo_name}/feed-atom",
271 272 "rss_feed_home_old": "/{repo_name}/feed/rss",
272 273 "atom_feed_home_old": "/{repo_name}/feed/atom",
273 274 "repo_fork_new": "/{repo_name}/fork",
274 275 "repo_fork_create": "/{repo_name}/fork/create",
275 276 "repo_forks_show_all": "/{repo_name}/forks",
276 277 "repo_forks_data": "/{repo_name}/forks/data",
277 278 "edit_repo_issuetracker": "/{repo_name}/settings/issue_trackers",
278 279 "edit_repo_issuetracker_test": "/{repo_name}/settings/issue_trackers/test",
279 280 "edit_repo_issuetracker_delete": "/{repo_name}/settings/issue_trackers/delete",
280 281 "edit_repo_issuetracker_update": "/{repo_name}/settings/issue_trackers/update",
281 282 "edit_repo_maintenance": "/{repo_name}/settings/maintenance",
282 283 "edit_repo_maintenance_execute": "/{repo_name}/settings/maintenance/execute",
283 284 "repo_changelog_file": "/{repo_name}/changelog/{commit_id}/{f_path}",
284 285 "pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
285 286 "pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
286 287 "pullrequest_new": "/{repo_name}/pull-request/new",
287 288 "pullrequest_create": "/{repo_name}/pull-request/create",
288 289 "pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
289 290 "pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
290 291 "pullrequest_delete": "/{repo_name}/pull-request/{pull_request_id}/delete",
291 292 "pullrequest_comment_create": "/{repo_name}/pull-request/{pull_request_id}/comment",
292 293 "pullrequest_comment_delete": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete",
293 294 "pullrequest_comment_edit": "/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit",
294 295 "edit_repo_caches": "/{repo_name}/settings/caches",
295 296 "edit_repo_perms": "/{repo_name}/settings/permissions",
296 297 "edit_repo_fields": "/{repo_name}/settings/fields",
297 298 "edit_repo_remote": "/{repo_name}/settings/remote",
298 299 "edit_repo_statistics": "/{repo_name}/settings/statistics",
299 300 "edit_repo_advanced": "/{repo_name}/settings/advanced",
300 301 "edit_repo_advanced_delete": "/{repo_name}/settings/advanced/delete",
301 302 "edit_repo_advanced_archive": "/{repo_name}/settings/advanced/archive",
302 303 "edit_repo_advanced_fork": "/{repo_name}/settings/advanced/fork",
303 304 "edit_repo_advanced_locking": "/{repo_name}/settings/advanced/locking",
304 305 "edit_repo_advanced_journal": "/{repo_name}/settings/advanced/journal",
305 306 "repo_stats": "/{repo_name}/repo_stats/{commit_id}",
306 307 "repo_refs_data": "/{repo_name}/refs-data",
307 308 "repo_refs_changelog_data": "/{repo_name}/refs-data-changelog",
308 309 "repo_artifacts_stream_store": "/_file_store/stream-upload",
309 310 }
310 311
311 312
312 313 def route_path(name, params=None, **kwargs):
313 314 import urllib.parse
314 315
315 316 base_url = get_url_defs()[name].format(**kwargs)
316 317
317 318 if params:
318 319 base_url = f"{base_url}?{urllib.parse.urlencode(params)}"
319 320 return base_url
General Comments 0
You need to be logged in to leave comments. Login now