Show More
@@ -446,6 +446,16 b' def includeme(config):' | |||
|
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) |
@@ -924,6 +924,26 b' class TestModifyFilesWithWebInterface(ob' | |||
|
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() |
@@ -950,6 +970,48 b' class TestModifyFilesWithWebInterface(ob' | |||
|
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', |
@@ -1341,6 +1341,9 b' class RepoFilesView(RepoAppView):' | |||
|
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) |
@@ -1357,8 +1360,10 b' class RepoFilesView(RepoAppView):' | |||
|
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 | |
@@ -1584,3 +1589,120 b' class RepoFilesView(RepoAppView):' | |||
|
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 | } |
@@ -255,6 +255,31 b' class ScmModel(BaseModel):' | |||
|
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()\ |
@@ -763,24 +788,10 b' class ScmModel(BaseModel):' | |||
|
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 = [] |
@@ -827,23 +838,10 b' class ScmModel(BaseModel):' | |||
|
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(): |
@@ -890,6 +888,39 b' class ScmModel(BaseModel):' | |||
|
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 | """ |
@@ -908,8 +939,8 b' class ScmModel(BaseModel):' | |||
|
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: |
@@ -919,20 +950,8 b' class ScmModel(BaseModel):' | |||
|
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)) |
@@ -36,7 +36,7 b'' | |||
|
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)}"> |
@@ -49,7 +49,11 b'' | |||
|
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} |
@@ -57,8 +61,8 b'' | |||
|
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."> |
@@ -75,6 +79,9 b'' | |||
|
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> |
@@ -83,7 +90,11 b'' | |||
|
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 |
@@ -75,6 +75,7 b' def get_url_defs():' | |||
|
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", |
General Comments 0
You need to be logged in to leave comments.
Login now