For a few years, I’ve generated my website — including a browsable Git repository site — using a large build script written in POSIX shell. After moving the main site generation to Quartz, I still needed a way to generate a Git repo site.
I figured — why not keep using POSIX shell scripting for that too? I also wanted the generation process to be automated and effortless, so I decided to use a post-receive
hook in Git.
What I needed
- A site generator for building a repo site for each Git repository
- An index generator to link all the individual repo sites together
- A generalized
post-receive
hook to trigger site generation on push - An upload script to publish everything externally
Here’s how it all fits together
Every time a push is made to a repository, the post-receive
hook triggers the site generator for that specific repo and regenerates the central index. This ensures that the latest commits and changes are “immediately” reflected on the site. Then, at 02:00 every night, a scheduled upload script syncs the entire collection of repo sites — along with the updated index — to the external host.
The scripts
These are the scripts. Might need some polish — a quick hack!
View git2site
#!/bin/sh
set -e
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <repo.git> <output-path>"
exit 1
fi
AUTHOR="Chris Noxz"
REPO_URL="https://git.noxz.tech"
FAVIOCON="/favicon.png"
# helper function to html encode stdin
# shellcheck disable=SC2120
html_encode() {
if [ ! -t 0 ]; then
cat -
elif [ -n "$1" ]; then
echo "$1"
fi | sed \
-e 's/&/\&/g' \
-e 's/"/\"/g' \
-e 's/</\</g' \
-e 's/>/\>/g'
}
# helper function to fix dot files if asked to do so
# shellcheck disable=SC2120
fix_dot_file() {
if [ ! -t 0 ]; then
cat -
elif [ -n "${1}" ]; then
echo "${1}"
fi | sed \
-e "s,/\.,/_\.,g" \
-e "s,^\.,_\.,g"
}
REPO_PATH="${1%/}"
OUTPUT_PATH="${2%/}"
REPO_FILENAME="${REPO_PATH##*/}"
REPO_NAME="${REPO_FILENAME%.git}"
ARCHIVE_OUTPUT_PATH="${OUTPUT_PATH}/archive"
FILES_OUTPUT_PATH="${OUTPUT_PATH}/file"
COMMITS_OUTPUT_PATH="${OUTPUT_PATH}/commit"
LOG_PATH="${OUTPUT_PATH}/log.html"
FILES_PATH="${OUTPUT_PATH}/files.html"
TAGS_PATH="${OUTPUT_PATH}/tags.html"
if [ -z "${REPO_PATH}" ]; then
echo "Error: <repo.git> is empty" >&2
exit 1
fi
if [ -z "${OUTPUT_PATH}" ]; then
echo "Error: <output-path> is empty" >&2
exit 1
fi
if [ ! -d "${REPO_PATH}" ]; then
echo "Error: directory '${REPO_PATH}' does not exist" >&2
exit 1
fi
if [ ! -d "${OUTPUT_PATH}" ]; then
echo "Error: directory '${OUTPUT_PATH}' does not exist" >&2
exit 1
fi
if ! git -C "${REPO_PATH}" rev-parse --git-dir >/dev/null 2>&1; then
echo "Directory '${REPO_PATH}' is not a Git repository"
exit 1
fi
if [ "$(ls -A "${OUTPUT_PATH}")" ]; then
echo "Directory '${OUTPUT_PATH}' is not empty"
exit 1
fi
REPO_URL="${REPO_URL}/${REPO_FILENAME}"
REPO_DESCRIPTION="$([ -f "${REPO_PATH}/description" ] && html_encode < "${REPO_PATH}/description" || printf '')"
REPO_AUTHOR="$([ -f "${REPO_PATH}/owner" ] && html_encode < "${REPO_PATH}/owner" || printf '')"
REPO_ISFORK="$([ -f "${REPO_PATH}/fork" ] && printf '1' || printf '0')"
REPO_ISDISCONTINUED="$([ -f "${REPO_PATH}/discontinued" ] && printf '1' || printf '0')"
REPO_LAST_COMMIT="$(git --git-dir="${REPO_PATH}" log -1 --format='%ai' 2>/dev/null || echo "N/A")"
git clone --bare "${REPO_PATH}" "${OUTPUT_PATH}/repo"
git -C "${OUTPUT_PATH}/repo" --bare update-server-info
ln -s "${OUTPUT_PATH}/reporefs" "${OUTPUT_PATH}/repo/info/refs?service=git-upload-pack"
mv "${OUTPUT_PATH}/repo/"* "${OUTPUT_PATH}"
rmdir "${OUTPUT_PATH}/repo"
printf '%s\n%s\n%s\n%s\n%s\n%s\n%s' \
"${REPO_NAME}" \
"${REPO_DESCRIPTION}" \
"${REPO_AUTHOR}" \
"${REPO_URL}" \
"${REPO_ISFORK}" \
"${REPO_ISDISCONTINUED}" \
"${REPO_LAST_COMMIT}" > "${OUTPUT_PATH}/.repo_metadata"
mkdir -p "${ARCHIVE_OUTPUT_PATH}"
mkdir -p "${FILES_OUTPUT_PATH}"
mkdir -p "${COMMITS_OUTPUT_PATH}"
REPO_MENU="$(
printf '<a href="../log.html">Log</a>'
printf ' | <a href="../files.html">Files</a>'
if git -C "${REPO_PATH}" describe --tags >/dev/null 2>&1; then
printf ' | <a href="../tags.html">Tags</a>'
fi
if git -C "${REPO_PATH}" show HEAD:README >/dev/null 2>&1; then
printf ' | <a href="../file/README.html">README</a>'
fi
if git -C "${REPO_PATH}" show HEAD:LICENSE >/dev/null 2>&1; then
printf ' | <a href="../file/LICENSE.html">LICENSE</a>'
fi
)"
render_head() {
cat <<EOF
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="author" content="${AUTHOR}">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="0"/>
<title>Repositories: ${REPO_NAME}</title>
<link rel="icon" type="image/png" href="${FAVIOCON}"/>
<style>
/* Base styles */
body {
color: #000;
background-color: #fff;
font-family: monospace;
}
h1, h2, h3, h4, h5, h6 {
font-size: 1em;
margin: 0;
}
img {
border: 0;
vertical-align: middle;
}
h1, h2 {
vertical-align: middle;
}
a {
color: #005386;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Tables */
table td {
padding: 0 0.4em;
white-space: nowrap;
vertical-align: top;
}
table tr.nohi td {
font-weight: bold;
}
table#files td:nth-child(3) {
text-align: right;
}
table#files td:nth-child(3):after {
content: " bytes";
}
table#files tr.nohi td:nth-child(3):after {
content: "";
}
/* Highlight on hover */
#index tr:hover td,
#files tr:hover td,
#log tr:hover td,
#tags tr:hover td {
background-color: #f3f1f9;
}
thead tr:hover td,
tr.nohi:hover td {
background-color: transparent !important;
}
/* Expand second column for specific tables */
#index,
#files,
#log,
#tags,
#index td:nth-child(2),
#files td:nth-child(2),
#log td:nth-child(2),
#tags td:nth-child(2) {
width: 100%;
white-space: normal;
}
/* Misc */
.desc {
color: #777;
}
hr {
border: 0;
border-top: 1px solid #777;
height: 1px;
}
pre {
font-family: monospace;
tab-size: 4;
}
/* Code lines */
pre a.line {
display: inline-block;
width: 32px;
text-align: right;
color: #777;
padding-right: 5px;
margin-right: 5px;
border-right: 3px solid #eee;
user-select: none;
}
pre a:target {
background: #efefef;
}
/* Highlight types */
pre a.h,
pre a.i,
pre a.d,
table span.i,
table span.d {
text-decoration: none;
}
pre a.h {
color: #00c;
}
pre a.h:target {
background: #efefff;
}
pre a.i,
table span.i {
color: #080;
}
pre a.i:target {
background: #efffef;
}
pre a.d,
table span.d {
color: #d00;
}
pre a.d:target {
background: #ffefef;
}
pre b.r {
color: #808;
padding-left: 1em;
}
</style>
</head>
<body>
<table>
<tr>
<td rowspan="3" valign="top"><a href="../.."><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="32px" height="32px" viewBox="0 0 40 40" version="1.1" id="svg5995" inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="logo.svg">
<defs id="defs5989"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="2" inkscape:cx="169.22484" inkscape:cy="86.16882" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="2560" inkscape:window-height="1024" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1"/>
<metadata id="metadata5992">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-257)">
<g id="g4971" transform="matrix(1.4417433,0,0,1.4417433,-185.22241,-98.665593)">
<g id="g1447-3" transform="matrix(4.5481071,0,0,4.5481071,128.69855,246.91874)">
<path id="path1435-6" style="fill:none;stroke:#000000;stroke-width:0.1;stroke-opacity:1" d="M 0,0 H 6 V 6 H 0 Z M 0,2 H 6 M 0,4 H 6 M 2,0 V 6 M 4,0 v 6" inkscape:connector-curvature="0"/>
</g>
<rect transform="rotate(-45)" y="275.88495" x="-79.740433" height="5.1449485" width="5.1449485" id="rect4878" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="288.75497" x="-79.733536" height="5.1449485" width="5.1449485" id="rect4878-2" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="295.18683" x="-86.165421" height="5.1449485" width="5.1449485" id="rect4878-2-9" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="288.75497" x="-92.597275" height="5.1449485" width="5.1449485" id="rect4878-2-9-2" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="282.31039" x="-99.041801" height="5.1449485" width="5.1449485" id="rect4878-2-9-2-7" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
</g>
</g></svg></a></td>
<td><h1>${REPO_NAME}</h1><span class="desc">${REPO_DESCRIPTION}</span></td>
</tr>
<tr>
<td>git clone <a href="${REPO_URL}">${REPO_URL}</a></td>
</tr>
<tr>
<td>${REPO_MENU}</td>
</tr>
</table>
<hr />
EOF
}
render_tail() {
cat <<EOF
</body>
</html>
EOF
}
render_file() {
printf '\t<span class="git-file">%s</span>\n\t<hr />\n' "${1}"
if git -C "${REPO_PATH}" diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 \
--numstat HEAD -- "${1}" | grep '^-' >/dev/null 2>&1; then
printf '\t%s\n' "binary file."
else
git -C "${REPO_PATH}" cat-file "HEAD:${1}" -p \
| html_encode \
| awk '
BEGIN {
print "<pre>"
}
{
print "<a class=\"line\" href=\"#l" NR "\" id=\"l" NR "\">" NR "</a>" $0
}
END {
print "</pre>"
}
'
fi
}
render_commit() {
COMMIT_FILES="$(git -C "${REPO_PATH}" whatchanged "${1}"^! | grep '^:.*$' | cut -d' ' -f5 | sed 's/\t/:/g')"
git -C "${REPO_PATH}" show --stat=1000 --stat-graph-width=20 "${1}" \
--format="format:%H%n%P%n%aN%n%aE%n%aD%n%B%H" \
| sed 's/{\(.*\) => \(.*\)}/\1/g' \
| html_encode | awk -v flist="${COMMIT_FILES}" '
BEGIN {
marker=""; files=0; split(flist, farr, "\n");
for (i in farr) {
split(farr[i], ftmp, ":");
farr[i,1] = substr(ftmp[1], 1, 1);
farr[i,2] = ftmp[2];
farr[i,3] = ftmp[3];
}
}
{
switch (NR) {
case 1:
marker=$0
printf "<pre>\n"
printf "<b>commit</b>: <a href=\"../commit/%s.html\">%s</a>\n",$0,$0
break
case 2:
printf "<b>parent</b>: <a href=\"../commit/%s.html\">%s</a>\n",$0,$0
break
case 3:
printf "<b>author</b>: %s",$0
break
case 4:
if ($0 != "") { printf " <<a href=\"mailto:%s\">%s</a>>",$0,$0 }
printf "\n"
break
case 5:
printf "<b>date</b>: %s\n",$0
printf "</pre>\n"
break
case 6:
printf "<pre class=\"body\">\n"
default:
if (files) {
for (i in farr) {
if ($1 == farr[i,2]) {
printf "<tr>"
printf "<td>%s</td><td><a href=\"#f%d\">%s</a>",farr[i,1],i,farr[i,2]
if (farr[i,3] != "") {
printf " → <a href=\"#f%d\">%s</a>",i,farr[i,3]
}
printf "</td>",$3
printf "<td>%s</td>",$3
printf "<td><span class=\"i\">"
p=1
for (j = 1; j <= length($4); j++) {
if (p && substr($4, j, 1) != "+") {
p=0; printf "</span><span class=\"d\">"
}
printf "%s",substr($4,j,1)
}
printf "</span></td>"
print "</tr>"
next
}
}
sub(/^[ \t\r\n]+/, "", $0)
printf "</table>\n%s\n",$0
} else {
if (marker != "" && marker == $0) { printf "</pre>\n<table>\n"; files=1 }
else { print }
}
break
}
}'
echo "<hr />"
git -C "${REPO_PATH}" show "${1}" --format="" | html_encode | awk -v flist="${COMMIT_FILES}" '
BEGIN {
indiff=0; files=0; h=0; l=0 split(flist, farr, "\n");
for (i in farr) {
split(farr[i], ftmp, ":");
farr[i,1] = ftmp[1];
farr[i,2] = ftmp[2];
}
print "<pre>"
}
/^diff --git/ {
indiff=1
if (match($0, /^diff --git a\/(.*) b\/(.*)$/, m)) {
for (i in farr) {
if (m[1] == farr[i,2]) {
printf "<b>diff --git"
printf " a/<a id=\"f%d\" href=\"#f%d\">%s</a>",i,i,m[1]
printf " b/<a href=\"#f%d\">%s</a>",i,m[2]
printf "</b>\n"
if (m[1] != m[2]) {
printf "<b class=\"r\">rename %s → ",m[1]
printf "%s</b>\n",m[2]
}
next
}
}
}
next
}
/^@@/ {
indiff=0
printf "<a href=\"#h%d\" id=\"h%d\" class=\"h\">%s</a>\n",h,h++,$0
next
}
/^\+.*$/ {
if (indiff) { next }
printf "<a href=\"#l%d\" id=\"l%d\" class=\"i\">%s</a>\n",l,l++,$0
next
}
/^\-.*$/ {
if (indiff) { next }
printf "<a href=\"#l%d\" id=\"l%d\" class=\"d\">%s</a>\n",l,l++,$0
next
}
{
if (indiff) { next }
print
}
END {
print "</pre>"
}'
}
# Render tag archives
git -C "${REPO_PATH}" tag \
| xargs -I{} git -C "${REPO_PATH}" archive --format=tar.gz --prefix="${REPO_NAME}-{}/" -o "${ARCHIVE_OUTPUT_PATH}/${REPO_NAME}-{}.tar.gz" "{}"
# Render preview files
git -C "${REPO_PATH}" ls-tree -r --format="%(objectmode)%x0a%(path)" HEAD \
| grep '^100[0-9][0-9][0-9]$' -A1 | grep -v '^--$\|^100[0-9][0-9][0-9]$' | while read -r file; do
FILE_OUTPUT_PATH="$(echo "${FILES_OUTPUT_PATH}/${file}" | fix_dot_file).html"
echo "[+] Generating file page: ${FILE_OUTPUT_PATH}"
mkdir -p "${FILE_OUTPUT_PATH%/*}"
FILE_DEPTH=$(printf "%s\n" "${file}" | awk -F/ '{ print NF }')
PARENTS=$(printf '../%.0s' $(seq 1 $FILE_DEPTH))
render_head | sed "s,href=\"../,href=\"${PARENTS},g" > "${FILE_OUTPUT_PATH}"
render_file ${file} >> "${FILE_OUTPUT_PATH}"
render_tail >> "${FILE_OUTPUT_PATH}"
done
# Render commits
git -C "${REPO_PATH}" log --pretty="format:%H" | grep . | while read -r commit; do
COMMIT_OUTPUT_PATH="${COMMITS_OUTPUT_PATH}/${commit}.html"
echo "[+] Generating commit page: ${COMMIT_OUTPUT_PATH}"
render_head > "${COMMIT_OUTPUT_PATH}"
render_commit ${commit} >> "${COMMIT_OUTPUT_PATH}"
render_tail >> "${COMMIT_OUTPUT_PATH}"
done
# Render log file
echo "[+] Generating log file"
render_head | sed 's,href="../,href="./,g' > "${LOG_PATH}"
git -C "${REPO_PATH}" log --pretty="format:%ai%n%s%n%an%n%H" \
| html_encode \
| awk '
BEGIN {
printf "\t<table id=\"log\">\n"
printf "\t\t<tr class=\"nohi\">"
printf "<td>Date</td>"
printf "<td>Commit message</td>"
printf "<td>Author</td>"
printf "<td>Commit</td>"
printf "</tr>\n"
printf "\t\t<tbody>\n"
}
{
entry[(NR-1) % 4] = $0
if (NR % 4 == 0) {
printf "\t\t\t<tr>"
for (i = 0; i < 4; i++) {
if (i == 1) {
printf "<td><a href=\"commit/%s.html\">%s</a></td>",entry[3],entry[i]
} else if (i == 3) {
printf "<td><a href=\"commit/%s.html\">%*.*s</a></td>",entry[i],8,8,entry[i]
} else {
printf "<td>%s</td>",entry[i]
}
}
printf "</tr>\n"
}
}
END {
printf "\t\t</tbody>\n"
printf "\t</table>\n"
}
' >> "${LOG_PATH}"
render_tail >> "${LOG_PATH}"
echo "[+] Generating files file"
# render files
render_head | sed 's,href="../,href="./,g' > "${FILES_PATH}"
git -C "${REPO_PATH}" ls-tree -r --format="%(objectmode)%x0a%(path)%x0a%(objectsize)" HEAD \
| html_encode \
| fix_dot_file \
| awk '
function dotrep(data)
{
output = data
gsub(/\/_\./, "/.", output)
gsub(/^_\./, ".", output)
return output
}
function oct2sym(data)
{
type = substr(data, 0, 3)
if (type == "100") {
type = "-"
} else if (type == "060") {
type = "b"
} else if (type == "020") {
type = "c"
} else if (type == "040") {
type = "d"
} else if (type == "010") {
type = "p"
} else if (type == "120") {
type = "l"
} else if (type == "140") {
type = "s"
} else {
type = "?"
}
sym = substr(data, 4, 3)
gsub("0", "---", sym)
gsub("1", "--x", sym)
gsub("2", "-w-", sym)
gsub("3", "-wx", sym)
gsub("4", "r--", sym)
gsub("5", "r-x", sym)
gsub("6", "rw-", sym)
gsub("7", "rwx", sym)
return type sym
}
BEGIN {
printf "\t<table id=\"files\">\n"
printf "\t\t<tr class=\"nohi\">"
printf "<td>Mode</td>"
printf "<td>Name</td>"
printf "<td>Size</td>"
printf "</tr>\n"
printf "\t\t<tbody>\n"
}
{
entry[(NR-1) % 3] = $0
if (NR % 3 == 0) {
printf "\t\t\t<tr>"
for (i = 0; i < 3; i++) {
if (i == 1 && substr(entry[0], 0, 3) == "100") {
printf "<td><a href=\"file/%s.html\">%s</a></td>", entry[i], dotrep(entry[i])
} else {
printf "<td>%s</td>", i == 0 ? oct2sym(entry[i]) : entry[i]
}
}
printf "</tr>\n"
}
}
END {
printf "\t\t</tbody>\n"
printf "\t</table>\n"
}
' >> "${FILES_PATH}"
render_tail >> "${FILES_PATH}"
echo "[+] Generating tags file"
# render files
render_head | sed 's,href="../,href="./,g' > "${TAGS_PATH}"
# render tags
git -C "${REPO_PATH}" log --no-walk --tags --pretty="format:%S%n%ai%n%an%n%h" \
| html_encode \
| awk '
BEGIN {
printf "\t<table id=\"tags\">\n"
printf "\t\t<tr class=\"nohi\">"
printf "<td>Name</td>"
printf "<td>Date</td>"
printf "<td>Author</td>"
printf "<td>Commit</td>"
printf "</tr>\n"
printf "\t\t<tbody>\n"
}
{
entry[(NR-1) % 4] = $0
if (NR % 4 == 0) {
printf "\t\t\t<tr>"
for (i = 0; i < 4; i++) {
if (i == 0) {
printf "<td><a href=\"./archive/__REPONAME__-%s.tar.gz\">%s</a></td>",entry[i],entry[i]
} else {
printf "<td>%s</td>",entry[i]
}
}
printf "</tr>\n"
}
}
END {
printf "\t\t</tbody>\n"
printf "\t</table>\n"
}
' | sed "s/__REPONAME__/${REPO_NAME}/g" >> "${TAGS_PATH}"
render_tail >> "${TAGS_PATH}"
printf '<meta http-equiv="refresh" content="0; url=%s" />' "$(git -C "${REPO_PATH}" show HEAD:README >/dev/null 2>&1 && echo "${FILES_OUTPUT_PATH##*/}/README.html" || echo "${LOG_PATH##*/}")" > "${OUTPUT_PATH}/index.html"
View gengitindex
#!/bin/sh
AUTHOR="Chris Noxz"
HOME_URL="https://noxz.tech"
FAVIOCON="/favicon.png"
SITE_ROOT="/strg/src/stage/git-site"
INDEX_HTML="${SITE_ROOT}/index.html"
TMP_NORMAL=$(mktemp)
TMP_FORKS=$(mktemp)
TMP_DISCONTINUED=$(mktemp)
# Loop over all .repo_metadata files
find "$SITE_ROOT" -maxdepth 2 -name '.repo_metadata' | while IFS= read -r metafile; do
REPO_DIR=$(dirname "$metafile")
REPO_NAME=$(sed -n '1p' "$metafile")
REPO_DESC=$(sed -n '2p' "$metafile")
REPO_AUTHOR=$(sed -n '3p' "$metafile")
REPO_URL=$(sed -n '4p' "$metafile")
REPO_ISFORK=$(sed -n '5p' "$metafile")
REPO_ISDISCONTINUED=$(sed -n '6p' "$metafile")
REPO_LAST_COMMIT=$(sed -n '7p' "$metafile")
GITDIR="${SITE_ROOT}/${REPO_NAME}.git"
TR_LINE="<tr><td><a href=\"${REPO_URL##*/}\">${REPO_NAME}</a></td><td>${REPO_DESC}</td><td>${REPO_AUTHOR}</td><td>${REPO_LAST_COMMIT}</td></tr>"
# Categorize into buckets
if [ "$REPO_ISDISCONTINUED" = "1" ]; then
echo "$TR_LINE" >> "$TMP_DISCONTINUED"
elif [ "$REPO_ISFORK" = "1" ]; then
echo "$TR_LINE" >> "$TMP_FORKS"
else
echo "$TR_LINE" >> "$TMP_NORMAL"
fi
done
# Sort each group alphabetically by link text (between <a> and </a>)
sort_html_by_name() {
sed 's/<tr><td><a href="[^"]*">//;s,</a>.*,,' "$1" | paste - "$1" | sort | cut -f2-
}
sort_html_by_name "$TMP_NORMAL" > "$TMP_NORMAL.sorted"
sort_html_by_name "$TMP_FORKS" > "$TMP_FORKS.sorted"
sort_html_by_name "$TMP_DISCONTINUED" > "$TMP_DISCONTINUED.sorted"
# Generate final HTML
{
cat <<EOF
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="author" content="${AUTHOR}">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="0"/>
<title>Repositories</title>
<link rel="icon" type="image/png" href="${FAVIOCON}"/>
<style>
/* Base styles */
body {
color: #000;
background-color: #fff;
font-family: monospace;
}
h1, h2, h3, h4, h5, h6 {
font-size: 1em;
margin: 0;
}
img {
border: 0;
vertical-align: middle;
}
h1, h2 {
vertical-align: middle;
}
a {
color: #005386;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Tables */
table td {
padding: 0 0.4em;
white-space: nowrap;
vertical-align: top;
}
table tr.nohi td {
font-weight: bold;
}
/* Highlight on hover */
#index tr:hover td,
#files tr:hover td,
#log tr:hover td,
#tags tr:hover td {
background-color: #f3f1f9;
}
thead tr:hover td,
tr.nohi:hover td {
background-color: transparent !important;
}
/* Expand second column for specific tables */
#index,
#files,
#log,
#tags,
#index td:nth-child(2),
#files td:nth-child(2),
#log td:nth-child(2),
#tags td:nth-child(2) {
width: 100%;
white-space: normal;
}
/* Misc */
.desc {
color: #777;
}
hr {
border: 0;
border-top: 1px solid #777;
height: 1px;
}
pre {
font-family: monospace;
tab-size: 4;
}
/* Code lines */
pre a.line {
display: inline-block;
width: 32px;
text-align: right;
color: #777;
padding-right: 5px;
margin-right: 5px;
border-right: 3px solid #eee;
user-select: none;
}
pre a:target {
background: #efefef;
}
/* Highlight types */
pre a.h,
pre a.i,
pre a.d,
table span.i,
table span.d {
text-decoration: none;
}
pre a.h {
color: #00c;
}
pre a.h:target {
background: #efefff;
}
pre a.i,
table span.i {
color: #080;
}
pre a.i:target {
background: #efffef;
}
pre a.d,
table span.d {
color: #d00;
}
pre a.d:target {
background: #ffefef;
}
pre b.r {
color: #808;
padding-left: 1em;
}
</style>
</head>
<body>
<table>
<tr>
<td rowspan="3" valign="top"><a href="../.."><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="32px" height="32px" viewBox="0 0 40 40" version="1.1" id="svg5995" inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="logo.svg">
<defs id="defs5989"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="2" inkscape:cx="169.22484" inkscape:cy="86.16882" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="2560" inkscape:window-height="1024" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1"/>
<metadata id="metadata5992">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-257)">
<g id="g4971" transform="matrix(1.4417433,0,0,1.4417433,-185.22241,-98.665593)">
<g id="g1447-3" transform="matrix(4.5481071,0,0,4.5481071,128.69855,246.91874)">
<path id="path1435-6" style="fill:none;stroke:#000000;stroke-width:0.1;stroke-opacity:1" d="M 0,0 H 6 V 6 H 0 Z M 0,2 H 6 M 0,4 H 6 M 2,0 V 6 M 4,0 v 6" inkscape:connector-curvature="0"/>
</g>
<rect transform="rotate(-45)" y="275.88495" x="-79.740433" height="5.1449485" width="5.1449485" id="rect4878" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="288.75497" x="-79.733536" height="5.1449485" width="5.1449485" id="rect4878-2" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="295.18683" x="-86.165421" height="5.1449485" width="5.1449485" id="rect4878-2-9" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="288.75497" x="-92.597275" height="5.1449485" width="5.1449485" id="rect4878-2-9-2" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
<rect transform="rotate(-45)" y="282.31039" x="-99.041801" height="5.1449485" width="5.1449485" id="rect4878-2-9-2-7" style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.20321889;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"/>
</g>
</g></svg></a></td>
<td><span class="desc">Repositories</span></td>
</tr>
<tr>
<td><a href="${HOME_URL}">${HOME_URL##*://}</a></td>
</tr>
</table>
<hr />
<div id="content">
<table id="index">
EOF
# Helper to emit each group
emit_group() {
FILE="$1"
if [ -s "$FILE" ]; then
echo ' <tr class="nohi"><td>Name</td><td>Description</td><td>Owner</td><td>Last commit</td></tr>'
echo ' <tbody'
cat "$FILE"
echo ' </tbody>'
echo ' <tr class="nohi"><td colspan="4"> </td></tr>'
fi
}
emit_group "$TMP_NORMAL.sorted"
emit_group "$TMP_FORKS.sorted"
emit_group "$TMP_DISCONTINUED.sorted"
cat <<EOF
</table>
</div>
</body>
</html>
EOF
} > "$INDEX_HTML"
# Clean up
rm -f "$TMP_NORMAL" "$TMP_FORKS" "$TMP_DISCONTINUED"
rm -f "$TMP_NORMAL.sorted" "$TMP_FORKS.sorted" "$TMP_DISCONTINUED.sorted"
View post-receive
#!/bin/bash
export PATH=/usr/bin:/bin:/usr/local/bin
set -euo pipefail
# Get path of the current (bare) Git repo — this is where the hook is running
GIT_REPO_PATH=$(dirname "$(dirname "$0")")
REPO_NAME="$(basename "$GIT_REPO_PATH" .git)"
# Define where the static site should be generated
GIT_SITE_PATH="/strg/src/stage/git-site"
REPO_SITE_PATH="${GIT_SITE_PATH}/${REPO_NAME}.git"
echo "[POST RECEIVE HOOK] Generating site for repository: $REPO_NAME"
echo "Repo path: $GIT_REPO_PATH"
echo "Target site path: $REPO_SITE_PATH"
# Ensure target directory exists
mkdir -p "${REPO_SITE_PATH}"
# Sanity check before removing contents
if [[ -z "$REPO_SITE_PATH" || "$REPO_SITE_PATH" == "/" || ! -d "$REPO_SITE_PATH" ]]; then
echo "Error: Invalid or dangerous REPO_SITE_PATH: '$REPO_SITE_PATH'" >&2
exit 1
fi
# Clean old site content safely
rm -rf "${REPO_SITE_PATH}/"*
rm -f "${REPO_SITE_PATH}/.repo_metadata"
# Generate site from Git repo
git2site "$GIT_REPO_PATH" "$REPO_SITE_PATH"
gengitindex
echo "[POST RECEIVE HOOK] Site generation completed."
As css style is included in each .html
file there is no need for a separate style.css
file. Except for these scripts, favicon.png
and favicon.ico
are needed to be placed inside the git-site
directory.
Updating the style is as simple as editing the script and trigger site generation for every repo:
find /strg/src/z0noxz/ -type f -path '*/hooks/post-receive' | xargs -I{} sh -c 'echo "0000000000000000000000000000000000000000 eabf1234567890abcdef1234567890abcdef1234 refs/heads/main" | {}'