diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -947,10 +947,97 @@ class ReadmeFinder: different. """ - def __init__(self, default_renderer): + readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE) + path_re = re.compile(r'^docs?', re.IGNORECASE) + + default_priorities = { + None: 0, + '.text': 2, + '.txt': 3, + '.rst': 1, + '.rest': 2, + '.md': 1, + '.mkdn': 2, + '.mdown': 3, + '.markdown': 4, + } + + path_priority = { + 'doc': 0, + 'docs': 1, + } + + FALLBACK_PRIORITY = 99 + + RENDERER_TO_EXTENSION = { + 'rst': ['.rst', '.rest'], + 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'], + } + + def __init__(self, default_renderer=None): self._default_renderer = default_renderer + self._renderer_extensions = self.RENDERER_TO_EXTENSION.get( + default_renderer, []) - def search(self, commit): + def search(self, commit, path='/'): + """ + Find a readme in the given `commit`. + """ + nodes = commit.get_nodes(path) + matches = self._match_readmes(nodes) + matches = self._sort_according_to_priority(matches) + if matches: + return matches[0].path + + paths = self._match_paths(nodes) + paths = self._sort_paths_according_to_priority(paths) + for path in paths: + match = self.search(commit, path=path) + if match: + return match + + return None + + def _match_readmes(self, nodes): + for node in nodes: + if not node.is_file(): + continue + path = node.path.rsplit('/', 1)[-1] + match = self.readme_re.match(path) + if match: + extension = match.group(1) + yield ReadmeMatch(node, match, self._priority(extension)) + + def _match_paths(self, nodes): + for node in nodes: + if not node.is_dir(): + continue + match = self.path_re.match(node.path) + if match: + yield node.path + + def _priority(self, extension): + renderer_priority = ( + 0 if extension in self._renderer_extensions else 1) + extension_priority = self.default_priorities.get( + extension, self.FALLBACK_PRIORITY) + return (renderer_priority, extension_priority) + + def _sort_according_to_priority(self, matches): + + def priority_and_path(match): + return (match.priority, match.path) + + return sorted(matches, key=priority_and_path) + + def _sort_paths_according_to_priority(self, paths): + + def priority_and_path(path): + return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path) + + return sorted(paths, key=priority_and_path) + + def search_old(self, commit): """ Try to find a readme in the given `commit`. """ @@ -966,3 +1053,18 @@ class ReadmeFinder: continue return f + + +class ReadmeMatch: + + def __init__(self, node, match, priority): + self._node = node + self._match = match + self.priority = priority + + @property + def path(self): + return self._node.path + + def __repr__(self): + return '. +# +# 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 pytest + +from rhodecode.lib.vcs import nodes +from rhodecode.model.repo import ReadmeFinder + + +@pytest.fixture +def commit_util(vcsbackend_stub): + """ + Provide a commit which has certain files in it's tree. + + This is based on the fixture "vcsbackend" and will automatically be + parametrized for all vcs backends. + """ + return CommitUtility(vcsbackend_stub) + + +class CommitUtility: + + def __init__(self, vcsbackend): + self.vcsbackend = vcsbackend + + def commit_with_files(self, filenames): + commits = [ + {'message': 'Adding all requested files', + 'added': [ + nodes.FileNode(filename, content='') + for filename in filenames + ]}] + repo = self.vcsbackend.create_repo(commits=commits) + return repo.get_commit() + + +def test_no_matching_file_returns_none(commit_util): + commit = commit_util.commit_with_files(['LIESMICH']) + finder = ReadmeFinder(default_renderer='rst') + filename = finder.search(commit) + assert filename is None + + +def test_matching_file_returns_the_file_name(commit_util): + commit = commit_util.commit_with_files(['README']) + finder = ReadmeFinder(default_renderer='rst') + filename = finder.search(commit) + assert filename == 'README' + + +def test_matching_file_with_extension(commit_util): + commit = commit_util.commit_with_files(['README.rst']) + finder = ReadmeFinder(default_renderer='rst') + filename = finder.search(commit) + assert filename == 'README.rst' + + +def test_prefers_readme_without_extension(commit_util): + commit = commit_util.commit_with_files(['README.rst', 'Readme']) + finder = ReadmeFinder() + filename = finder.search(commit) + assert filename == 'Readme' + + +@pytest.mark.parametrize('renderer, expected', [ + ('rst', 'readme.rst'), + ('markdown', 'readme.md'), +]) +def test_prefers_renderer_extensions(commit_util, renderer, expected): + commit = commit_util.commit_with_files( + ['readme.rst', 'readme.md', 'readme.txt']) + finder = ReadmeFinder(default_renderer=renderer) + filename = finder.search(commit) + assert filename == expected + + +def test_finds_readme_in_subdirectory(commit_util): + commit = commit_util.commit_with_files(['doc/README.rst', 'LIESMICH']) + finder = ReadmeFinder() + filename = finder.search(commit) + assert filename == 'doc/README.rst' + + +def test_prefers_subdirectory_with_priority(commit_util): + commit = commit_util.commit_with_files( + ['Doc/Readme.rst', 'Docs/Readme.rst']) + finder = ReadmeFinder() + filename = finder.search(commit) + assert filename == 'Doc/Readme.rst'