summary refs log tree commit diff stats
path: root/win32/ext/zlib-wdk
diff options
context:
space:
mode:
Diffstat (limited to 'win32/ext/zlib-wdk')
0 files changed, 0 insertions, 0 deletions
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
#!/usr/bin/env python

# HTMLGDump - dumps a git repo to html (and symlinks)
# Copyright (c) 2021 Soni L.
#
# Permission is hereby granted, free of charge, to any person ("You") obtaining
# a copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# This license shall be void if You bring a copyright lawsuit, related or
# unrelated to the Software, against any of the copyright holders.
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# tl;dr: install this as a git hook (post-receive)
# then configure your webserver and stuff

import dataclasses
import os
import os.path
import pathlib
import shutil
import subprocess
import sys
from urllib.parse import quote

import pygit2

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import get_lexer_for_filename
from pygments.lexers import guess_lexer
from pygments.lexers import guess_lexer_for_filename
import pygments.util

@dataclasses.dataclass
class GitChange:
    old_value: str
    new_value: str
    ref_name: str
    deleting: bool = dataclasses.field(init=False)

    def __post_init__(self):
        self.deleting = self.new_value == "0"*40 or self.new_value == "0"*64

def get_relative(path, target):
    """Makes target relative to path, without filesystem operations."""
    return os.path.relpath(target, start=path)

def find_lexer(text, meta):
    """Attempts to find a lexer for the given text/meta."""
    # TODO this can probably be improved
    # try exact lexers based on filename
    # this is by far the fastest, but may lead to incorrect results sometimes.
    try:
        if len(set(get_lexer_for_filename(f[1]).name for f in meta)) == 1:
            lex = get_lexer_for_filename(meta[0][1])
            return lex
    except pygments.util.ClassNotFound:
        pass
    # try lexers based on filename and content
    try:
        if len(set(guess_lexer_for_filename(f[1], text).name for f in meta)) == 1:
            lex = guess_lexer_for_filename(meta[0][1], text)
            return lex
    except pygments.util.ClassNotFound:
        pass
    # try lexers based only on content
    try:
        lex = guess_lexer(text)
        return lex
    except pygments.util.ClassNotFound:
        pass
    return None

def check_soupault_version(soupault):
    """Checks if the given soupault command provides the correct version."""
    # e.g. soupault 3.1.1
    # versions up to 3.1.0 have a major security flaw which makes them
    # unsuitable for use with this program
    version = subprocess.run([
        soupault,
        "--version"
    ], stdout=subprocess.PIPE, check=True).stdout.splitlines()[0].decode()
    # support for soupault 4.x.y
    if version.startswith("soupault 4."):
        return
    if not version.startswith("soupault 3."):
        print("please use soupault 3.1.1 or newer")
        exit()
    if version.startswith("soupault 3.0."):
        print("please use soupault 3.1.1 or newer")
        exit()
    # semver doesn't allow leading 0 on any numeric fields, so this is safe
    if version.startswith("soupault 3.1.0"):
        print("please use soupault 3.1.1 or newer")
        exit()

def find_soupault_config(dirs):
    for d in dirs:
        path = pathlib.Path(d) / "soupault.toml"
        try:
            f = path.open()
            print("using {} as soupault config".format(path))
            return f
        except OSError as e:
            pass
    print("couldn't find soupault config. tried paths:")
    for d in dirs:
        path = pathlib.Path(d) / "soupault.toml"
        print(path)
    exit()

CACHE_HOME = os.environ.get('XDG_CACHE_HOME', '')
if not CACHE_HOME:
    CACHE_HOME = os.environ['HOME'] + '/.cache'
CACHE_HOME = CACHE_HOME + "/htmlgdump"

CONFIG_HOME = os.environ.get('XDG_CONFIG_HOME', '')
if not CONFIG_HOME:
    CONFIG_HOME = os.environ['HOME'] + '/.config'
CONFIG_HOME = CONFIG_HOME + "/htmlgdump"

CONFIG_DIRS = os.environ.get('XDG_CONFIG_DIRS', '')
if not CONFIG_DIRS:
    CONFIG_DIRS = '/etc/xdg'
# TODO check if this is correct
CONFIG_DIRS = [config_dir + "/htmlgdump" for config_dir in CONFIG_DIRS.split(':')]

soupault_config = find_soupault_config([CONFIG_HOME] + CONFIG_DIRS)

# post-receive runs on $GIT_DIR
repo = pygit2.Repository(os.getcwd())

try:
    name = pathlib.Path.cwd().relative_to(repo.config["htmlgdump.base"])
except (KeyError, ValueError):
    print("please set htmlgdump.base")
    exit()

soupault = "soupault"
try:
    soupault = repo.config["htmlgdump.soupault"]
except (KeyError, ValueError):
    pass

check_soupault_version(soupault)

changes = [GitChange(*l.rstrip("\n").split(" ", 2)) for l in sys.stdin]

