diff options
| author | Christoph Schlosser <christoph@linux.com> | 2025-08-31 01:09:37 +0200 |
|---|---|---|
| committer | Christoph Schlosser <christoph@linux.com> | 2025-08-31 15:53:33 +0200 |
| commit | 284f3972844ea124c0cd9ee5500e99440bb96aac (patch) | |
| tree | b8dee22222c38b12cc00daf98280e1b2060bcca7 | |
| parent | b06dbf67b9ac9e329922d7911ee7a9b09419c5f0 (diff) | |
| download | git2hugo-284f3972844ea124c0cd9ee5500e99440bb96aac.tar.gz | |
g2h: Add file page
Also move helpers to dedicated files.
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | g2h/cli.py | 2 | ||||
| -rw-r--r-- | g2h/commit_page.py | 2 | ||||
| -rw-r--r-- | g2h/file.py | 16 | ||||
| -rw-r--r-- | g2h/file_page.py | 45 | ||||
| -rw-r--r-- | g2h/page_factory.py | 8 | ||||
| -rw-r--r-- | g2h/tree.py | 2 | ||||
| -rw-r--r-- | g2h/tree_page.py | 31 | ||||
| -rw-r--r-- | requirements.txt | 1 | ||||
| -rw-r--r-- | templates/file.md | 13 | ||||
| -rw-r--r-- | templates/tree.md | 2 | ||||
| -rw-r--r-- | test_repo/_gitignore | 1 | ||||
| -rw-r--r-- | test_repo/git.tgz | bin | 22304 -> 41560 bytes | |||
| -rw-r--r-- | test_repo/pic.png | bin | 0 -> 16694 bytes | |||
| -rw-r--r-- | test_repo/src/include/lib.h | 9 | ||||
| -rw-r--r-- | test_repo/src/lib.cc | 9 | ||||
| -rw-r--r-- | test_repo/src/main.cc | 7 | ||||
| -rw-r--r-- | tests/1px.webp | bin | 0 -> 34 bytes | |||
| -rw-r--r-- | tests/base_page_test.py (renamed from tests/page_test.py) | 0 | ||||
| -rw-r--r-- | tests/commit_page_test.py (renamed from tests/commit_test.py) | 2 | ||||
| -rw-r--r-- | tests/file_page_test.py | 81 | ||||
| -rw-r--r-- | tests/file_test.py | 26 | ||||
| -rw-r--r-- | tests/log_page_test.py (renamed from tests/log_test.py) | 0 | ||||
| -rw-r--r-- | tests/overview_page_test.py (renamed from tests/overview_test.py) | 0 | ||||
| -rw-r--r-- | tests/tree_page_test.py | 120 | ||||
| -rw-r--r-- | tests/tree_test.py | 106 |
26 files changed, 379 insertions, 111 deletions
@@ -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. @@ -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' %} + +{% 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 Binary files differindex df8887e..bac5752 100644 --- a/test_repo/git.tgz +++ b/test_repo/git.tgz diff --git a/test_repo/pic.png b/test_repo/pic.png Binary files differnew file mode 100644 index 0000000..f70142b --- /dev/null +++ b/test_repo/pic.png 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 <string_view> + +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 <string_view> + +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 <iostream> + +int main() { + std::cout << lib::print() << std::endl +} diff --git a/tests/1px.webp b/tests/1px.webp Binary files differnew file mode 100644 index 0000000..15e1c70 --- /dev/null +++ b/tests/1px.webp diff --git a/tests/page_test.py b/tests/base_page_test.py index c68e435..c68e435 100644 --- a/tests/page_test.py +++ b/tests/base_page_test.py diff --git a/tests/commit_test.py b/tests/commit_page_test.py index b32901e..839d493 100644 --- a/tests/commit_test.py +++ b/tests/commit_page_test.py @@ -6,7 +6,7 @@ def test_init(): 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.out_file_path == 'test/commit/123/_index.md') assert(page.template.name == 'commit.md') def test_context(): 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_test.py b/tests/log_page_test.py index 041544e..041544e 100644 --- a/tests/log_test.py +++ b/tests/log_page_test.py diff --git a/tests/overview_test.py b/tests/overview_page_test.py index 06eea97..06eea97 100644 --- a/tests/overview_test.py +++ b/tests/overview_page_test.py 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') |