gemini.git

going-flying.com gemini git repository

summary

tree

log

refs

bae7ad41487b76320ba63f92ad170fdc536b58f0 - Matthew Ernisse - 1595973172

remove git-gmi, it wants the bare repo and I do not want to mount that in the container... sorry.

view tree

view raw

diff --git a/cgi-bin/git b/cgi-bin/git
deleted file mode 100755
index 8fc81cb..0000000
--- a/cgi-bin/git
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env python3
-
-# allow importing from within the place.
-import os
-import sys
-sys.path.append(os.path.join(
-	os.path.dirname(os.path.realpath(__file__)),
-	'git-gmi'
-))
-import gateway
diff --git a/cgi-bin/git-gmi/config.py b/cgi-bin/git-gmi/config.py
deleted file mode 100644
index 13a42ab..0000000
--- a/cgi-bin/git-gmi/config.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# where on the disk are the repos located
-GIT_CATALOG = "/var/gemini"
-# which path leads to your cgi app after the URL's host part
-CGI_PATH = "/cgi-bin/"
-# cache dir
-CACHE_DIR = "/tmp"
-# how long before cache expires, in seconds: int
-CACHE_TTL = 120
-# your site's display name
-GIT_GMI_SITE_TITLE = "going-flying.com"
-# the "main" branch that git.gmi defaults to
-MAIN_BRANCH = "master"
diff --git a/cgi-bin/git-gmi/const.py b/cgi-bin/git-gmi/const.py
deleted file mode 100644
index f1f757f..0000000
--- a/cgi-bin/git-gmi/const.py
+++ /dev/null
@@ -1,6 +0,0 @@
-STATUS_SUCCESS = "20"
-STATUS_NOT_FOUND = "51 NOT FOUND"
-STATUS_TEMPORARY_FAILURE = "40 TEMPORARY FAILURE"
-META_GEMINI = "text/gemini"
-META_PLAINTEXT = "text/plain"
-MAX_DISPLAYED_BLOB_SIZE = 500 * 1024  # 500KB
diff --git a/cgi-bin/git-gmi/gateway.py b/cgi-bin/git-gmi/gateway.py
deleted file mode 100644
index b0c9b0a..0000000
--- a/cgi-bin/git-gmi/gateway.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from git import *
-from const import *
-from config import *
-from os import environ, listdir
-import sys
-
-# be careful when using print(); stdout is passed to the client.
-# this cgi uses \n as newline.
-
-
-def handle_cgi_request(path: str, query: str):
-    # intended to work with Jetforce.
-    # hypothetical example:
-    # url: gemini://git.gemini.site/git/cgi/repo/src/static/css/[index.css]
-    # path: /repo/src/static/css/[index.css]
-    # path_trace = ['repo', 'src', 'static', 'css', 'index.css']
-    path_trace = path[1:].split("/")
-    if path_trace == [""]:  # empty path
-        print(f"{STATUS_SUCCESS} {META_GEMINI}")  # welcome page
-        print(f"# Welcome to {GIT_GMI_SITE_TITLE}")
-        print("## Available repositories:")
-        print("\n".join([f"=> {dir}/" for dir in listdir(GIT_CATALOG)]))
-        return
-
-    try:
-        repo = GitGmiRepo(path_trace[0], f"{GIT_CATALOG}/{path_trace[0]}")
-    except FileNotFoundError:
-        print(STATUS_NOT_FOUND)
-        return
-
-    if len(path_trace) > 1:
-        view = path_trace[1]  # e.g. summary, tree, log
-    else:
-        # gemini://git.gemini.site/git/cgi/<repo>/
-        print("31 summary")
-        return
-
-    if view == "summary":
-        try:
-            print(repo.view_summary())
-        except:
-            print(STATUS_TEMPORARY_FAILURE)
-
-    elif view == "tree":
-        if len(path_trace) == 2:
-            # gemini://git.gemini.site/git/cgi/<repo>/tree/
-            print(f"31 {MAIN_BRANCH}/")
-
-        elif len(path_trace) > 2:
-            # gemini://git.gemini.site/git/cgi/<repo>/tree/<branch>/
-            branch = path_trace[2]
-
-        location = path_trace[3:]
-
-        try:  # is dir
-            print(repo.view_tree(branch, location))
-        except FileNotFoundError:  # is file
-            try:
-                if query == "raw":
-                    sys.stdout.buffer.write(repo.view_raw_blob(branch, location))
-                else:
-                    print(repo.view_blob(branch, location))
-            except FileNotFoundError:
-                print(STATUS_NOT_FOUND)
-
-    elif view == "log":
-        try:
-            print(repo.view_log())
-        except:
-            print(STATUS_TEMPORARY_FAILURE)
-
-    elif view == "commit":
-        try:
-            commit_str = path_trace[2]
-        except IndexError:
-            print("50 No commit id given")
-            return
-
-        try:
-            if query == "raw":
-                print(repo.view_raw_commit(commit_str))
-            else:
-                print(repo.view_commit(commit_str))
-        except FileNotFoundError:
-            print("50 No such commit")
-        except:
-            print(STATUS_TEMPORARY_FAILURE)
-
-    elif view == "refs":
-        try:
-            print(repo.view_refs())
-        except:
-            print(STATUS_TEMPORARY_FAILURE)
-
-
-handle_cgi_request(environ.get("PATH_INFO"), environ.get("QUERY_STRING"))
diff --git a/cgi-bin/git-gmi/git.py b/cgi-bin/git-gmi/git.py
deleted file mode 100644
index b758440..0000000
--- a/cgi-bin/git-gmi/git.py
+++ /dev/null
@@ -1,371 +0,0 @@
-from pygit2 import *
-from hurry.filesize import size, alternative
-from datetime import datetime, timedelta
-import dateutil.parser
-from pathlib import Path
-import os
-import shutil
-import mimetypes
-from const import *
-from config import *
-from utils import *
-
-mimetypes.add_type("text/gemini", ".gmi")
-mimetypes.add_type("text/gemini", ".gemini")
-
-
-def convert_filesize(bytes: int) -> str:
-    # convert filesize in bytes to a human-friendly format
-    return size(bytes, system=alternative)
-
-
-class GitGmiRepo:
-    def __init__(self, name: str, path: str):
-        self.name = name
-        self.path = path
-        self.cache_dir = Path(CACHE_DIR) / name
-        self._init_cache()
-        try:
-            self.repo = Repository(path)
-        except GitError:
-            raise FileNotFoundError(f"Error: no such repo: {name}")
-
-    def _init_cache(self):
-        try:
-            os.mkdir(self.cache_dir)
-        except FileExistsError:
-            pass
-
-    def _read_cache(self, req: list) -> str:
-        # req is what the user requests after the repo name,
-        # like ["tree", "master", "src"]
-        # which points to a file called tree_master_src.gmi
-        # file content:
-        # 20 text/gemini
-        # [body - page content]
-        # [newline]
-        # cached at:
-        # [iso timestamp]
-        fn = "_".join(req) + ".gmi"
-        try:
-            with open(self.cache_dir / fn) as f:
-                response = f.read()
-                f.close()
-                created_at = dateutil.parser.isoparse(response.splitlines()[-1])
-                if datetime.now() - created_at < timedelta(seconds=CACHE_TTL):
-                    # cache valid
-                    # response will include the timestamp
-                    return response
-        except FileNotFoundError:
-            pass
-
-        return None
-
-    def _write_cache(self, req: list, resp: str):
-        # write resp into cache, appended with timestamp
-        fn = "_".join(req) + ".gmi"
-        try:
-            f = open(self.cache_dir / fn, "x")
-        except FileExistsError:
-            f = open(self.cache_dir / fn, "w")
-        f.write(resp + "\ncached at:\n" + datetime.now().isoformat())
-
-    def _flush_cache(self):
-        try:
-            shutil.rmtree(self.cache_dir)
-        except FileNotFoundError:
-            pass
-
-    def _generate_header(self):
-        # global "header" to display above all views (except raw files)
-        header = (
-            f"# {self.name}\n"
-            f"=> {CGI_PATH} {GIT_GMI_SITE_TITLE}\n"
-            f"=> {CGI_PATH}{self.name}/summary summary\n"
-            f"=> {CGI_PATH}{self.name}/tree/{MAIN_BRANCH}/ tree\n"
-            f"=> {CGI_PATH}{self.name}/log log\n"
-            f"=> {CGI_PATH}{self.name}/refs refs\n\n"
-        )
-        return header
-
-    def view_summary(self) -> str:
-        cached = self._read_cache(["summary"])
-        if cached is not None:
-            return cached
-
-        response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
-        # show 3 recent commits
-        recent_commits = self._get_commit_log()[:3]
-        for cmt in recent_commits:
-            time = str(datetime.utcfromtimestamp(cmt["time"])) + " UTC"
-            response += (
-                f"### {cmt['short_id']} - {cmt['author']} - {time}\n"
-                f"{cmt['msg'].splitlines()[0]}\n\n"
-            )  # TODO: link to commit view
-        # find and display readme(.*)
-        tree = self._get_tree(MAIN_BRANCH)
-        trls = self._list_tree(tree)
-        found_readme = False
-        for item in trls:
-            if (
-                item["type"] == "file"
-                and item["name"].lower().split(".")[0] == ("readme")
-                and not found_readme
-            ):
-                found_readme = True
-                response += (
-                    f"## {item['name']} | {convert_filesize(item['size'])}\n"
-                    f"{item['blob'].data.decode('utf-8')}"
-                )
-        if not found_readme:
-            response += "## No readme found."
-
-        self._write_cache(["summary"], response)
-
-        return response
-
-    def _get_commit_log(self) -> list:
-        # returns useful info from commit log.
-        repo = self.repo
-        commits = list(repo.walk(repo[repo.head.target].id, GIT_SORT_TIME))
-        log = [
-            {
-                "id": str(cmt.id),  # hex SHA-1 hash
-                "short_id": str(cmt.short_id),  # short version of the above
-                "author": cmt.author.name,  # author's display name
-                "time": cmt.commit_time,  # unix timestamp
-                "msg": cmt.message,  # full commit message
-            }
-            for cmt in commits
-        ]
-
-        return log  # reverse chronical order
-
-    def view_log(self) -> str:
-        cached = self._read_cache(["log"])
-        if cached is not None:
-            return cached
-        response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
-        log = self._get_commit_log()
-        for cmt in log:
-            # looks like "2020-06-06 04:51:21 UTC"
-            time = str(datetime.utcfromtimestamp(cmt["time"])) + " UTC"
-            response += (
-                f"## {cmt['short_id']} - {cmt['author']} - {time}\n"
-                f"=> commit/{cmt['id']} view diff\n"
-                f"=> tree/{cmt['id']}/ view tree\n"
-                f"{cmt['msg']}\n\n"
-            )
-        self._write_cache(["log"], response)
-        return response
-
-    def _get_commit(self, commit_str) -> dict:
-        try:
-            commit = self.repo.revparse_single(commit_str)
-            diff = self.repo.diff(commit.parents[0], commit)
-            return {
-                "id": commit.id,
-                "author": commit.author.name,
-                "time": commit.commit_time,
-                "msg": commit.message,
-                "patch": diff.patch,
-            }
-        except ValueError:
-            raise FileNotFoundError(f"Error: no such commit: {commit_str}")
-
-    def view_commit(self, commit_str) -> str:
-        cached = self._read_cache(["commit", commit_str])
-        if cached is not None:
-            return cached
-        commit = self._get_commit(commit_str)
-        response = (
-            f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
-            + self._generate_header()
-            + f"{commit['id']} - {commit['author']} - {commit['time']}\n"
-            + commit["msg"]
-            + "\n"
-            + f"=> {CGI_PATH}{self.name}/tree/{commit['id']}/ view tree\n"
-            + f"=> {commit_str}?raw view raw\n"
-            + "\n```\n"
-            + commit["patch"]
-            + "\n```"
-        )
-        self._write_cache(["commit", commit_str], response)
-        return response
-
-    def view_raw_commit(self, commit_str) -> str:
-        commit = self.get_commit(commit_str)
-        response = f"{STATUS_SUCCESS} {META_PLAINTEXT}\r\n" + commit["patch"]
-        return response
-
-    def _get_refs(self) -> list:
-        refs = self.repo.listall_reference_objects()
-        return [
-            {
-                "name": ref.name,
-                "shorthand": ref.shorthand,
-                "target": ref.target,
-                "type": ref.type,
-            }
-            for ref in refs
-        ]
-
-    def view_refs(self) -> str:
-        cached = self._read_cache(["refs"])
-        if cached is not None:
-            return cached
-        response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
-        refs = self._get_refs()
-        for ref in refs:
-            # HACK: filter out refs with slashes as remote branches
-            if ref["shorthand"].find("/") == -1:
-                response += (
-                    f"## {ref['shorthand']}\n=> tree/{ref['shorthand']}/ view tree\n\n"
-                )
-        self._write_cache(["refs"], response)
-        return response
-
-    @classmethod
-    def _parse_recursive_tree(cls, tree: Tree) -> list:
-        # recursively replace all Trees with a list of Blobs inside it,
-        # bundled with the Tree's name as a tuple,
-        # e.g. [('src', [blob0, blob1]), otherblob].
-        tree_list = list(tree)
-        for idx, item in enumerate(tree_list):
-            if isinstance(item, Tree):
-                tree_list[idx] = (item.name, cls._parse_recursive_tree(tree_list[idx]))
-
-        return tree_list
-
-    def _get_tree(self, revision_str: str) -> list:
-        # returns a recursive list of Blob objects
-        try:
-            revision = self.repo.revparse_single(revision_str)
-            if isinstance(revision, Commit):
-                # top level tree; may contain sub-trees
-                return self._parse_recursive_tree(revision.tree)
-            elif isinstance(revision, Tag):
-                return self._parse_recursive_tree(revision.get_object().tree)
-        except ValueError:
-            raise FileNotFoundError(f"Error: no such tree: {revision_str}")
-            return None
-
-    @staticmethod
-    def _list_tree(tree_list: list, location=[]) -> list:
-        # tree_list is the output of _parse_recursive_tree(<tree>);
-        # location is which dir you are viewing, represented path-like
-        # in a list, e.g. ['src', 'static', 'css'] => 'src/static/css',
-        # which this method will cd into and display to the visitor.
-        # when there is no such dir, raises FileNotFoundError.
-        trls = tree_list
-        for loc in location:
-            found = False
-            for item in trls:
-                if isinstance(item, tuple) and item[0] == loc:
-                    trls = item[1]
-                    found = True
-                    break
-            if not found:
-                raise FileNotFoundError(
-                    f"Error: no such directory: {'/'.join(location)}"
-                )
-
-        contents = []
-        for item in trls:
-            if isinstance(item, tuple):
-                # was originally a Tree; structure: ('dir_name', [list_of_blobs])
-                contents.append(
-                    {
-                        "type": "dir",
-                        "name": item[0],
-                        "items": len(item[1]),  # number of objects in dir
-                    }
-                )
-
-            elif isinstance(item, Blob):
-                contents.append(
-                    {
-                        "type": "file",
-                        "name": item.name,
-                        "blob": item,
-                        "size": item.size,  # size in bytes
-                    }
-                )
-
-        return contents
-
-    def view_tree(self, branch: str, location=[]) -> str:
-        # actual Gemini response
-        # consists of a header and a body
-        cached = self._read_cache(["tree", branch] + location)
-        if cached is not None:
-            return cached
-
-        tree = self._get_tree(branch)
-        contents = self._list_tree(tree, location)
-        items = len(contents)
-        response = (
-            f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
-            + self._generate_header()
-            + f"## {self.name}{'/' if location else ''}{'/'.join(location)}/"
-            f" | {items} {'items' if items > 1 else 'item'}\n\n"
-        )
-        for item in contents:
-            if item["type"] == "dir":
-                response += (
-                    f"=> {item['name']}/ {item['name']}/ | {item['items']} items\n"
-                )
-            elif item["type"] == "file":
-                response += f"=> {item['name']} {item['name']} | {convert_filesize(item['size'])}\n"
-        self._write_cache(["tree", branch] + location, response)
-        return response
-
-    def _get_blob(self, commit_str: str, location=[]) -> Blob:
-        # returns a specific Blob object
-        # location: just like that of _list_tree, but the last element
-        # is the filename
-        try:
-            tree = self._get_tree(commit_str)
-            trls = self._list_tree(tree, location[:-1])
-            for item in trls:
-                if item["type"] == "file" and item["name"] == location[-1]:
-                    return item["blob"]
-            raise FileNotFoundError(f"Error: no such file: {'/'.join(location)}")
-        except FileNotFoundError:
-            raise FileNotFoundError(f"Error: No such tree: {'/'.join(location[:-1])}")
-
-    def view_blob(self, branch: str, location=[]) -> str:
-        cached = self._read_cache(["tree", branch] + location)
-        if cached is not None:
-            return cached
-        blob = self._get_blob(branch, location)
-        response = (
-            f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
-            + self._generate_header()
-            + f"## {self.name}/{'/'.join(location)} | {convert_filesize(blob.size)}\n\n"
-        )
-
-        if blob.is_binary:
-            response += (
-                "This file seems to be binary. Open link below to download.\n"
-                f"=> {blob.name}?raw download"
-            )
-        elif blob.size < MAX_DISPLAYED_BLOB_SIZE:
-            response += (
-                f"=> {blob.name}?raw view raw\n\n"
-                "```\n" + add_line_numbers(blob.data.decode("utf-8")) + "\n```"
-            )
-        else:
-            response += (
-                "This file is too large to be displayed. Open link below to download.\n"
-                f"=> {blob.name}?raw download\n\n"
-            )
-        return response
-
-    def view_raw_blob(self, branch: str, location=[]) -> bytes:
-        blob = self._get_blob(branch, location)
-        # if mimetypes can't make out the type, set it to plaintext
-        guessed_mimetype = mimetypes.guess_type(blob.name)[0] or META_PLAINTEXT
-        response = bytes(f"{STATUS_SUCCESS} {guessed_mimetype}\r\n", encoding="utf-8")
-        response += blob.data
-        return response
diff --git a/cgi-bin/git-gmi/index.gmi b/cgi-bin/git-gmi/index.gmi
deleted file mode 100644
index 49a4cbe..0000000
--- a/cgi-bin/git-gmi/index.gmi
+++ /dev/null
@@ -1,18 +0,0 @@
-# This is a git.gmi instance - a frontend for Git on Gemini
-
-```git.gmi
-         _   _                             _
-        (_) | |                           (_)
-  __ _   _  | |_        __ _   _ __ ___    _
- / _` | | | | __|      / _` | | '_ ` _ \  | |
-| (_| | | | | |_   _  | (_| | | | | | | | | |
- \__, | |_|  \__| (_)  \__, | |_| |_| |_| |_|
-  __/ |                 __/ |
- |___/                 |___/
-```
-
-=> /cgi/ Repo index
-
-=> gemini://git.fkfd.me/cgi/git.gmi/ Source code
-=> https://git.sr.ht/~fkfd/git.gmi/ Source code (HTTPS)
-
diff --git a/cgi-bin/git-gmi/utils.py b/cgi-bin/git-gmi/utils.py
deleted file mode 100644
index c3dbd59..0000000
--- a/cgi-bin/git-gmi/utils.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import math
-
-
-def add_line_numbers(code: str) -> str:
-    lines = code.splitlines()
-    if not lines:
-        return code  # empty anyway
-
-    # cannot use math.ceil() here bc lg100=2
-    max_digits = math.floor(math.log10(len(lines))) + 1
-
-    for n, l in enumerate(lines, 1):
-        digits_in_n = math.floor(math.log10(n)) + 1
-        spaces_before_number = max_digits - digits_in_n
-        lines[n - 1] = " " * spaces_before_number + str(n) + " " + l
-
-    return "\n".join(lines)