Git Repos / blogofile_gitview / _controllers /

Last commit to this repo was on 2022-01-23 at 14:14.

# -*- coding: utf-8 -*-

A Blogofile controller that generates static HTML files from a git repo.
Works like stagit or git-arr (and is in fact a simplified clone of stagit).

This script requires blogofile, pygit2, and pygments.

See init() and run() for the program entry points.

This program is Copyright (c) 2021 Cameron Vanderzanden and is provided
under the terms of the MIT License. Please see the file "LICENSE" in
the root dir of this repository for more information.

import datetime, os, shutil, stat, sys
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
from pygit2 import Repository
from pygit2 import GIT_OBJ_COMMIT, GIT_OBJ_TAG
except ImportError:
print("gitview: pygit2 is required for this controller to work!")
import pygments
except ImportError:
print("gitview: pygments is required for this controller to work!")
from blogofile import util
from blogofile.cache import (
HierarchicalCache as HC
from blogofile import plugin

config = HC(
name = "gitview",
author = "",
description = "A static-page git repo viewer that uses mako templates",
priority = 50,
url = "",
commitlimit = 20,
objdir = "f",
path = "code",
reporoot = None,
enable = False,
template_path = None

repos = []

tools = plugin.PluginTools(sys.modules[__name__])
pygments_formatter = pygments.formatters.HtmlFormatter(linenos=False,
cssclass="syntax_highlight", linespans="ln", lineseparator="<br>",
style="vim", wrapcode=True)
pygments_differ = pygments.formatters.HtmlFormatter(linenos=False,
cssclass="syntax_highlight", lineseparator="<br>", style="vim",

def human_readable_size(size, decimal_places=1):
# TODO: limit output to four characters
for unit in ['B', 'K', 'M', 'G', 'T', 'P']:
if size < 1024.0 or unit == 'P':
size /= 1024.0
return f"{size:.{decimal_places}f} {unit}"

def init():
config.url = + '/' + config.path
if bf.config.gitview.commitlimit:
config.commitlimit = bf.config.gitview.commitlimit
if bf.config.gitview.objdir: config.objdir = bf.config.gitview.objdir
if bf.config.gitview.path: config.path = bf.config.gitview.path
if bf.config.gitview.reporoot: config.reporoot = bf.config.gitview.reporoot
if config.template_path:
#Add the user's custom template path first
tools.add_template_dir(config.template_path, append=False)

def write_log(repo, repoinfo):
parentlimit = 1
cl = []
next_id = None
for i, commit in enumerate(repo.walk(, GIT_SORT_NONE)):
if i > config.commitlimit:
parentids = []
patches = []
deltas = []
dschanged = 0
dsinsert = 0
dsdelete = 0
for ip, parent in enumerate(commit.parents):
if ip > parentlimit: break
diff = repo.diff(parent, commit)
# TODO diffstats are expensive?
dschanged += diff.stats.files_changed
dsinsert += diff.stats.insertions
dsdelete += diff.stats.deletions
for patch in diff:
pd = {
"add": patch.line_stats[1],
"del": patch.line_stats[2],
"newpath": util.html_escape(,
"oldpath": util.html_escape(,
# run patch.text thru pygments
lexer = pygments.lexers.get_lexer_by_name("diff")
except pygments.util.ClassNotFound:
lexer = pygments.lexers.get_lexer_by_name("text")
lexer.tabsize = 8
patchhtml = pygments.highlight(patch.text, lexer,
ci = {
"author": util.html_escape(,
"author_email": util.html_escape(,
"author_time": datetime.datetime.fromtimestamp(,
"committer": util.html_escape(,
"committer_email": util.html_escape(,
"committer_time": datetime.datetime.fromtimestamp(
"date": datetime.datetime.fromtimestamp(commit.commit_time),
"deltas": deltas,
"ds_changed": dschanged,
"ds_insert": dsinsert,
"ds_delete": dsdelete,
"id": str(,
"msg": util.html_escape(commit.message.strip()),
"parentids": parentids,
"patches": patches,
"summary": util.html_escape(commit.message.split('\n')[0])
prev_id = None
for pid in ci['parentids']:
prev_id = pid
# is this the initial commit?
if prev_id == str( prev_id = None
# are we at the end of commitlimit?
if i == config.commitlimit: prev_id = None
# write the single-commit view
util.path_join(config.path, repoinfo['name'],
str( + ".html"), {"commitinfo": ci, "repo": repoinfo,
"next_id": next_id, "prev_id": prev_id} )
next_id = str(
# write the commit log list
# NOTE: commented out in favor of git_repo.mako
# tools.materialize_template('git_log.mako',
# util.path_join(config.path, repoinfo['name'], "log.html"),
# {"commits": cl, "repo": repoinfo })
return cl

def format_obj_binary(repoinfo, obj, path):
# if this is a gif, jpeg, or png then display it inline
extl = os.path.splitext(
if len(extl) > 0 and extl[1] != "":
# we're testing file extension here and that's not ideal
ext = extl[1].lstrip('.')
if ext in "png jpeg jpg gif".split():
# this looks a mess. It writes to
# its appropriate place in the site output tree.
with open(util.path_join(bf.writer.output_dir, config.path,
repoinfo['name'], config.objdir, path,, "wb") as imgf:
return True, "<img src=\"/{}\" />".format(util.path_join(
config.path, repoinfo['name'], config.objdir, path,
else: return False, None
else: return False, None

def format_obj_text(repoinfo, obj, path):
# run thru pygments
data = str(, sys.stdout.encoding)
# TODO: for some reason guess_lexer_for_filename doesn't detect
# my mako templates. So let's test file extension...
lexer = pygments.lexers.get_lexer_by_name("mako")
lexer = pygments.lexers.guess_lexer_for_filename(".fix"), data)
lexer = pygments.lexers.guess_lexer_for_filename(, data)
except pygments.util.ClassNotFound:
lexer = pygments.lexers.get_lexer_by_name("text")
lexer.tabsize = 8
return pygments.highlight(data, lexer, pygments_formatter)

def write_obj(fl, repo, repoinfo, tree, path):
for obj in tree:
if obj.type_str == "tree":
# NOTE: this recurses. (Thank you stagit.c line 1020.)
write_obj(fl, repo, repoinfo, repo.get(,
if not os.path.isdir(util.path_join(config.path,
repoinfo['name'], config.objdir, path)):
config.path, repoinfo['name'], config.objdir, path))
isimage = False
objsize = human_readable_size(obj.size)
if obj.is_binary:
isimage, data = format_obj_binary(repoinfo, obj, path)
else: data = format_obj_text(repoinfo, obj, path)
if'.'): safename = 'dot{}'.format([1:])
else: safename =
f = {
"fullname": util.html_escape(util.path_join(path,,
"isbinary": obj.is_binary,
"isimage": isimage,
"mode": stat.filemode(obj.filemode),
"name": util.html_escape(,
"objsize": objsize,
"sfname": util.html_escape(util.path_join(path, safename)),
"type": obj.type_str
tools.materialize_template("git_file.mako", util.path_join(
config.path, repoinfo['name'], config.objdir, path, safename +
{"file": f, "data": data, "repo": repoinfo} )

def write_files(repo, repoinfo):
fl = []
for commit in repo.walk(, GIT_SORT_NONE):
write_obj(fl, repo, repoinfo, commit.tree, "")
return fl

def get_branches(repo, repoinfo):
bl = []
for branch_s in repo.branches:
branch_b = repo.lookup_branch(branch_s)
last_commit = None
author = None
commitid = ""
summary = None
commit_link = False
for commit in repo.walk(
author = util.html_escape(
commitid = str(
last_commit = datetime.datetime.fromtimestamp(commit.commit_time)
summary = util.html_escape(commit.message.split('\n')[0])
if os.path.exists(util.path_join(bf.writer.output_dir,
config.path, repoinfo['name'], str(commitid) + ".html")):
commit_link = True
b = {
"author": author,
"commit_link": commit_link,
"id": commitid,
"is_head": branch_b.is_head(),
"last_commit": last_commit,
"name": util.html_escape(branch_b.branch_name),
"summary": summary,
return bl

def get_tags(repo, repoinfo):
tl = []
for ref in repo.references:
if not ref.startswith("refs/tags/"): continue
tag = repo.revparse_single(ref)
author = None
commitid = ""
last_commit = None
summary = None
commit_link = False
if tag.type == GIT_OBJ_COMMIT:
author = util.html_escape(
commitid = str(
last_commit = datetime.datetime.fromtimestamp(tag.commit_time)
summary = util.html_escape(tag.message.split('\n')[0])
elif tag.type == GIT_OBJ_TAG:
target_commit = tag.get_object()
author = util.html_escape(
commitid = str(
last_commit = datetime.datetime.fromtimestamp(
summary = util.html_escape(target_commit.message.split('\n')[0])
if os.path.exists(util.path_join(bf.writer.output_dir,
config.path, repoinfo['name'], str(commitid) + ".html")):
commit_link = True
t = {
"author": author,
"commit_link": commit_link,
"id": commitid,
"last_commit": last_commit,
"name": util.html_escape(ref[10:]),
"summary": summary
return tl

def run():
if not config.reporoot:
print("gitview: repo root not configured!")
for d in os.listdir(config.reporoot):
fullpath = util.path_join(config.reporoot, d)
if os.path.isdir(fullpath):
ri = []
for r in repos:
# 1. collect info for index
rd_path = r.path.rstrip(os.sep)
nl = rd_path.split(os.sep)
rd_name = os.path.splitext(nl[len(nl) - 1])[0]
rd_desc = ""
rd_owner = ""
rd_last_commit = None
commit_list = []
file_list = []
branch_list = []
tag_list = []
with open(util.path_join(rd_path, "description"), "r") as f:
rd_desc = f.readline().strip()
except FileNotFoundError:
print("file " + util.path_join(rd_path, "description") + \
" not found!")
rd_desc = "Empty description"
with open(util.path_join(rd_path, "owner"), "r") as f:
rd_owner = f.readline().strip()
except FileNotFoundError:
print("file " + util.path_join(rd_path, "owner") + " not found!")
rd_owner = "Undefined"
if not os.path.isdir(util.path_join(config.path, rd_name)):
config.path, rd_name))
for commit in r.walk(, GIT_SORT_NONE):
dt = datetime.datetime.fromtimestamp(commit.commit_time)
if not rd_last_commit: rd_last_commit = dt
if rd_last_commit < dt: rd_last_commit = dt
# high-level repo description.
rd = {
"path": rd_path,
"name": rd_name,
"desc": util.html_escape(rd_desc),
"owner": util.html_escape(rd_owner),
"last_commit": rd_last_commit
# 2. write log and commits; store the commit log dict list
commit_list = write_log(r, rd)
# 3. collect branches and tags
branch_list = get_branches(r, rd)
tag_list = get_tags(r, rd)
# 4. write the individual files in the tree
file_list = write_files(r, rd)
# NOTE: commented out in favor of git_repo.mako
# tools.materialize_template("git_files.mako",
# util.path_join(config.path, name, "files.html"),
# {"commits": cl, "files": fl, "repo": rd })
# 5. write the repo index
util.path_join(config.path, rd['name'], "index.html"),
{"commits": commit_list, "files": file_list,
"branches": branch_list, "tags": tag_list, "repo": rd})
# NOTE: commented out in favor of git_repo.mako above
# shutil.copyfile(
# util.path_join(bf.writer.output_dir,
# config.path, name, "files.html"),
# util.path_join(bf.writer.output_dir,
# config.path, name, "index.html"))
# 6. write the overall repository list index
util.path_join(config.path, "index.html"), {"repos": ri} )

Return to top of this page or return to the overview of this repository