summary refs log tree commit diff stats
path: root/git-hooks/post-receive
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2021-09-10 07:56:19 -0300
committerSoniEx2 <endermoneymod@gmail.com>2021-09-10 11:26:19 -0300
commit886b8bd893b8f0186d508d8ae295a6974829bdc7 (patch)
tree8018a1b040996107113d9383682d25a4b794765c /git-hooks/post-receive
[Project] HTMLGDump
A static git repository browser, powered by symlinks.
Diffstat (limited to 'git-hooks/post-receive')
-rwxr-xr-xgit-hooks/post-receive286
1 files changed, 286 insertions, 0 deletions
diff --git a/git-hooks/post-receive b/git-hooks/post-receive
new file mode 100755
index 0000000..e21caef
--- /dev/null
+++ b/git-hooks/post-receive
@@ -0,0 +1,286 @@
+#!/usr/bin/env python
+
+# HTMLGDump - dumps a git repo to html (and symlinks)
+# Copyright (C) 2021  Soni L.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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 Affero 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 <https://www.gnu.org/licenses/>.
+
+# tl;dr: install this as a git hook (post-receive)
+# then configure your webserver and stuff
+
+import dataclasses
+import os
+import os.path
+import pathlib
+import shutil
+import subprocess
+import sys
+from urllib.parse import quote
+
+import pygit2
+
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_for_filename
+import pygments.util
+
+@dataclasses.dataclass
+class GitChange:
+    old_value: str
+    new_value: str
+    ref_name: str
+    deleting: bool = dataclasses.field(init=False)
+
+    def __post_init__(self):
+        self.deleting = self.new_value == "0"*40 or self.new_value == "0"*64
+
+def get_relative(path, target):
+    """Makes target relative to path, without filesystem operations."""
+    return os.path.relpath(target, start=path)
+
+CACHE_HOME = os.environ.get('XDG_CACHE_HOME', '')
+if not CACHE_HOME:
+    CACHE_HOME = os.environ['HOME'] + '/.cache'
+CACHE_HOME = CACHE_HOME + "/htmlgdump"
+
+# post-receive runs on $GIT_DIR
+repo = pygit2.Repository(os.getcwd())
+
+try:
+    name = pathlib.Path.cwd().relative_to(repo.config["htmlgdump.base"])
+except (KeyError, ValueError):
+    exit()
+
+changes = [GitChange(*l.rstrip("\n").split(" ", 2)) for l in sys.stdin]
+
+gen_dir = pathlib.Path(CACHE_HOME) / name / "gen"
+gen_dir.mkdir(parents=True,exist_ok=True)
+
+todocommits = set()
+
+print("updating refs")
+
+# build changed refs
+for c in changes:
+    path = gen_dir / c.ref_name
+    if c.deleting:
+        try:
+            shutil.rmtree(path)
+        except FileNotFoundError:
+            pass
+    else:
+        path.mkdir(parents=True,exist_ok=True)
+        index = path / "index.html"
+        link = path / "tree"
+        tree = gen_dir / "trees" / str(repo[c.new_value].tree_id)
+        with index.open("w") as f:
+            # TODO
+            f.write("<html><head><title>ref</title><body><a href=\"./tree\">view tree</a></body></html>")
+        todocommits.add(repo[c.new_value])
+        linktarget = get_relative(path, tree)
+        link.unlink(missing_ok=True)
+        link.symlink_to(linktarget, target_is_directory=True)
+
+print("generating refs")
+
+# create missing refs
+for ref in repo.references:
+    ref = repo.references.get(ref)
+    path = gen_dir / ref.name
+    path.mkdir(parents=True,exist_ok=True)
+    index = path / "index.html"
+    link = path / "tree"
+    tree = gen_dir / "trees" / str(ref.peel(pygit2.Commit).tree_id)
+    try:
+        f = index.open("x")
+    except FileExistsError:
+        # check if we've already visited this commit
+        continue
+    with f:
+        # TODO
+        f.write("<html><head><title>ref</title><body><a href=\"./tree\">view tree</a></body></html>")
+    todocommits.add(ref.peel(pygit2.Commit))
+    linktarget = get_relative(path, tree)
+    link.symlink_to(linktarget, target_is_directory=True)
+
+todotrees = set()
+
+print("generating commits")
+
+# build commits
+while todocommits:
+    c = todocommits.pop()
+    path = gen_dir / "commits" / str(c.id)
+    path.mkdir(parents=True,exist_ok=True)
+    index = path / "index.html"
+    link = path / "tree"
+    tree = gen_dir / "trees" / str(c.tree_id)
+    try:
+        f = index.open("x")
+    except FileExistsError:
+        # check if we've already visited this commit
+        continue
+    with f:
+        # TODO
+        f.write("<html><head><title>commit</title><body><a href=\"./tree\">view tree</a></body></html>")
+    todotrees.add(c.tree)
+    todocommits.update(c.parents)
+    linktarget = get_relative(path, tree)
+    link.symlink_to(linktarget, target_is_directory=True)
+
+# a dict /!\
+# maps blobs to some metadata
+# FIXME this can get quite expensive with larger repos, and might even run out
+# of RAM.
+todoblobs = {}
+
+print("generating trees")
+
+# build trees
+while todotrees:
+    t = todotrees.pop()
+    path = gen_dir / "trees" / str(t.id)
+    path.mkdir(parents=True,exist_ok=True)
+    index = path / "index.html"
+    try:
+        f = index.open("x")
+    except FileExistsError:
+        # check if we've already visited this tree
+        continue
+    with f:
+        f.write("<html><head><title>tree</title><body><ul>")
+        for obj in t:
+            linkname = obj.name
+            # a git repo can contain any file, including index.html among
+            # others, but you can never make a file conflict with the id of
+            # the tree it's in. (or at least, it's impractical to do so.)
+            # hashes are kinda awesome!
+            # so we just mangle those to not conflict with our own index.html
+            # note that this does mean the index.html files cannot be easily
+            # permalinked, sorry.
+            if linkname == "index.html":
+                linkname = str(t.id) + "_index.html"
+            quoted = quote(linkname, safe='')
+            link = path / linkname
+            if isinstance(obj, pygit2.Blob):
+                blobmeta = todoblobs.setdefault(obj, [])
+                blobmeta += [(obj.filemode, obj.name)]
+                tree = gen_dir / "blobs" / str(obj.id)
+                linktarget = get_relative(path, tree)
+                link.symlink_to(linktarget, target_is_directory=True)
+                # FIXME html-escape
+                f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
+            elif isinstance(obj, pygit2.Tree):
+                todotrees.add(obj)
+                tree = gen_dir / "trees" / str(obj.id)
+                linktarget = get_relative(path, tree)
+                link.symlink_to(linktarget, target_is_directory=True)
+                # FIXME html-escape
+                f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
+            else:
+                # TODO not implemented, sorry. altho apparently submodules use
+                # commits in trees?
+                raise TypeError
+        f.write("</ul></body></html>")
+
+print("generating blobs")
+
+# build blobs
+while todoblobs:
+    (b, meta) = todoblobs.popitem()
+    path = gen_dir / "blobs" / str(b.id)
+    path.mkdir(parents=True,exist_ok=True)
+    index = path / "index.html"
+    try:
+        f = index.open("x")
+    except FileExistsError:
+        # check if we've already visited this tree
+        continue
+    with f:
+        f.write("<html><head><title>blob</title><body>")
+        f.write("<a href=\"./raw.bin\">view raw</a>")
+        try:
+            text = b.data.decode("utf-8", errors="strict")
+            if len(set(get_lexer_for_filename(f[1]).name for f in meta)) == 1:
+                lex = get_lexer_for_filename(meta[0][1])
+                f.write(highlight(text, lex, HtmlFormatter()))
+            else:
+                # TODO maybe just write `text` (html escaped)?
+                pass
+        except UnicodeError:
+            pass
+        except pygments.util.ClassNotFound:
+            pass
+        f.write("</body></html>")
+    raw = path / "raw.bin"
+    with raw.open("wb") as f:
+        f.write(b)
+
+# create index.html
+path = gen_dir / "index.html"
+with path.open("w") as f:
+    f.write("<html><head><title>index</title><body><ul>")
+    if not repo.head_is_unborn:
+        ref = repo.head
+        quoted = quote(ref.name, safe='/')
+        # FIXME html-escape
+        f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
+    for ref in repo.references:
+        ref = repo.references.get(ref)
+        quoted = quote(ref.name, safe='/')
+        # FIXME html-escape
+        f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
+    f.write("</ul></body></html>")
+
+print("copying to output")
+
+# CANNOT use shutil.copytree - it is broken.
+# also need to be aware of copying into a directory, so we just always make it
+# a directory.
+browse = pathlib.Path.cwd() / "browse"
+browse.mkdir(parents=True,exist_ok=True)
+subprocess.run(["cp", "-R", "-P", *gen_dir.glob("*"), browse], check=True)
+
+# └── gen
+#     ├── blobs
+#     │   └── e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#     │       ├── index.html
+#     │       └── raw.bin
+#     ├── commits
+#     │   ├── 21177a2933b1a9d21d8437159405c5bc68b4d32e
+#     │   │   ├── index.html
+#     │   │   └── tree -> ../../trees/1663be45d5f6b9f092c4b98d44cf7992b427172f
+#     │   └── 3ea9318f6271ece3c7560f18d0b22f50bd3cefe5
+#     │       ├── index.html
+#     │       └── tree -> ../../trees/17d6338b3a3dc189bdc3bea8481fe5f32fd388c8
+#     ├── refs
+#     │   └── heads
+#     │       └── default
+#     │           ├── index.html
+#     │           └── tree -> ../../../trees/1663be45d5f6b9f092c4b98d44cf7992b427172f
+#     └── trees
+#         ├── 1663be45d5f6b9f092c4b98d44cf7992b427172f
+#         │   ├── bar -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#         │   ├── baz -> ../29ba47b07d262ad717095f2d94ec771194c4c083
+#         │   ├── deleteme -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#         │   ├── foo -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#         │   └── index.html
+#         ├── 17d6338b3a3dc189bdc3bea8481fe5f32fd388c8
+#         │   ├── bar -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#         │   ├── baz -> ../29ba47b07d262ad717095f2d94ec771194c4c083
+#         │   ├── foo -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+#         │   └── index.html
+#         └── 29ba47b07d262ad717095f2d94ec771194c4c083
+#             ├── index.html
+#             └── qux -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391