From 284f3972844ea124c0cd9ee5500e99440bb96aac Mon Sep 17 00:00:00 2001 From: Christoph Schlosser Date: Sun, 31 Aug 2025 01:09:37 +0200 Subject: g2h: Add file page Also move helpers to dedicated files. --- README.md | 7 +- g2h/cli.py | 2 + g2h/commit_page.py | 2 +- g2h/file.py | 16 ++++ g2h/file_page.py | 45 +++++++++++ g2h/page_factory.py | 8 ++ g2h/tree.py | 2 + g2h/tree_page.py | 31 ++++++-- requirements.txt | 1 + templates/file.md | 13 +++ templates/tree.md | 2 +- test_repo/_gitignore | 1 + test_repo/git.tgz | Bin 22304 -> 41560 bytes test_repo/pic.png | Bin 0 -> 16694 bytes test_repo/src/include/lib.h | 9 +++ test_repo/src/lib.cc | 9 +++ test_repo/src/main.cc | 7 ++ tests/1px.webp | Bin 0 -> 34 bytes tests/base_page_test.py | 19 +++++ tests/commit_page_test.py | 37 +++++++++ tests/commit_test.py | 37 --------- tests/file_page_test.py | 81 +++++++++++++++++++ tests/file_test.py | 26 ++++++ tests/log_page_test.py | 188 ++++++++++++++++++++++++++++++++++++++++++++ tests/log_test.py | 188 -------------------------------------------- tests/overview_page_test.py | 9 +++ tests/overview_test.py | 9 --- tests/page_test.py | 19 ----- tests/tree_page_test.py | 120 ++++++++++++++++++++++++++++ tests/tree_test.py | 106 +++---------------------- 30 files changed, 631 insertions(+), 363 deletions(-) create mode 100644 g2h/file.py create mode 100644 g2h/file_page.py create mode 100644 g2h/tree.py create mode 100644 templates/file.md create mode 100644 test_repo/_gitignore create mode 100644 test_repo/pic.png create mode 100644 test_repo/src/include/lib.h create mode 100644 test_repo/src/lib.cc create mode 100644 test_repo/src/main.cc create mode 100644 tests/1px.webp create mode 100644 tests/base_page_test.py create mode 100644 tests/commit_page_test.py delete mode 100644 tests/commit_test.py create mode 100644 tests/file_page_test.py create mode 100644 tests/file_test.py create mode 100644 tests/log_page_test.py delete mode 100644 tests/log_test.py create mode 100644 tests/overview_page_test.py delete mode 100644 tests/overview_test.py delete mode 100644 tests/page_test.py create mode 100644 tests/tree_page_test.py diff --git a/README.md b/README.md index cb09b0d..0e42056 100644 --- a/README.md +++ b/README.md @@ -54,14 +54,15 @@ For a full list of options, their defaults and how they're changing the behavior Show the state of the tree at the current commit. - [x] Folders should be traversable by clicking on their name. - [ ] Clicking on a file should open that file's page. -- [ ] Individual file page. +- [x] Individual file page. File content in a codeblock with file extension based language detection. For binary files a note that it can't be displayed. - - [ ] Don't treat pictures as binary files. + - [x] Don't treat pictures as binary files. - [ ] Breadcrumbs for navigating. + - [ ] Add button to download raw file. - [ ] Nav bar for navigating the pages mentioned above. - [ ] Link to special readme, if a readme can be found, rendering markdown. -- [ ] Wrapper script that takes care fo `venv` setup. +- [ ] Wrapper script that takes care of `venv` setup. - [ ] Limit to certain branch(es). - [ ] Limit commit range. - [ ] Parallelize generating output. diff --git a/g2h/cli.py b/g2h/cli.py index 52a2be7..fbc456e 100644 --- a/g2h/cli.py +++ b/g2h/cli.py @@ -34,6 +34,8 @@ def main(): tree = factory.new_tree(commit, '', git.get_repo()) for tree_page in tree.build_pages(factory): tree_page.render() + for file in tree_page.get_files(): + factory.new_file(commit, file).render() if __name__ == '__main__': main() diff --git a/g2h/commit_page.py b/g2h/commit_page.py index 6111013..a998417 100644 --- a/g2h/commit_page.py +++ b/g2h/commit_page.py @@ -17,7 +17,7 @@ def redact_email(email): class CommitPage(Page): def __init__(self, out_dir, name, commit, content_subdir): - super(CommitPage, self).__init__('commit.md', out_dir, os.path.join(commit.hexsha, 'index.md'), name, content_subdir) + super(CommitPage, self).__init__('commit.md', out_dir, os.path.join(commit.hexsha, '_index.md'), name, content_subdir) self.commit = commit def context(self): diff --git a/g2h/file.py b/g2h/file.py new file mode 100644 index 0000000..8f683cd --- /dev/null +++ b/g2h/file.py @@ -0,0 +1,16 @@ +import os + +def sanitize_file_name(file_name, files): + if not file_name.startswith('.'): + return file_name + + file_names = [ f.name for f in files ] + for i in range(1, 10): + new_name = file_name.replace('.', '_' * i, 1) + if not new_name in file_names: + return new_name + + raise RuntimeError('Couldn not find unused sanitized file name') + +def tree_join(commit, path): + return os.path.join(commit.hexsha, 't', path) diff --git a/g2h/file_page.py b/g2h/file_page.py new file mode 100644 index 0000000..b154ee5 --- /dev/null +++ b/g2h/file_page.py @@ -0,0 +1,45 @@ +import base64 +import filetype +import os + +from .file import sanitize_file_name, tree_join +from .page import Page +from .tree import safe_join + +def bytes_to_content(file_bytes): + if not file_bytes: + return (None, 'empty') + try: + text = file_bytes.decode('utf-8') + return (text, 'text') + except UnicodeError: + if filetype.is_image(file_bytes): + # TODO: Refer to the static file once that's implemented. + img = base64.b64encode(file_bytes) + return (img.decode('utf-8'), 'image') + + return (None, None) + +class FilePage(Page): + def __init__(self, out_dir, name, commit, file_name, file_path, content_subdir): + tree = safe_join(commit.tree, file_path) + super(FilePage, self).__init__('file.md', out_dir, os.path.join(tree_join(commit, file_path), sanitize_file_name(file_name, tree.blobs) + '.md'), name, content_subdir) + self.commit = commit + self.file_name = file_name + self.file_path = file_path + self.tree = tree + + def read_file(self): + return self.tree.join(self.file_name).data_stream.read() + + def context(self): + file_bytes = self.read_file() + content, file_type = bytes_to_content(file_bytes) + + return { + 'content': content, + 'content_subdir': self.content_subdir, + 'file_extension': os.path.splitext(self.file_name)[1].lstrip('.'), + 'title': self.name + ': ' + self.file_name, + 'type': file_type + } diff --git a/g2h/page_factory.py b/g2h/page_factory.py index c4b89ed..da9c5d5 100644 --- a/g2h/page_factory.py +++ b/g2h/page_factory.py @@ -1,4 +1,7 @@ +import os + from .commit_page import CommitPage +from .file_page import FilePage from .overview_page import OverviewPage from .log_page import LogPage from .tree_page import TreePage @@ -12,6 +15,11 @@ class PageFactory: def new_commit(self, commit): return CommitPage(self.out_dir, self.name, commit, self.content_subdir) + def new_file(self, commit, path): + filename = os.path.basename(path) + dir = os.path.dirname(path) + return FilePage(self.out_dir, self.name, commit, filename, dir, self.content_subdir) + def new_overview(self, branches, tags): return OverviewPage(self.out_dir, self.name, branches, tags, self.content_subdir) diff --git a/g2h/tree.py b/g2h/tree.py new file mode 100644 index 0000000..bd8116c --- /dev/null +++ b/g2h/tree.py @@ -0,0 +1,2 @@ +def safe_join(tree, path): + return tree.join(path) if path else tree diff --git a/g2h/tree_page.py b/g2h/tree_page.py index 977e981..9b7a0d5 100644 --- a/g2h/tree_page.py +++ b/g2h/tree_page.py @@ -1,14 +1,14 @@ import os +from .file import sanitize_file_name, tree_join from .page import Page - -def tree_prefix(sha, tree): - return os.path.join(sha, 't', tree) +from .tree import safe_join class TreePage(Page): def __init__(self, out_dir, name, commit, sub_tree, repo, content_subdir): - super(TreePage, self).__init__('tree.md', out_dir, os.path.join(tree_prefix(commit.hexsha, sub_tree), 'index.md'), name, content_subdir) - self.tree = commit.tree.join(sub_tree) if sub_tree else commit.tree + tree = safe_join(commit.tree, sub_tree) + super(TreePage, self).__init__('tree.md', out_dir, os.path.join(tree_join(commit, tree.path), '_index.md'), name, content_subdir) + self.tree = tree self.repo = repo self.commit = commit self.sub_tree = sub_tree @@ -17,8 +17,16 @@ class TreePage(Page): entries = [] for node in nodes: latest_commit = next(self.repo.iter_commits(self.commit, max_count=1, paths=node.path)) + name = node.name + file_name = name + if node.type == 'tree': + name += '/' + file_name = name + else: + file_name = sanitize_file_name(name, self.tree.blobs) entry = { - 'name': node.name + str('/' if node.type == 'tree' else ''), + 'file_name': file_name, + 'name': name, 'modified': latest_commit.committed_datetime } entries.append(entry) @@ -35,7 +43,8 @@ class TreePage(Page): return { 'content_subdir': self.content_subdir, 'entries': self._get_entries(), - 'subdir': '/'.join([self.content_subdir, tree_prefix(self.commit.hexsha, self.tree.path)]), + # TODO: Make this fit the rest of the system. + 'subdir': '/'.join([self.content_subdir, tree_join(self.commit, self.tree.path)]), 'title': self.name + ': Tree' } @@ -47,10 +56,16 @@ class TreePage(Page): current = remaining.pop() if not current in visited: visited.add(current) - tree = self.commit.tree.join(current) if current else self.commit.tree + tree = safe_join(self.commit.tree, current) tree_pages.append(factory.new_tree(self.commit, tree.path, self.repo)) for sub_tree in tree.trees: remaining.append(sub_tree.path) return tree_pages + def get_files(self): + file_paths = [] + for blob in self.tree.blobs: + file_paths.append(blob.path) + + return file_paths diff --git a/requirements.txt b/requirements.txt index 65a012c..0a0a9e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ GitPython==3.1.45 +filetype==1.2.0 Jinja2==3.1.6 pytest==8.4.1 diff --git a/templates/file.md b/templates/file.md new file mode 100644 index 0000000..12a5a81 --- /dev/null +++ b/templates/file.md @@ -0,0 +1,13 @@ +{% include '_head.md' %} + +{% if type == 'text' %} +```{{ file_extension }} +{{ content }} +``` +{% elif type == 'image' %} +![](data:image/png;base64,{{ content }}) +{% elif type == 'empty' %} +File is empty. +{% else %} +Can't display binary file. +{% endif %} diff --git a/templates/tree.md b/templates/tree.md index 7a891ab..53644bc 100644 --- a/templates/tree.md +++ b/templates/tree.md @@ -4,7 +4,7 @@ | Name | Last modified | |------|---------------| {%- for entry in entries %} -| [{{ entry.name }}](/{{ subdir }}/{{ entry.name}}) | {% if entry.modified %} {{ entry.modified.strftime('%Y-%m-%d %H:%M:%S') }} {% endif %} |{% endfor %} +| [{{ entry.name }}](/{{ subdir }}/{{ entry.file_name}}) | {% if entry.modified %} {{ entry.modified.strftime('%Y-%m-%d %H:%M:%S') }} {% endif %} |{% endfor %} {% else %} No files. {% endif %} diff --git a/test_repo/_gitignore b/test_repo/_gitignore new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test_repo/_gitignore @@ -0,0 +1 @@ +test diff --git a/test_repo/git.tgz b/test_repo/git.tgz index df8887e..bac5752 100644 Binary files a/test_repo/git.tgz and b/test_repo/git.tgz differ diff --git a/test_repo/pic.png b/test_repo/pic.png new file mode 100644 index 0000000..f70142b Binary files /dev/null and b/test_repo/pic.png differ diff --git a/test_repo/src/include/lib.h b/test_repo/src/include/lib.h new file mode 100644 index 0000000..28fbe8b --- /dev/null +++ b/test_repo/src/include/lib.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace lib { + +std::string_view print(); + +} diff --git a/test_repo/src/lib.cc b/test_repo/src/lib.cc new file mode 100644 index 0000000..2083478 --- /dev/null +++ b/test_repo/src/lib.cc @@ -0,0 +1,9 @@ +#include "lib.h" + +#include + +namespace lib { +std::string_view print() { + return "test"; +} +} diff --git a/test_repo/src/main.cc b/test_repo/src/main.cc new file mode 100644 index 0000000..db3a36e --- /dev/null +++ b/test_repo/src/main.cc @@ -0,0 +1,7 @@ +#include "lib.h" + +#include + +int main() { + std::cout << lib::print() << std::endl +} diff --git a/tests/1px.webp b/tests/1px.webp new file mode 100644 index 0000000..15e1c70 Binary files /dev/null and b/tests/1px.webp differ diff --git a/tests/base_page_test.py b/tests/base_page_test.py new file mode 100644 index 0000000..c68e435 --- /dev/null +++ b/tests/base_page_test.py @@ -0,0 +1,19 @@ +from g2h.page import Page +from jinja2.exceptions import TemplateNotFound +import pytest + +def test_init(): + page = Page('overview.md', 'dir', 'file', 'name', 'sub/dir') + assert(page.name == 'name') + assert(page.out_file_path == 'dir/file') + assert(page.template.name == 'overview.md') + assert(page.content_subdir == 'sub/dir') + +def test_wrong_template(): + with pytest.raises(TemplateNotFound): + Page('not existant.md', 'dir', 'file', 'name', '') + +def test_no_context(): + page = Page('overview.md', 'dir', 'file', 'name', '') + with pytest.raises(NotImplementedError): + page.context() diff --git a/tests/commit_page_test.py b/tests/commit_page_test.py new file mode 100644 index 0000000..839d493 --- /dev/null +++ b/tests/commit_page_test.py @@ -0,0 +1,37 @@ +from g2h.commit_page import CommitPage, redact_email +from unittest.mock import MagicMock + +def test_init(): + commit = MagicMock() + commit.hexsha = '123' + page = CommitPage('test/commit', 'commit test', commit, '') + assert(page.name == 'commit test') + assert(page.out_file_path == 'test/commit/123/_index.md') + assert(page.template.name == 'commit.md') + +def test_context(): + commit = MagicMock() + commit.hexsha = '123' + commit.author.email = 'unit@te.st' + page = CommitPage('test/commit', 'commit test', commit, 'commit/content') + context = page.context() + assert(context['title'] == 'commit test: 123') + assert(context['commit'] == commit) + assert(context['redacted_email'] == 'u...t@te...st') + assert(context['content_subdir'] == 'commit/content') + +def test_redacting_email(): + assert(redact_email(None) == '') + assert(redact_email('') == '') + assert(redact_email('a') == 'a...a') + assert(redact_email('az') == 'a...z') + assert(redact_email('root') == 'r...t') + assert(redact_email('root@localhost') == 'r...t@localhost') + assert(redact_email('test@example.com') == 't...t@example...com') + assert(redact_email('test@sub.example.com') == 't...t@sub...example...com') + assert(redact_email('az@example.com') == 'a...z@example...com') + assert(redact_email('a@example.com') == 'a...a@example...com') + assert(redact_email('@example.com') == '@example...com') + assert(redact_email('test@') == 't...t@') + assert(redact_email('az@') == 'a...z@') + assert(redact_email('@') == '@') diff --git a/tests/commit_test.py b/tests/commit_test.py deleted file mode 100644 index b32901e..0000000 --- a/tests/commit_test.py +++ /dev/null @@ -1,37 +0,0 @@ -from g2h.commit_page import CommitPage, redact_email -from unittest.mock import MagicMock - -def test_init(): - commit = MagicMock() - commit.hexsha = '123' - page = CommitPage('test/commit', 'commit test', commit, '') - assert(page.name == 'commit test') - assert(page.out_file_path == 'test/commit/123/index.md') - assert(page.template.name == 'commit.md') - -def test_context(): - commit = MagicMock() - commit.hexsha = '123' - commit.author.email = 'unit@te.st' - page = CommitPage('test/commit', 'commit test', commit, 'commit/content') - context = page.context() - assert(context['title'] == 'commit test: 123') - assert(context['commit'] == commit) - assert(context['redacted_email'] == 'u...t@te...st') - assert(context['content_subdir'] == 'commit/content') - -def test_redacting_email(): - assert(redact_email(None) == '') - assert(redact_email('') == '') - assert(redact_email('a') == 'a...a') - assert(redact_email('az') == 'a...z') - assert(redact_email('root') == 'r...t') - assert(redact_email('root@localhost') == 'r...t@localhost') - assert(redact_email('test@example.com') == 't...t@example...com') - assert(redact_email('test@sub.example.com') == 't...t@sub...example...com') - assert(redact_email('az@example.com') == 'a...z@example...com') - assert(redact_email('a@example.com') == 'a...a@example...com') - assert(redact_email('@example.com') == '@example...com') - assert(redact_email('test@') == 't...t@') - assert(redact_email('az@') == 'a...z@') - assert(redact_email('@') == '@') diff --git a/tests/file_page_test.py b/tests/file_page_test.py new file mode 100644 index 0000000..a1123de --- /dev/null +++ b/tests/file_page_test.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock + +from g2h.file_page import FilePage, bytes_to_content + +def test_init(): + commit = MagicMock() + commit.hexsha = '123' + page = FilePage('test/file', 'file test', commit, '.test_file', 'path/test', '') + assert(page.name == 'file test') + assert(page.out_file_path == 'test/file/123/t/path/test/_test_file.md') + assert(page.template.name == 'file.md') + assert(page.commit == commit) + assert(page.file_name == '.test_file') + assert(page.file_path == 'path/test') + commit.tree.join.assert_called_with('path/test') + +def test_read_file(): + file = MagicMock() + tree = MagicMock() + commit = MagicMock() + commit.hexsha = '123' + commit.tree = tree + commit.tree.join.return_value = file + page = FilePage('test/file', 'file test', commit, 'test_file', '', '') + page.read_file() + tree.join.assert_called_with('test_file') + file.data_stream.read.assert_called() + +def test_context(): + file = MagicMock() + tree = MagicMock() + commit = MagicMock() + commit.hexsha = '123' + commit.tree = tree + commit.tree.join.return_value = file + page = FilePage('test/file', 'file test', commit, 'test_file.txt', '', 'file/content') + file.data_stream.read.return_value = 'test'.encode() + context = page.context() + assert(context['content'] == 'test') + assert(context['content_subdir'] == 'file/content') + assert(context['file_extension'] == 'txt') + assert(context['title'] == 'file test: test_file.txt') + assert(context['type'] == 'text') + +def test_context_dot_file(): + file = MagicMock() + tree = MagicMock() + commit = MagicMock() + commit.hexsha = '123' + commit.tree = tree + commit.tree.join.return_value = file + page = FilePage('test/file', 'file test', commit, '.file', '', 'file/content') + file.data_stream.read.return_value = 'test'.encode() + context = page.context() + assert(context['content'] == 'test') + assert(context['content_subdir'] == 'file/content') + assert(context['file_extension'] == '') + assert(context['title'] == 'file test: .file') + assert(context['type'] == 'text') + +def test_bytes_to_content_text(): + content, type = bytes_to_content('test'.encode()) + assert(content == 'test') + assert(type == 'text') + +def test_bytes_to_content_pic(): + webp_1px = bytes([0x52, 0x49, 0x46, 0x46, 0x1a, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4c, 0x0e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x07, 0x10, 0x11, 0xfd, 0x0f, 0x44, 0x44, 0xff, 0x03, 0x0a]) + content, type = bytes_to_content(webp_1px) + assert(content == 'UklGRhoAAABXRUJQVlA4TA4AAAAvAAAAAAcQEf0PRET/Awo=') + assert(type == 'image') + +def test_bytes_to_content_empty(): + content, type = bytes_to_content(bytes()) + assert(content == None) + assert(type == 'empty') + +def test_bytes_to_content_bin(): + content, type = bytes_to_content(bytes([0xc3, 0x28])) + assert(content == None) + assert(type == None) + diff --git a/tests/file_test.py b/tests/file_test.py new file mode 100644 index 0000000..838df71 --- /dev/null +++ b/tests/file_test.py @@ -0,0 +1,26 @@ +import os +import pytest + +from unittest.mock import Mock + +from g2h.file import sanitize_file_name, tree_join + +def test_sanitize_not_necessary(): + assert(sanitize_file_name('foo', None) == 'foo') + +def test_sanitize_success(): + assert(sanitize_file_name('.test', []) == '_test') + +def test_sanitize_collissions(): + entries = [] + for i in range(1, 10): + entry = Mock() + entry.name = '_' * i + 'test' + entries.append(entry) + with pytest.raises(RuntimeError): + sanitize_file_name('.test', entries) + +def test_tree_join(): + commit = Mock() + commit.hexsha = '123' + assert(tree_join(commit, 'test') == os.path.join('123', 't', 'test')) diff --git a/tests/log_page_test.py b/tests/log_page_test.py new file mode 100644 index 0000000..041544e --- /dev/null +++ b/tests/log_page_test.py @@ -0,0 +1,188 @@ +from g2h.log_page import LogPage +from unittest.mock import MagicMock + +def test_init(): + page = LogPage('test/log', 'log test', None, None, None, '') + assert(page.name == 'log test') + assert(page.out_file_path == 'test/log/l.md') + assert(page.template.name == 'log.md') + +def test_context_no_commits(): + page = LogPage('test/log', 'log test', None, None, None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == []) + +def test_context_no_branches_and_tags(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + page = LogPage('test/log', 'log test', [commit], None, None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) + +def test_context_matching_branch_no_tags(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + branch = MagicMock() + branch.commit.hexsha = commit.hexsha + branch.name = 'mock branch' + page = LogPage('test/log', 'log test', [commit], [branch], None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock branch'], 'summary': 'mock commit'}]) + +def test_context_unmatching_branch_no_tags(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + branch = MagicMock() + branch.commit.hexsha = 4321 + branch.name = 'mock branch' + page = LogPage('test/log', 'log test', [commit], [branch], None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) + +def test_context_no_branch_matching_tag(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + tag = MagicMock() + tag.commit.hexsha = commit.hexsha + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit], None, [tag], '') + context = page.context() + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock tag'], 'summary': 'mock commit'}]) + +def test_context_no_branch_no_matching_tag(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + tag = MagicMock() + tag.commit.hexsha = 4321 + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit], None, [tag], '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) + +def test_context_no_matching_branch_no_matching_tag(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + branch = MagicMock() + branch.commit.hexsha = 321 + branch.name = 'mock branch' + tag = MagicMock() + tag.commit.hexsha = 4321 + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit], [branch], [tag], '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) + +def test_context_matching_branch_matching_tag(): + commit = MagicMock() + commit.summary = 'mock commit' + commit.hexsha = 1234; + commit.committed_datetime = 0 + branch = MagicMock() + branch.commit.hexsha = commit.hexsha + branch.name = 'mock branch' + tag = MagicMock() + tag.commit.hexsha = commit.hexsha + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit], [branch], [tag], '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock branch', 'mock tag'], 'summary': 'mock commit'}]) + +def test_multiple_commits(): + commit1 = MagicMock() + commit1.summary = 'mock commit 1' + commit1.hexsha = 1; + commit1.committed_datetime = 0 + commit2 = MagicMock() + commit2.summary = 'mock commit 2' + commit2.hexsha = 2; + commit2.committed_datetime = 1 + page = LogPage('test/log', 'log test', [commit1, commit2], None, None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [ + {'committed_datetime': 0, 'hexsha': 1, 'refs': [], 'summary': 'mock commit 1'}, + {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} + ]) + +def test_multiple_commits_branch(): + commit1 = MagicMock() + commit1.summary = 'mock commit 1' + commit1.hexsha = 1; + commit1.committed_datetime = 0 + commit2 = MagicMock() + commit2.summary = 'mock commit 2' + commit2.hexsha = 2; + commit2.committed_datetime = 1 + branch = MagicMock() + branch.commit.hexsha = 1 + branch.name = 'mock branch' + page = LogPage('test/log', 'log test', [commit1, commit2], [branch], None, '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [ + {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock branch'], 'summary': 'mock commit 1'}, + {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} + ]) + +def test_multiple_commits_tag(): + commit1 = MagicMock() + commit1.summary = 'mock commit 1' + commit1.hexsha = 1; + commit1.committed_datetime = 0 + commit2 = MagicMock() + commit2.summary = 'mock commit 2' + commit2.hexsha = 2; + commit2.committed_datetime = 1 + tag = MagicMock() + tag.commit.hexsha = 1 + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit1, commit2], None, [tag], '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [ + {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock tag'], 'summary': 'mock commit 1'}, + {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} + ]) + +def test_multiple_commits_branch_and_tag(): + commit1 = MagicMock() + commit1.summary = 'mock commit 1' + commit1.hexsha = 1; + commit1.committed_datetime = 0 + commit2 = MagicMock() + commit2.summary = 'mock commit 2' + commit2.hexsha = 2; + commit2.committed_datetime = 1 + branch = MagicMock() + branch.commit.hexsha = 1 + branch.name = 'mock branch' + tag = MagicMock() + tag.commit.hexsha = 2 + tag.name = 'mock tag' + page = LogPage('test/log', 'log test', [commit1, commit2], [branch], [tag], '') + context = page.context() + assert(context['title'] == 'log test: Log') + assert(context['commits'] == [ + {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock branch'], 'summary': 'mock commit 1'}, + {'committed_datetime': 1, 'hexsha': 2, 'refs': ['mock tag'], 'summary': 'mock commit 2'} + ]) + diff --git a/tests/log_test.py b/tests/log_test.py deleted file mode 100644 index 041544e..0000000 --- a/tests/log_test.py +++ /dev/null @@ -1,188 +0,0 @@ -from g2h.log_page import LogPage -from unittest.mock import MagicMock - -def test_init(): - page = LogPage('test/log', 'log test', None, None, None, '') - assert(page.name == 'log test') - assert(page.out_file_path == 'test/log/l.md') - assert(page.template.name == 'log.md') - -def test_context_no_commits(): - page = LogPage('test/log', 'log test', None, None, None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == []) - -def test_context_no_branches_and_tags(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - page = LogPage('test/log', 'log test', [commit], None, None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) - -def test_context_matching_branch_no_tags(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - branch = MagicMock() - branch.commit.hexsha = commit.hexsha - branch.name = 'mock branch' - page = LogPage('test/log', 'log test', [commit], [branch], None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock branch'], 'summary': 'mock commit'}]) - -def test_context_unmatching_branch_no_tags(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - branch = MagicMock() - branch.commit.hexsha = 4321 - branch.name = 'mock branch' - page = LogPage('test/log', 'log test', [commit], [branch], None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) - -def test_context_no_branch_matching_tag(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - tag = MagicMock() - tag.commit.hexsha = commit.hexsha - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit], None, [tag], '') - context = page.context() - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock tag'], 'summary': 'mock commit'}]) - -def test_context_no_branch_no_matching_tag(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - tag = MagicMock() - tag.commit.hexsha = 4321 - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit], None, [tag], '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) - -def test_context_no_matching_branch_no_matching_tag(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - branch = MagicMock() - branch.commit.hexsha = 321 - branch.name = 'mock branch' - tag = MagicMock() - tag.commit.hexsha = 4321 - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit], [branch], [tag], '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': [], 'summary': 'mock commit'}]) - -def test_context_matching_branch_matching_tag(): - commit = MagicMock() - commit.summary = 'mock commit' - commit.hexsha = 1234; - commit.committed_datetime = 0 - branch = MagicMock() - branch.commit.hexsha = commit.hexsha - branch.name = 'mock branch' - tag = MagicMock() - tag.commit.hexsha = commit.hexsha - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit], [branch], [tag], '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [{'committed_datetime': 0, 'hexsha': 1234, 'refs': ['mock branch', 'mock tag'], 'summary': 'mock commit'}]) - -def test_multiple_commits(): - commit1 = MagicMock() - commit1.summary = 'mock commit 1' - commit1.hexsha = 1; - commit1.committed_datetime = 0 - commit2 = MagicMock() - commit2.summary = 'mock commit 2' - commit2.hexsha = 2; - commit2.committed_datetime = 1 - page = LogPage('test/log', 'log test', [commit1, commit2], None, None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [ - {'committed_datetime': 0, 'hexsha': 1, 'refs': [], 'summary': 'mock commit 1'}, - {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} - ]) - -def test_multiple_commits_branch(): - commit1 = MagicMock() - commit1.summary = 'mock commit 1' - commit1.hexsha = 1; - commit1.committed_datetime = 0 - commit2 = MagicMock() - commit2.summary = 'mock commit 2' - commit2.hexsha = 2; - commit2.committed_datetime = 1 - branch = MagicMock() - branch.commit.hexsha = 1 - branch.name = 'mock branch' - page = LogPage('test/log', 'log test', [commit1, commit2], [branch], None, '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [ - {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock branch'], 'summary': 'mock commit 1'}, - {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} - ]) - -def test_multiple_commits_tag(): - commit1 = MagicMock() - commit1.summary = 'mock commit 1' - commit1.hexsha = 1; - commit1.committed_datetime = 0 - commit2 = MagicMock() - commit2.summary = 'mock commit 2' - commit2.hexsha = 2; - commit2.committed_datetime = 1 - tag = MagicMock() - tag.commit.hexsha = 1 - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit1, commit2], None, [tag], '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [ - {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock tag'], 'summary': 'mock commit 1'}, - {'committed_datetime': 1, 'hexsha': 2, 'refs': [], 'summary': 'mock commit 2'} - ]) - -def test_multiple_commits_branch_and_tag(): - commit1 = MagicMock() - commit1.summary = 'mock commit 1' - commit1.hexsha = 1; - commit1.committed_datetime = 0 - commit2 = MagicMock() - commit2.summary = 'mock commit 2' - commit2.hexsha = 2; - commit2.committed_datetime = 1 - branch = MagicMock() - branch.commit.hexsha = 1 - branch.name = 'mock branch' - tag = MagicMock() - tag.commit.hexsha = 2 - tag.name = 'mock tag' - page = LogPage('test/log', 'log test', [commit1, commit2], [branch], [tag], '') - context = page.context() - assert(context['title'] == 'log test: Log') - assert(context['commits'] == [ - {'committed_datetime': 0, 'hexsha': 1, 'refs': ['mock branch'], 'summary': 'mock commit 1'}, - {'committed_datetime': 1, 'hexsha': 2, 'refs': ['mock tag'], 'summary': 'mock commit 2'} - ]) - diff --git a/tests/overview_page_test.py b/tests/overview_page_test.py new file mode 100644 index 0000000..06eea97 --- /dev/null +++ b/tests/overview_page_test.py @@ -0,0 +1,9 @@ +from g2h.overview_page import OverviewPage + +def test_init(): + page = OverviewPage('test/overview', 'overview test', 'test branch', 'test tag', 'overview/sub/dir') + context = page.context() + assert(context['title'] == 'overview test') + assert(context['branches'] == 'test branch') + assert(context['tags'] == 'test tag') + assert(context['content_subdir'] == 'overview/sub/dir') diff --git a/tests/overview_test.py b/tests/overview_test.py deleted file mode 100644 index 06eea97..0000000 --- a/tests/overview_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from g2h.overview_page import OverviewPage - -def test_init(): - page = OverviewPage('test/overview', 'overview test', 'test branch', 'test tag', 'overview/sub/dir') - context = page.context() - assert(context['title'] == 'overview test') - assert(context['branches'] == 'test branch') - assert(context['tags'] == 'test tag') - assert(context['content_subdir'] == 'overview/sub/dir') diff --git a/tests/page_test.py b/tests/page_test.py deleted file mode 100644 index c68e435..0000000 --- a/tests/page_test.py +++ /dev/null @@ -1,19 +0,0 @@ -from g2h.page import Page -from jinja2.exceptions import TemplateNotFound -import pytest - -def test_init(): - page = Page('overview.md', 'dir', 'file', 'name', 'sub/dir') - assert(page.name == 'name') - assert(page.out_file_path == 'dir/file') - assert(page.template.name == 'overview.md') - assert(page.content_subdir == 'sub/dir') - -def test_wrong_template(): - with pytest.raises(TemplateNotFound): - Page('not existant.md', 'dir', 'file', 'name', '') - -def test_no_context(): - page = Page('overview.md', 'dir', 'file', 'name', '') - with pytest.raises(NotImplementedError): - page.context() diff --git a/tests/tree_page_test.py b/tests/tree_page_test.py new file mode 100644 index 0000000..891e940 --- /dev/null +++ b/tests/tree_page_test.py @@ -0,0 +1,120 @@ +import os +from pytest import fail +from unittest.mock import MagicMock + +from g2h.tree_page import TreePage + +def test_init(): + repo = MagicMock() + commit = MagicMock() + commit.hexsha = '123' + page = TreePage('test/tree', 'tree test', commit, 'test/sub/tree', repo, '') + assert(page.name == 'tree test') + assert(page.repo == repo) + assert(page.commit == commit) + commit.tree.join.assert_called_once_with('test/sub/tree') + commit_no_subtree = MagicMock() + commit_no_subtree.hexsha = '123' + commit_no_subtree.tree = MagicMock() + commit_no_subtree.tree.path = 'no sub tree' + page_no_subtree = TreePage('test/tree', 'tree test', commit_no_subtree, '', repo, '') + assert(page_no_subtree.name == 'tree test') + assert(page_no_subtree.repo == repo) + assert(page_no_subtree.commit == commit_no_subtree) + assert(page_no_subtree.tree.path == 'no sub tree') + +def add_nodes(parent, paths): + nodes = [] + for node in paths: + nodes.append(add_node(parent, node)) + return nodes + +def add_node(parent, path): + node = MagicMock() + node.trees = [] + node.blobs = [] + node.name = path + node.path = os.path.join(parent.path, path) + if path.endswith('/'): + parent.trees.append(node) + node.type = 'tree' + else: + parent.blobs.append(node) + node.type = 'blob' + return node + +def test_context_empty(): + commit = MagicMock() + commit.hexsha = '123' + commit.tree.path = 'tree/path' + page = TreePage('test/tree', 'tree test', commit, '', None, '') + context = page.context() + assert(context['entries'] == []) + assert(context['content_subdir'] == '') + assert(context['subdir'] == '/123/t/tree/path') + assert(context['title'] == 'tree test: Tree') + +def test_context_entries(): + tree = MagicMock() + tree.trees = [] + tree.blobs = [] + repo = MagicMock() + sub_dir = add_node(tree, 'sub/') + add_node(sub_dir, 'file.txt') + add_node(tree, 'README') + add_node(tree, '.gitignore') + commit = MagicMock() + commit.hexsha = '123' + commit.tree = tree + commit.committed_datetime = 'Date Time' + repo.iter_commits = MagicMock(return_value=iter([commit, commit, commit])) + page = TreePage('test/tree', 'tree test', commit, '', repo, '') + context = page.context() + assert(repo.iter_commits.called) + assert(context['entries'] == [{'file_name': 'sub//', 'name': 'sub//', 'modified': 'Date Time'}, {'file_name': '_gitignore', 'name': '.gitignore', 'modified': 'Date Time'}, {'file_name': 'README', 'name': 'README', 'modified': 'Date Time'}]) + +def test_traversal(): + factory = MagicMock() + def new_tree_sideeffect(*args, **kwargs): + return args[1] + factory.new_tree.side_effect = new_tree_sideeffect + tree = MagicMock() + tree.trees = [] + tree.blobs = [] + tree.path = '' + root_tree = add_nodes(tree, ['README', 'sub/']) + sub_tree = add_nodes(root_tree[1], ['main.cc', 'lib.cc', 'include/']) + add_node(sub_tree[2], 'lib.h') + def tree_join_sideeffect(*args, **kwargs): + if args[0] == 'sub/': + return root_tree[1] + elif args[0] == 'sub/include/': + return sub_tree[2] + fail('Unexpected tree join: ' + args[0]) + tree.join.side_effect = tree_join_sideeffect + commit = MagicMock() + commit.tree = tree + page = TreePage('test/tree', 'tree test', commit, '', None, '') + pages = page.build_pages(factory) + assert(len(pages) == 3) + assert(pages[0] == '') + assert(pages[1] == 'sub/') + assert(pages[2] == 'sub/include/') + +def test_get_files(): + tree = MagicMock() + tree.trees = [] + tree.blobs = [] + tree.path = '' + commit = MagicMock() + commit.hexsha = '123' + commit.tree = tree + expected = [] + for case in [ None, '.file', 'README', 'main.cc' ]: + print(str(case)) + if case: + add_node(tree, case) + expected.append(case) + page = TreePage('test/tree', 'tree test', commit, '', None, '') + assert(page.get_files() == expected) + diff --git a/tests/tree_test.py b/tests/tree_test.py index e7d23a8..7ec0f0e 100644 --- a/tests/tree_test.py +++ b/tests/tree_test.py @@ -1,100 +1,12 @@ -import os -from pytest import fail -from unittest.mock import MagicMock +from unittest.mock import Mock -from g2h.tree_page import TreePage +from g2h.tree import safe_join -def test_init(): - repo = MagicMock() - commit = MagicMock() - commit.hexsha = '123' - page = TreePage('test/tree', 'tree test', commit, 'test/sub/tree', repo, '') - assert(page.name == 'tree test') - assert(page.repo == repo) - assert(page.commit == commit) - commit.tree.join.assert_called_once_with('test/sub/tree') - commit_no_subtree = MagicMock() - commit_no_subtree.hexsha = '123' - commit_no_subtree.tree = 'no sub tree' - page_no_subtree = TreePage('test/tree', 'tree test', commit_no_subtree, '', repo, '') - assert(page_no_subtree.name == 'tree test') - assert(page_no_subtree.repo == repo) - assert(page_no_subtree.commit == commit_no_subtree) - assert(page_no_subtree.tree == 'no sub tree') - -def add_nodes(parent, paths): - nodes = [] - for node in paths: - nodes.append(add_node(parent, node)) - return nodes - -def add_node(parent, path): - node = MagicMock() - node.trees = [] - node.blobs = [] - node.name = path - node.path = os.path.join(parent.path, path) - if path.endswith('/'): - parent.trees.append(node) - else: - parent.blobs.append(node) - return node - -def test_context_empty(): - commit = MagicMock() - commit.hexsha = '123' - commit.tree.path = 'tree/path' - page = TreePage('test/tree', 'tree test', commit, '', None, '') - context = page.context() - assert(context['entries'] == []) - assert(context['content_subdir'] == '') - assert(context['subdir'] == '/123/t/tree/path') - assert(context['title'] == 'tree test: Tree') - -def test_context_entries(): - tree = MagicMock() - tree.trees = [] - tree.blobs = [] - repo = MagicMock() - sub_dir = add_node(tree, 'sub/') - sub_file = add_node(sub_dir, 'file.txt') - readme = add_node(tree, 'README') - commit = MagicMock() - commit.hexsha = '123' - commit.author.email = 'unit@te.st' - commit.tree = tree - commit.committed_datetime = 'Date Time' - repo.iter_commits = MagicMock(return_value=iter([commit, commit])) - page = TreePage('test/tree', 'tree test', commit, '', repo, '') - context = page.context() - assert(repo.iter_commits.called) - assert(context['entries'] == [{'name': 'sub/', 'modified': 'Date Time'}, {'name': 'README', 'modified': 'Date Time'}]) - -def test_traversal(): - factory = MagicMock() - def new_tree_sideeffect(*args, **kwargs): - return args[1] - factory.new_tree.side_effect = new_tree_sideeffect - tree = MagicMock() - tree.trees = [] - tree.blobs = [] - tree.path = '' - root_tree = add_nodes(tree, ['README', 'sub/']) - sub_tree = add_nodes(root_tree[1], ['main.cc', 'lib.cc', 'include/']) - add_node(sub_tree[2], 'lib.h') - def tree_join_sideeffect(*args, **kwargs): - if args[0] == 'sub/': - return root_tree[1] - elif args[0] == 'sub/include/': - return sub_tree[2] - fail('Unexpected tree join: ' + args[0]) - tree.join.side_effect = tree_join_sideeffect - commit = MagicMock() - commit.tree = tree - page = TreePage('test/tree', 'tree test', commit, '', None, '') - pages = page.build_pages(factory) - assert(len(pages) == 3) - assert(pages[0] == '') - assert(pages[1] == 'sub/') - assert(pages[2] == 'sub/include/') +def test_safe_join_empty(): + tree = Mock() + assert(safe_join(tree, '') == tree) +def test_safe_join_non_empty(): + tree = Mock() + safe_join(tree, 'test') + tree.join.assert_called_with('test') -- cgit v1.2.3