djcev.com

//

Git Repos / blogofile_gitview / _controllers / gitview.py

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

# -*- coding: utf-8 -*-
"""gitview.py

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
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
try:
from pygit2 import Repository
from pygit2 import GIT_SORT_TOPOLOGICAL, GIT_SORT_REVERSE, GIT_SORT_NONE
from pygit2 import GIT_OBJ_COMMIT, GIT_OBJ_TAG
except ImportError:
print("gitview: pygit2 is required for this controller to work!")
sys.exit(1)
try:
import pygments
except ImportError:
print("gitview: pygments is required for this controller to work!")
sys.exit(1)
from blogofile import util
from blogofile.cache import (
bf,
HierarchicalCache as HC
)
from blogofile import plugin

config = HC(
name = "gitview",
author = "cvanderz@gmail.com",
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",
wrapcode=True)

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':
break
size /= 1024.0
return f"{size:.{decimal_places}f} {unit}"


def init():
config.url = bf.config.site.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)
tools.add_template_dir(util.path_join('_templates', config.name))


def write_log(repo, repoinfo):
parentlimit = 1
cl = []
next_id = None
for i, commit in enumerate(repo.walk(repo.head.target, GIT_SORT_NONE)):
if i > config.commitlimit:
cl.append(None)
break
parentids = []
patches = []
deltas = []
dschanged = 0
dsinsert = 0
dsdelete = 0
for ip, parent in enumerate(commit.parents):
if ip > parentlimit: break
parentids.append(str(parent.id))
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(patch.delta.new_file.path),
"oldpath": util.html_escape(patch.delta.old_file.path),
"status": patch.delta.status_char()
}
deltas.append(pd)
# run patch.text thru pygments
try:
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,
pygments_differ)
patches.append(patchhtml)
ci = {
"author": util.html_escape(commit.author.name),
"author_email": util.html_escape(commit.author.email),
"author_time": datetime.datetime.fromtimestamp(commit.author.time),
"committer": util.html_escape(commit.committer.name),
"committer_email": util.html_escape(commit.committer.email),
"committer_time": datetime.datetime.fromtimestamp(
commit.committer.time),
"date": datetime.datetime.fromtimestamp(commit.commit_time),
"deltas": deltas,
"ds_changed": dschanged,
"ds_insert": dsinsert,
"ds_delete": dsdelete,
"id": str(commit.id),
"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
break
# is this the initial commit?
if prev_id == str(commit.id): prev_id = None
# are we at the end of commitlimit?
if i == config.commitlimit: prev_id = None
# write the single-commit view
tools.materialize_template("git_commit.mako",
util.path_join(config.path, repoinfo['name'],
str(commit.id) + ".html"), {"commitinfo": ci, "repo": repoinfo,
"next_id": next_id, "prev_id": prev_id} )
next_id = str(commit.id)
cl.append(ci)
# 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(obj.name)
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 obj.data 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, obj.name), "wb") as imgf:
imgf.write(obj.data)
return True, "<img src=\"/{}\" />".format(util.path_join(
config.path, repoinfo['name'], config.objdir, path, obj.name))
else: return False, None
else: return False, None


def format_obj_text(repoinfo, obj, path):
# run obj.data thru pygments
data = str(obj.data, sys.stdout.encoding)
try:
# TODO: for some reason guess_lexer_for_filename doesn't detect
# my mako templates. So let's test file extension...
if obj.name.endswith(".mako"):
lexer = pygments.lexers.get_lexer_by_name("mako")
elif obj.name.endswith(".fix"):
lexer = pygments.lexers.guess_lexer_for_filename(
obj.name.rstrip(".fix"), data)
else:
lexer = pygments.lexers.guess_lexer_for_filename(obj.name, 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(obj.id),
util.path_join(path, obj.name))
continue
if not os.path.isdir(util.path_join(config.path,
repoinfo['name'], config.objdir, path)):
util.mkdir(util.path_join(bf.writer.output_dir,
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 obj.name.startswith('.'): safename = 'dot{}'.format(obj.name[1:])
else: safename = obj.name
f = {
"fullname": util.html_escape(util.path_join(path, obj.name)),
"id": obj.id,
"isbinary": obj.is_binary,
"isimage": isimage,
"mode": stat.filemode(obj.filemode),
"name": util.html_escape(obj.name),
"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 +
".html"),
{"file": f, "data": data, "repo": repoinfo} )
fl.append(f)


def write_files(repo, repoinfo):
fl = []
for commit in repo.walk(repo.head.target, GIT_SORT_NONE):
write_obj(fl, repo, repoinfo, commit.tree, "")
break
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(branch_b.target):
author = util.html_escape(commit.author.name)
commitid = str(commit.id)
last_commit = datetime.datetime.fromtimestamp(commit.commit_time)
summary = util.html_escape(commit.message.split('\n')[0])
break
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,
"target": branch_b.target
}
bl.append(b)
return bl


def get_tags(repo, repoinfo):
tl = []
# https://www.pygit2.org/recipes/git-tag.html
# https://github.com/libgit2/pygit2/issues/856
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(tag.author.name)
commitid = str(tag.id)
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(tag.tagger.name)
commitid = str(target_commit.id)
last_commit = datetime.datetime.fromtimestamp(
target_commit.commit_time)
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
}
tl.append(t)
return tl


def run():
if not config.reporoot:
print("gitview: repo root not configured!")
sys.exit(1)
for d in os.listdir(config.reporoot):
fullpath = util.path_join(config.reporoot, d)
if os.path.isdir(fullpath):
repos.append(Repository(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 = []
try:
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"
try:
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)):
util.mkdir(util.path_join(bf.writer.output_dir,
config.path, rd_name))
for commit in r.walk(r.head.target, 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
}
ri.append(rd)
# 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
tools.materialize_template("git_repo.mako",
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
tools.materialize_template("git_repos.mako",
util.path_join(config.path, "index.html"), {"repos": ri} )

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