git3html

git clone https://orangeshoelaces.net/git/git3html.git

/git3html

   1 #!/bin/sh
   2 
   3 # Copyright 2019 Vasilii Kolobkov
   4 
   5 # This program is free software: you can redistribute it and/or modify
   6 # it under the terms of the GNU General Public License as published by
   7 # the Free Software Foundation, either version 3 of the License, or
   8 # (at your option) any later version.
   9 
  10 # This program is distributed in the hope that it will be useful,
  11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13 # GNU General Public License for more details.
  14 
  15 # You should have received a copy of the GNU General Public License
  16 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
  17 
  18 set -eu
  19 ht="$(printf '\t')"
  20 
  21 # ........ functions ........
  22 
  23 escape() {
  24         # assume the input is in utf8
  25         # \302\240 is U+00A0 or &nbsp; in utf8
  26         sed "s,&,\&amp;,g; s,$(printf '\302\240'),\&nbsp;,g; \
  27                 s,\",\&quot;,g; s,<,\&lt;,g; s,>,\&gt;,g"
  28 }
  29 
  30 parent() {
  31         if [ "${1%/}" = '.' ]; then
  32                 echo ".."
  33         else
  34                 echo "${1%/}/.."
  35         fi
  36 }
  37 
  38 # snatched from libgit2
  39 isbinary() {
  40         od -A n -N 256 -t u1 | awk '
  41         {
  42                 for(i=1; i <= NF; ++i) {
  43                         c=$i
  44                         if ((c > 31 && c != 127) || c == 8 || c == 27 || c == 12) {
  45                                 # BS, ESC, FF and anything over US excluding DEL is printable
  46                                 ++printable
  47                         } else if (c == 0) {
  48                                 null=1
  49                                 exit
  50                         } else if (c < 9 || c > 13) {
  51                                  # everything else, sans [:space:] is not
  52                                  ++nonprintable
  53                         }
  54                 }
  55         }
  56 
  57         END {
  58                 if (null) print "y"
  59                 else print (printable / 128 < nonprintable) ? "y" : "n"
  60         }'
  61 }
  62 
  63 g() {
  64         git -C "$repodir" "$@"
  65 }
  66 
  67 # tree of the most recent commit
  68 tmrc() {
  69         g rev-list -n 1 --all --format=tformat:%T | tail -n +2
  70 }
  71 
  72 # this and morecommits should use same filter options to maintain the
  73 # invariant: morecommits exits with positive status if pickcommits selects
  74 # proper subset of all commits
  75 pickcommits() {
  76         g rev-list ${clim+-n $clim} --all --date-order "$@"
  77 }
  78 
  79 morecommits() {
  80         [ -n "${clim:-}" ] &&
  81         [ "$(g rev-list --skip="$clim" -n 1 --all | wc -l | cut -d ' ' -f1)" -gt 0 ]
  82 }
  83 
  84 defaultname() {
  85         if [ "$(basename "$repodir")" = '.' ]; then
  86                 basename $(pwd)
  87         else
  88                 basename "$repodir"
  89         fi | sed 's/\.git$//'
  90 }
  91 
  92 header() (
  93         title="$1"
  94         toroot="$2"
  95 
  96         cat <<-END
  97         <!doctype html>
  98         <html>
  99         <head>
 100         END
 101         printf '<title>%s</title>\n' "$(echo "$title" | escape)"
 102         printf '<link rel="stylesheet" type="text/css" href="%s">\n' \
 103                 "$(echo "${toroot%/}/style.css" | escape)"
 104         cat <<-END
 105         </head>
 106         <body>
 107         <header>
 108         END
 109         printf '<h1>%s</h1>\n' "$(echo "$projname" | escape)"
 110         if [ -n "$cloneurl" ]; then
 111                 printf '<pre><code>git clone %s</code></pre>\n' \
 112                         "$(echo "$cloneurl" | escape)"
 113         fi
 114         cat <<-END
 115         <nav>
 116         <ul>
 117         END
 118         printf '<li><a href="%s">Readme</a></li>\n' \
 119                 "$(echo "${toroot%/}/readme.html" | escape)"
 120         printf '<li><a href="%s">Log</a></li>\n' \
 121                 "$(echo "${toroot%/}/log.html" | escape)"
 122         printf '<li><a href="%s">Files</a></li>\n' \
 123                 "$(echo "${toroot%/}/files.html" | escape)"
 124         cat <<-END
 125         </ul>
 126         </nav>
 127         </header>
 128         END
 129 )
 130 
 131 footer() {
 132         cat <<-END
 133         </body>
 134         </html>
 135         END
 136 }
 137 
 138 readme() (
 139         header "${projname} readme" .
 140         root=$(tmrc)
 141         printf '<h2>Readme</h2>\n'
 142         printf '<pre>'
 143         g ls-tree --name-only "$root" | grep '^README*' | while read -r readme; do
 144                 g cat-file --textconv "$root:$readme" | escape
 145         done
 146         printf '</pre>'
 147         footer
 148 )
 149 
 150 commit() (
 151         id="$1"
 152         g rev-list -n 1 --date=format:%x --format='%h%n%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%b' "$id" | \
 153         tail -n +2 | escape | {
 154                 IFS= read -r shortid
 155                 header "${projname} commit ${shortid}" ..
 156                 printf '<h2>%s<span class="id-suffix">%s</h2>\n' "$shortid" \
 157                         "$(echo "$id" | cut -c "$(expr ${#shortid} + 1)-")"
 158                 for v in an ae ad cn ce cd subj; do
 159                         IFS= read -r "$v"
 160                 done
 161                 printf '<p>Author: <a href="mailto:%s">%s</a> on %s</p>\n' "$ae" "$an" "$ad"
 162                 printf '<p>Committer: <a href="mailto:%s">%s</a> on %s</p>\n' "$ce" "$cn" "$cd"
 163                 printf '<p>%s</p>\n' "$subj"
 164                 body="$(cat)"
 165                 if [ "${#body}" -gt 0 ]; then
 166                         printf '<p>'
 167                         bodylnfmt='%s'
 168                         echo "$body" | while IFS= read -r bodyln; do
 169                                 printf "$bodylnfmt" "$bodyln"
 170                                 bodylnfmt='<br>\n%s'
 171                         done
 172                         printf '</p>\n'
 173                 fi
 174         }
 175 
 176         printf '<h3>Stats</h3>\n'
 177         printf '<pre><code>'
 178         g diff-tree --stat --stat-graph-width=8 --root "$id" | tail -n +2 | escape | sed \
 179                 '$d;
 180                 s#\(+*\)\(-*\)$#<span class="add">\1</span><span class="delete">\2</span>#
 181                 s/^[[:space:]]*//;'
 182         g diff-tree --stat --stat-graph-width=8 --root "$id" | tail -n 1 | escape | sed \
 183                 's,+,<span class="add">+</span>,;
 184                 s,-,<span class="delete">-</span>,;
 185                 s/^[[:space:]]*//;'
 186         printf '</code></pre>\n'
 187         printf '<h3>Patch</h3>\n'
 188         printf '<pre><code>'
 189         g diff-tree -p --root "$id" | tail -n +2 | escape | sed "\
 190                 $(for p in diff old new deleted copy rename similarity dissimilarity index; do
 191                         echo "/^$p/ { s,^,<span class=\"diff-header\">,; s,$,</span>,; }"
 192                 done;)
 193                 /^@@/ { s,^,<span class=\"hunk-header\">,; s,$,</span>,; }
 194                 /^+/ { s,^,<span class=\"add\">,; s,$,</span>,; }
 195                 /^-/ { s,^,<span class=\"delete\">,; s,$,</span>,; }"
 196         printf '</code></pre>\n'
 197         footer
 198 )
 199 
 200 log() (
 201         outdir="$1"
 202         cdir="${outdir%/}/commits"
 203         if [ ! -d "$cdir" ]; then
 204                 mkdir "$cdir"
 205         fi
 206         exec >"${outdir%/}/log.html"
 207 
 208         header "${projname} log" .
 209         cat <<-END
 210         <h2>Log</h2>
 211         <table>
 212         <thead>
 213         <tr>
 214         <th>Subject</th>
 215         <th>Date</th>
 216         </tr>
 217         </thead>
 218         <tbody>
 219         END
 220         pickcommits --date=format:%x --format=tformat:%s%n%cd | while read prefix id; do
 221                 commit "$id" >"${cdir%/}/${id}.html"
 222                 IFS= read -r subj
 223                 IFS= read -r date
 224                 printf '<tr>\n<td><a href="commits/%s.html">%s</a></td>\n<td>%s</td>\n</tr>\n' \
 225                         "$id" "$(echo "$subj" | escape)" "$(echo "$date" | escape)"
 226         done
 227         if morecommits; then
 228                 cat <<-END
 229                 <tr>
 230                 <td>...</td>
 231                 </tr>
 232                 END
 233         fi
 234         cat <<-END
 235         </tbody>
 236         </table>
 237         END
 238         footer
 239 )
 240 
 241 dirent() {
 242         printf '<li><span class="sigil">d</span> <a href="%s">%s</a></li>\n' \
 243                 "$(echo "$2" | escape)" "$(echo "$1" | escape)"
 244 }
 245 
 246 blobent() (
 247         mode="$1"
 248         blob="$2"
 249         name="$3"
 250         outdir="$4"
 251         relout="$5"     # outdir relative to index dir
 252         reppath="$6"
 253         relbaseoutdir="$7"
 254 
 255         type="$(expr \( $mode - $mode % 10000 \) / 10000)"
 256         if [ "$type" -eq 10 ]; then     # regular file
 257                 if [ "$(g cat-file -p "$blob" | isbinary)" = y ]; then
 258                         printf '<li><span class="sigil">b</span> %s</li>\n' \
 259                                 "$(echo "$name" | escape)"
 260                 else
 261                         textfile "$blob" "$reppath" "$relbaseoutdir" > "${outdir%/}/${name}.html"
 262                         printf '<li><span class="sigil">t</span> <a href="%s">%s</a></li>\n' \
 263                                 "$(echo "${relout%/}/${name}.html" | escape)" \
 264                                 "$(echo "$name" | escape)"
 265                 fi
 266         elif [ "$type" -eq 12 ]; then   # link
 267                 printf "<li><span class="sigil">l</span> %s</li>\n" "$(echo "$name" | escape)"
 268         else
 269                 printf "<li><span class="sigil">o</span> %s</li>\n" "$(echo "$name" | escape)"
 270         fi
 271 )
 272 
 273 textfile() (
 274         blob="$1"
 275         reppath="$2"
 276         relbaseoutdir="$3"
 277 
 278         header "${projname} ${reppath}" "$relbaseoutdir"
 279         printf '<h2>%s</h2>\n' "$(echo "$reppath" | escape)"
 280         printf '<pre><code>'
 281         n=0
 282         g cat-file blob "$blob" | expand | escape | awk -F '' '{
 283                 ++n
 284                 printf "<a href=\"#l%d\" class=\"linea\">%4d</a> %s\n", n, n, $0
 285         }'
 286         printf '</code></pre>\n'
 287         footer
 288 )
 289 
 290 files() (
 291         tree="$1"
 292         name="$2"
 293         outdir="$3"
 294         relbaseoutdir="$4"      # these two are
 295         relparidx="$5"          # relative to outdir
 296         reppath="$6"
 297 
 298         {
 299                 header "${projname} ${reppath}" "$relbaseoutdir"
 300                 printf '<h2>%s</h2>\n' "$(echo "$reppath" | escape)"
 301                 echo '<ul class="dir">'
 302                 if [ -n "$relparidx" ]; then
 303                         dirent '..' "$relparidx"
 304                 fi
 305                 g ls-tree "$tree" | awk '$2 == "tree"' | while read rec; do
 306                         subtree="$(echo "$rec" | awk '{ print $3 }')"
 307                         stname="$(echo "$rec" | cut -d "$ht" -f2)"
 308                         stoutdir="${outdir%/}/${name}"
 309                         if [ ! -d "$stoutdir" ]; then
 310                                 mkdir "$stoutdir"
 311                         fi
 312 
 313                         files "$subtree" "$stname" "${stoutdir}" "$(parent "${relbaseoutdir}")" \
 314                                 "../${name}.html" "${reppath%/}/${stname}"
 315 
 316                         dirent "${stname}" "${name}/${stname}.html"
 317                 done
 318                 g ls-tree "$tree" | awk '$2 == "blob"' | while read rec; do
 319                         bmode="$(echo "$rec" | awk '{ print $1 }')"
 320                         blob="$(echo "$rec" | awk '{ print $3 }')"
 321                         bname="$(echo "$rec" | cut -d "$ht" -f2)"
 322                         boutdir="${outdir%/}/${name}"
 323                         if [ ! -d "$boutdir" ]; then
 324                                 mkdir "$boutdir"
 325                         fi
 326                         blobent "$bmode" "$blob" "$bname" "$boutdir" "$name" \
 327                                 "${reppath%/}/${bname}" "$(parent "$relbaseoutdir")"
 328                 done
 329                 echo '</ul>'
 330                 footer
 331         } > "${outdir%/}/${name}.html"
 332 )
 333 
 334 # ........ driver ........
 335 
 336 usage() {
 337         echo "Usage: $0 [-p name] [-c url] [-n lim] repodir htmldir" >&2
 338         exit 1
 339 }
 340 
 341 while getopts 'p:c:c:h' opt; do
 342         case "$opt" in
 343         p)
 344                 projname="$OPTARG"
 345                 ;;
 346         c)
 347                 cloneurl="$OPTARG"
 348                 ;;
 349         n)
 350                 clim="$OPTARG"
 351                 ;;
 352         *)
 353                 usage
 354                 ;;
 355         esac
 356 done
 357 
 358 shift $(expr "$OPTIND" - 1)
 359 if [ $# -ne 2 ]; then
 360         usage
 361 fi
 362 
 363 repodir="$1"
 364 htmldir="$2"
 365 projname="${projname:-$(defaultname)}"
 366 
 367 if [ ! -d "$htmldir" ]; then
 368         mkdir -p "$htmldir"
 369 fi
 370 
 371 readme > "${htmldir%/}/readme.html"
 372 log "$htmldir"
 373 files "$(tmrc)" 'files' "$htmldir" '.' '' '/'