# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import mock
import pytest
from collections import OrderedDict
from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
from rhodecode.lib import helpers as h
from rhodecode.lib.ext_json import json
from rhodecode.lib.str_utils import safe_str
from rhodecode.lib.vcs import nodes
from rhodecode.lib.vcs.conf import settings
from rhodecode.model.db import Session, Repository
from rhodecode.tests import assert_session_flash
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
def get_node_history(backend_type):
return {
'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
'git': json.loads(fixture.load_resource('git_node_history_response.json')),
'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
}[backend_type]
def route_path(name, params=None, **kwargs):
import urllib.request
import urllib.parse
import urllib.error
base_url = {
'repo_summary': '/{repo_name}',
'repo_archivefile': '/{repo_name}/archive/{fname}',
'repo_files_diff': '/{repo_name}/diff/{f_path}',
'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
'repo_files:default_commit': '/{repo_name}/files',
'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
'repo_files_upload_file': '/{repo_name}/upload_file/{commit_id}/{f_path}',
'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
return base_url
def assert_files_in_response(response, files, params):
template = (
'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
_assert_items_in_response(response, files, template, params)
def assert_dirs_in_response(response, dirs, params):
template = (
'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
_assert_items_in_response(response, dirs, template, params)
def _assert_items_in_response(response, items, template, params):
for item in items:
item_params = {'name': item}
item_params.update(params)
response.mustcontain(template % item_params)
def assert_timeago_in_response(response, items, params):
for item in items:
response.mustcontain(h.age_component(params['date']))
@pytest.mark.usefixtures("app")
class TestFilesViews(object):
def test_show_files(self, backend):
response = self.app.get(
route_path('repo_files',
repo_name=backend.repo_name,
commit_id='tip', f_path='/'))
commit = backend.repo.get_commit()
params = {
'repo_name': backend.repo_name,
'commit_id': commit.raw_id,
'date': commit.date
}
assert_dirs_in_response(response, ['docs', 'vcs'], params)
files = [
'.gitignore',
'.hgignore',
'.hgtags',
# TODO: missing in Git
# '.travis.yml',
'MANIFEST.in',
'README.rst',
# TODO: File is missing in svn repository
# 'run_test_and_report.sh',
'setup.cfg',
'setup.py',
'test_and_report.sh',
'tox.ini',
]
assert_files_in_response(response, files, params)
assert_timeago_in_response(response, files, params)
def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
repo = backend_hg['subrepos']
response = self.app.get(
route_path('repo_files',
repo_name=repo.repo_name,
commit_id='tip', f_path='/'))
assert_response = response.assert_response()
assert_response.contains_one_link(
'absolute-path @ 000000000000', 'http://example.com/absolute-path')
def test_show_files_links_submodules_with_absolute_url_subpaths(
self, backend_hg):
repo = backend_hg['subrepos']
response = self.app.get(
route_path('repo_files',
repo_name=repo.repo_name,
commit_id='tip', f_path='/'))
assert_response = response.assert_response()
assert_response.contains_one_link(
'subpaths-path @ 000000000000',
'http://sub-base.example.com/subpaths-path')
@pytest.mark.xfail_backends("svn", reason="Depends on branch support")
def test_files_menu(self, backend):
new_branch = "temp_branch_name"
commits = [
{'message': 'a'},
{'message': 'b', 'branch': new_branch}
]
backend.create_repo(commits)
backend.repo.landing_rev = f"branch:{new_branch}"
Session().commit()
# get response based on tip and not new commit
response = self.app.get(
route_path('repo_files',
repo_name=backend.repo_name,
commit_id='tip', f_path='/'))
# make sure Files menu url is not tip but new commit
landing_rev = backend.repo.landing_ref_name
files_url = route_path('repo_files:default_path',
repo_name=backend.repo_name,
commit_id=landing_rev, params={'at': landing_rev})
assert landing_rev != 'tip'
response.mustcontain(f'
add a new file'
repo_file_upload_url = route_path(
'repo_files_upload_file',
repo_name=repo.repo_name,
commit_id=0, f_path='')
upload_new = f'upload a new file'
assert_session_flash(
response,
'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
)
def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
self, backend_stub, autologin_regular_user):
repo = backend_stub.create_repo()
# init session for anon user
route_path('repo_summary', repo_name=repo.repo_name)
repo_file_add_url = route_path(
'repo_files_add_file',
repo_name=repo.repo_name,
commit_id=0, f_path='')
response = self.app.get(
route_path('repo_files',
repo_name=repo.repo_name,
commit_id='tip', f_path='/'))
assert_session_flash(response, no_=repo_file_add_url)
@pytest.mark.parametrize('file_node', [
b'archive/file.zip',
b'diff/my-file.txt',
b'render.py',
b'render',
b'remove_file',
b'remove_file/to-delete.txt',
])
def test_file_names_equal_to_routes_parts(self, backend, file_node):
backend.create_repo()
backend.ensure_file(file_node)
self.app.get(
route_path('repo_files',
repo_name=backend.repo_name,
commit_id='tip', f_path=safe_str(file_node)),
status=200)
class TestAdjustFilePathForSvn(object):
"""
SVN specific adjustments of node history in RepoFilesView.
"""
def test_returns_path_relative_to_matched_reference(self):
repo = self._repo(branches=['trunk'])
self.assert_file_adjustment('trunk/file', 'file', repo)
def test_does_not_modify_file_if_no_reference_matches(self):
repo = self._repo(branches=['trunk'])
self.assert_file_adjustment('notes/file', 'notes/file', repo)
def test_does_not_adjust_partial_directory_names(self):
repo = self._repo(branches=['trun'])
self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
def test_is_robust_to_patterns_which_prefix_other_patterns(self):
repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
self.assert_file_adjustment('trunk/new/file', 'file', repo)
def assert_file_adjustment(self, f_path, expected, repo):
result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
assert result == expected
def _repo(self, branches=None):
repo = mock.Mock()
repo.branches = OrderedDict((name, '0') for name in branches or [])
repo.tags = {}
return repo