gen_dir = pathlib.Path(CACHE_HOME) / name / "gen"
gen_dir.mkdir(parents=True,exist_ok=True)

build_dir = pathlib.Path(CACHE_HOME) / name / "build"
build_dir.mkdir(parents=True,exist_ok=True)

todocommits = set()

print("updating refs")

# build changed refs
for c in changes:
    path = gen_dir / c.ref_name
    linkpath = build_dir / c.ref_name
    if c.deleting:
        try:
            shutil.rmtree(path)
            shutil.rmtree(linkpath)
        except FileNotFoundError:
            pass
    else:
        path.mkdir(parents=True,exist_ok=True)
        linkpath.mkdir(parents=True,exist_ok=True)
        index = path / "index.html"
        link = linkpath / "tree"
        tree = gen_dir / "trees" / str(repo[c.new_value].tree_id)
        with index.open("w") as f:
            # TODO
            f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>ref</title><body><a href=\"./tree\">view tree</a> <a href=\"{}../commits/{}\">view commit</a></body></html>".format("".join("../" for x in c.ref_name if x == "/"), c.new_value))
        todocommits.add(repo[c.new_value])
        linktarget = get_relative(path, tree)
        link.unlink(missing_ok=True)
        link.symlink_to(linktarget, target_is_directory=True)

print("generating refs")

# create missing refs
for ref in repo.references:
    ref = repo.references.get(ref)
    path = gen_dir / ref.name
    linkpath = build_dir / ref.name
    path.mkdir(parents=True,exist_ok=True)
    linkpath.mkdir(parents=True,exist_ok=True)
    index = path / "index.html"
    link = linkpath / "tree"
    tree = gen_dir / "trees" / str(ref.peel(pygit2.Commit).tree_id)
    try:
        f = index.open("x")
    except FileExistsError:
        # check if we've already visited this commit
        continue
    with f:
        # TODO
        f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>ref</title><body><a href=\"./tree\">view tree</a> <a href=\"{}../commits/{}\">view commit</a></body></html>".format("".join("../" for x in ref.name if x == "/"), ref.peel(pygit2.Commit).id))
    todocommits.add(ref.peel(pygit2.Commit))
    linktarget = get_relative(path, tree)
    link.symlink_to(linktarget, target_is_directory=True)

todotrees = set()

print("generating commits")

# build commits
while todocommits:
    c = todocommits.pop()
    path = gen_dir / "commits" / str(c.id)
    linkpath = build_dir / "commits" / str(c.id)
    path.mkdir(parents=True,exist_ok=True)
    linkpath.mkdir(parents=True,exist_ok=True)
    index = path / "index.html"
    link = linkpath / "tree"
    tree = gen_dir / "trees" / str(c.tree_id)
    try:
        f = index.open("x")
    except FileExistsError:
        # check if we've already visited this commit
        continue
    with f:
        # TODO
        f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>commit</title><body><a href=\"./tree\">view tree</a><ul>")
        for parent in c.parents:
            f.write("<li><a href=\"../{}\">{}</a></li>".format(str(parent.id), str(parent.id)))
        f.write("</ul></body></html>")
    todotrees.add(c.tree)
    todocommits.update(c.parents)
    linktarget = get_relative(path, tree)
    link.symlink_to(linktarget, target_is_directory=True)

# a dict /!\
# maps blobs to some metadata
# FIXME this can get quite expensive with larger repos, and might even run out
# of RAM.
todoblobs = {}

print("generating trees")

# build trees
while todotrees:
    t = todotrees.pop()
    path = gen_dir / "trees" / str(t.id)
    linkpath = build_dir / "trees" / str(t.id)
    path.mkdir(parents=True,exist_ok=True)
    linkpath.mkdir(parents=True,exist_ok=True)
    index = path / "index.html"
    try:
        f = index.open("x")
    except FileExistsError:
        # check if we've already visited this tree
        continue
    with f:
        f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>tree</title><body><ul>")
        for obj in t:
            linkname = obj.name
            # a git repo can contain any file, including index.html among
            # others, but you can never make a file conflict with the id of
            # the tree it's in. (or at least, it's impractical to do so.)
            # hashes are kinda awesome!
            # so we just mangle those to not conflict with our own index.html
            # note that this does mean the index.html files cannot be easily
            # permalinked, sorry.
            if linkname == "index.html":
                linkname = str(t.id) + "_index.html"
            quoted = quote(linkname, safe='')
            link = linkpath / linkname
            if isinstance(obj, pygit2.Blob):
                blobmeta = todoblobs.setdefault(obj, [])
                blobmeta += [(obj.filemode, obj.name)]
                tree = gen_dir / "blobs" / str(obj.id)
                linktarget = get_relative(path, tree)
                link.symlink_to(linktarget, target_is_directory=True)
                # FIXME html-escape
                f.write("<li>")
                f.write("<a href=\"./{}\">{}</a>".format(quoted, quoted))
                #f.write(" <span class=\"bytes\">{}</span>".format(obj.size))
                f.write("</li>")
            elif isinstance(obj, pygit2.Tree):
                todotrees.add(obj)
                tree = gen_dir / "trees" / str(obj.id)
                linktarget = get_relative(path, tree)
                link.symlink_to(linktarget, target_is_directory=True)
                # FIXME html-escape
                f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
            else:
                # TODO not implemented, sorry. altho apparently submodules use
                # commits in trees?
                raise TypeError
        f.write("</ul></body></html>")

print("generating blobs")

# build blobs
while todoblobs:
    (b, meta) = todoblobs.popitem()
    path = gen_dir / "blobs" / str(b.id)
    rawpath = build_dir / "blobs" / str(b.id)
    path.mkdir(parents=True,exist_ok=True)
    rawpath.mkdir(parents=True,exist_ok=True)
    index = path / "index.html"
    try:
        f = index.open("x")
    except FileExistsError:
        # check if we've already visited this tree
        continue
    with f:
        f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>blob</title><body>")
        f.write("<a href=\"./raw.bin\">view raw</a> <span class=\"bytes\">")
        f.write(str(b.size))
        f.write("</span>")
        try:
            text = b.data.decode("utf-8", errors="strict")
            lex = find_lexer(text, meta)
            if lex is not None:
                # limit highlighting to files within 256KiB
                if b.size <= (256 * 1024):
                    f.write(highlight(text, lex, HtmlFormatter()))
                else:
                    f.write("<div>files larger than 256KiB may not be rendered</div>")
            else:
                # TODO maybe just write `text` (html escaped)?
                pass
        except UnicodeError:
            pass
        f.write("</body></html>")
    raw = rawpath / "raw.bin"
    with raw.open("wb") as f:
        f.write(b)

# create index.html
path = gen_dir / "index.html"
with path.open("w") as f:
    f.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>index</title><body><ul>")
    if not repo.head_is_unborn:
        ref = repo.head
        quoted = quote(ref.name, safe='/')
        # FIXME html-escape
        f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
    for ref in repo.references:
        ref = repo.references.get(ref)
        quoted = quote(ref.name, safe='/')
        # FIXME html-escape
        f.write("<li><a href=\"./{}\">{}</a></li>".format(quoted, quoted))
    f.write("</ul></body></html>")

print("running soupault")

# run soupault on it. note that soupault currently follows symlinks, but we
# workaround it.
subprocess.run(
    [
        soupault,
        "--site-dir",
        gen_dir,
        "--build-dir",
        build_dir,
    ],
    cwd=pathlib.Path(CONFIG_HOME),
    env={
        **os.environ,
        'SOUPAULT_CONFIG': '/dev/fd/{}'.format(soupault_config.fileno())
    },
    check=True,
    pass_fds=[soupault_config.fileno()]
)

print("copying to output")

# CANNOT use shutil.copytree - it is broken.
# also need to be aware of copying into a directory, so we just always make it
# a directory.
browse = pathlib.Path.cwd() / "browse"
browse.mkdir(parents=True,exist_ok=True)
subprocess.run(["cp", "-R", "-P", *build_dir.glob("*"), browse], check=True)

# └── gen
#     ├── blobs
#     │   └── e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#     │       ├── index.html
#     │       └── raw.bin
#     ├── commits
#     │   ├── 21177a2933b1a9d21d8437159405c5bc68b4d32e
#     │   │   ├── index.html
#     │   │   └── tree -> ../../trees/1663be45d5f6b9f092c4b98d44cf7992b427172f
#     │   └── 3ea9318f6271ece3c7560f18d0b22f50bd3cefe5
#     │       ├── index.html
#     │       └── tree -> ../../trees/17d6338b3a3dc189bdc3bea8481fe5f32fd388c8
#     ├── refs
#     │   └── heads
#     │       └── default
#     │           ├── index.html
#     │           └── tree -> ../../../trees/1663be45d5f6b9f092c4b98d44cf7992b427172f
#     └── trees
#         ├── 1663be45d5f6b9f092c4b98d44cf7992b427172f
#         │   ├── bar -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#         │   ├── baz -> ../29ba47b07d262ad717095f2d94ec771194c4c083
#         │   ├── deleteme -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#         │   ├── foo -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#         │   └── index.html
#         ├── 17d6338b3a3dc189bdc3bea8481fe5f32fd388c8
#         │   ├── bar -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#         │   ├── baz -> ../29ba47b07d262ad717095f2d94ec771194c4c083
#         │   ├── foo -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
#         │   └── index.html
#         └── 29ba47b07d262ad717095f2d94ec771194c4c083
#             ├── index.html
#             └── qux -> ../../blobs/e69de29bb2d1d6434b8b29ae775ad8c2e48c5391