commit d04929cf134167d416bea430c07cd07c9b4f840c
author: polymath <polymath@localhost>
date: 2026-08-26 12:07
parents: 5e04fb86
Replace stagit with two local demos and expand README. Document remote HTTPS/SSH/git URL setup and present xgit as its own project.
| M | README.md | +111 | -68 |
| M | config.toml | +41 | -12 |
| M | sync | +1 | -0 |
| M | xgit/cli.py | +13 | -6 |
| M | xgit/fetch.py | +132 | -52 |
diff --git a/README.md b/README.md index 3b42f2a..1599d19 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,164 @@ # xgit -**Author:** polymath +**Author:** [polymath](mailto:polymath@localhost) +**License:** MIT -Python static git page generator inspired by [stagit](https://git.codemadness.org/stagit/). -Auto-fetches git repos from `config.toml`, builds static HTML (logs, files, refs, diffs, Markdown, syntax-colored code, author profiles, stats), then serves them. +`xgit` is its own standalone Python project: a static git page generator +(inspired by [stagit](https://git.codemadness.org/stagit/)). Point it at git +repositories (local demos or remotes), and it builds a browsable site — commit +logs, files, refs, Markdown, syntax-colored code, author profiles, and stats. + +``` +xgit/ ← this repository (the product) +├── build serve sync demo ← run from here +├── config.toml ← which repos to publish +├── xgit/ ← Python package +├── LICENSE README.md pyproject.toml +├── data/ (generated, gitignored) +└── html/ (generated, gitignored) +``` ## Requirements -- Python **3.10+** -- `git` on `PATH` -- No third-party packages +- Python 3.10+ +- `git` on your `PATH` +- No pip dependencies ## Quick start ```bash +cd /path/to/xgit # this repository root chmod +x build serve sync demo # once -./demo # clean seed + fetch + build + serve +./demo # seed 2 demos → build → serve :3121 ``` Open **http://127.0.0.1:3121/** -## Main run files (repo root) +You should see two demo repos: **hello** and **notes** (both by polymath). -| File | What it does | -|------|----------------| -| `./demo` | Wipe `data/` + `html/`, seed, fetch, build, serve `:3121` | -| `./sync` | Seed (if needed) + fetch remotes + build | -| `./build` | Generate static pages into `html/` | -| `./serve` | Build then serve (`--no-build` to skip build) | -| `config.toml` | Site settings + list of repositories | -| `python -m xgit …` | Full CLI (same as above) | +## Run files (project root) -```bash -./sync # update mirrors + rebuild -./build # rebuild only -./build --fetch # fetch then rebuild -./serve # build + http://0.0.0.0:3121 -./serve --no-build # serve existing html/ -./serve --port 8080 -``` +| Command | Purpose | +|---------|---------| +| `./demo` | Wipe generated data, seed demos, fetch, build, serve | +| `./sync` | Seed demos if needed + fetch remotes + build `html/` | +| `./build` | Generate static pages only (`./build --fetch` to update remotes first) | +| `./serve` | Build then serve on `0.0.0.0:3121` | +| `./serve --no-build` | Serve existing `html/` without rebuilding | +| `config.toml` | Site name, ports/URLs, and **repository list** | -Or without the wrappers: +Same via module API: ```bash python -m xgit seed -python -m xgit fetch -python -m xgit build python -m xgit sync -python -m xgit serve +python -m xgit build +python -m xgit serve --port 3121 ``` -## Configure repos +## Adding remote git repositories -Edit `config.toml`: +1. Edit **`config.toml`** in this repo’s root. +2. Add a `[[repos]]` block with a clone URL. +3. Run `./sync` (clones/updates mirrors under `data/repos/`, rebuilds `html/`). +4. Open or refresh the site (`./serve --no-build` if the server is already up). + +### HTTPS (GitHub / GitLab / Codeberg / …) ```toml -site_name = "xgit" +[[repos]] +name = "myproject" +url = "https://github.com/you/myproject.git" +description = "short description" owner = "polymath" -base_url = "http://127.0.0.1:3121" +``` + +### `git://` protocol + +```toml +[[repos]] +name = "sbase" +url = "git://git.suckless.org/sbase" +description = "suckless unix tools" +owner = "suckless" +``` +### SSH + +```toml [[repos]] -name = "hello" -url = "local:hello" # created by seed / demo -description = "demo repo" +name = "private-app" +url = "git@github.com:you/private-app.git" +description = "private work" owner = "polymath" +``` + +Needs your SSH agent / keys available to the machine running xgit. + +### Local path mirrors +```toml [[repos]] -name = "myproject" -url = "https://github.com/you/myproject.git" -description = "…" +name = "workdir" +url = "local:workdir" +description = "from data/seed/workdir" owner = "polymath" ``` -Then: +Put a normal git checkout at `data/seed/workdir/` (or use `./demo` / `seed` for the built-in demos). + +### After config changes ```bash -./sync -./serve --no-build +./sync # fetch all [[repos]] + rebuild +./serve --no-build # if already serving ``` -- Remote URLs → bare mirrors in `data/repos/<name>.git` -- `local:*` → mirrored from `data/seed/<name>/` +Mirrors live in `data/repos/<name>.git`. Generated pages live in `html/<name>/`. -## What you get +## What the site includes -Per repository: +**Per repository** -- `log.html` — commits -- `files.html` — tree -- `refs.html` — branches & tags -- `commit/<sha>.html` — diffstat + diff -- `file/<path>.html` — Markdown or syntax-colored code -- `file/<path>.raw.html` — raw + **Copy** -- `file/<path>` — download -- `atom.xml` / `tags.xml` +- Log, Files, Refs +- Commit pages (diffstat + diff) +- Markdown render + Raw + Download +- Syntax-colored code + Raw + Download +- Atom feeds -Site-wide: +**Site-wide** -- `index.html` — repos (+ owner → author profile) -- `authors/` — profiles (same email = same profile) + commit stats -- `stats.html` — overall totals & leaderboard +- Index of all repos (owner links to author profile) +- Authors (same email → same profile + stats) +- Stats (totals, leaderboard, per-repo) -## Layout +## Develop / use as a project + +This directory **is** the xgit git repository: +```bash +git clone <your-xgit-remote> +cd xgit +./demo ``` -xgit/ # Python package - cli.py # CLI entry - generate.py # HTML generator - fetch.py # clone/pull + seed - … -config.toml # your repos -build | serve | sync | demo # root runners -LICENSE -README.md + +Optional install as a console script: + +```bash +pip install -e . +xgit sync +xgit serve ``` -Generated (gitignored): `data/`, `html/` +## Layout + +| Path | Role | +|------|------| +| `xgit/` | Library + CLI (`python -m xgit`) | +| `config.toml` | Your published repo list | +| `build` `serve` `sync` `demo` | Root runners | +| `data/` | Seeds + bare mirrors (ignored) | +| `html/` | Generated static site (ignored) | ## License diff --git a/config.toml b/config.toml index 7adf03d..aeb7ada 100644 --- a/config.toml +++ b/config.toml @@ -1,13 +1,13 @@ -# xgit — config -# Author: polymath +# xgit — site config (author: polymath) # -# Quick start (from repo root): -# ./demo # seed + fetch + build + serve -# ./build # generate html/ -# ./serve # build then serve on :3121 +# From repo root: +# ./demo clean seed + build + serve +# ./sync seed + fetch + build +# ./build generate html/ +# ./serve build + serve on :3121 site_name = "xgit" -site_description = "static git page generator by polymath" +site_description = "static git hosting pages — by polymath" owner = "polymath" base_url = "http://127.0.0.1:3121" clone_base = "http://127.0.0.1:3121" @@ -17,14 +17,43 @@ html_dir = "html" max_commits = 200 max_diff_bytes = 1048576 +# ── Demo repositories (local, created by ./demo or ./sync) ───────────── + [[repos]] name = "hello" url = "local:hello" -description = "demo repository seeded by xgit" +description = "first demo — Python greeting + Markdown docs" owner = "polymath" [[repos]] -name = "stagit" -url = "git://git.codemadness.org/stagit" -description = "static git page generator (upstream inspiration)" -owner = "Hiltjo Posthuma" +name = "notes" +url = "local:notes" +description = "second demo — tiny notes app sketch (JS/CSS/JSON)" +owner = "polymath" + +# ── Add your own remotes (uncomment / copy) ──────────────────────────── +# +# HTTPS: +# [[repos]] +# name = "myproject" +# url = "https://github.com/you/myproject.git" +# description = "my project description" +# owner = "polymath" +# +# Git protocol: +# [[repos]] +# name = "sbase" +# url = "git://git.suckless.org/sbase" +# description = "suckless unix tools" +# owner = "suckless" +# +# SSH (needs your keys / agent): +# [[repos]] +# name = "private-app" +# url = "git@github.com:you/private-app.git" +# description = "private work" +# owner = "polymath" +# +# After editing, run: +# ./sync +# ./serve --no-build diff --git a/sync b/sync index 6397b99..6c85c48 100755 --- a/sync +++ b/sync @@ -1,5 +1,6 @@ #!/usr/bin/env bash # Seed local demo + fetch remotes + build html/ +# Usage: ./sync [--no-seed] set -euo pipefail cd "$(dirname "$0")" exec python -m xgit sync "$@" diff --git a/xgit/cli.py b/xgit/cli.py index 65ad66f..144d89e 100644 --- a/xgit/cli.py +++ b/xgit/cli.py @@ -8,7 +8,7 @@ from pathlib import Path from xgit import __version__ from xgit.config import load_config -from xgit.fetch import fetch_all, seed_hello +from xgit.fetch import fetch_all, seed_demos, seed_hello, seed_notes from xgit.generate import generate_all from xgit.serve import serve @@ -16,7 +16,7 @@ from xgit.serve import serve def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="xgit", - description="Python static git page generator with auto-fetch (stagit-inspired)", + description="xgit — static git page generator by polymath", ) parser.add_argument( "-c", @@ -27,7 +27,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--version", action="version", version=f"xgit {__version__}") sub = parser.add_subparsers(dest="cmd", required=True) - sub.add_parser("seed", help="create local demo repository (hello)") + sub.add_parser("seed", help="create local demo repositories (hello + notes)") sub.add_parser("fetch", help="clone/update all repos from config") p_build = sub.add_parser( @@ -40,7 +40,6 @@ def main(argv: list[str] | None = None) -> int: help="fetch/update mirrors before building", ) - # alias kept for compatibility sub.add_parser("generate", help="alias for build") p_sync = sub.add_parser("sync", help="seed (if needed) + fetch + build") @@ -72,7 +71,7 @@ def main(argv: list[str] | None = None) -> int: try: if args.cmd == "seed": - seed_hello(cfg) + seed_demos(cfg) elif args.cmd == "fetch": fetch_all(cfg) elif args.cmd in ("build", "generate"): @@ -102,8 +101,16 @@ def main(argv: list[str] | None = None) -> int: def _maybe_seed(cfg) -> None: - if any(r.url.startswith("local:hello") for r in cfg.repos): + locals_needed = { + r.url.split(":", 1)[1] + for r in cfg.repos + if r.url.startswith("local:") + } + if "hello" in locals_needed: seed_hello(cfg) + if "notes" in locals_needed: + seed_notes(cfg) + # Any other local:* names must already exist under data/seed/ if __name__ == "__main__": diff --git a/xgit/fetch.py b/xgit/fetch.py index 7753ba4..15c0abf 100644 --- a/xgit/fetch.py +++ b/xgit/fetch.py @@ -28,7 +28,6 @@ def fetch_one(cfg: Config, repo: RepoConfig) -> Path: ) _mirror_from_workdir(seed_dir, dest) else: - # Refresh mirror from seed if seed exists if seed_dir.exists(): _mirror_from_workdir(seed_dir, dest) if repo.description: @@ -56,9 +55,123 @@ def _mirror_from_workdir(workdir: Path, dest: Path) -> None: gitutil.run_git(["clone", "--mirror", str(workdir), str(dest)]) +def seed_demos(cfg: Config) -> list[Path]: + """Create the two local demo repositories: hello + notes.""" + return [seed_hello(cfg), seed_notes(cfg)] + + +# Back-compat alias used by older call sites def seed_hello(cfg: Config) -> Path: - """Create a tiny local demo repository.""" - seed = cfg.root / "data" / "seed" / "hello" + return _seed_repo( + cfg, + name="hello", + files={ + "README.md": ( + "# hello\n\n" + "Demo repository for **xgit** by polymath.\n\n" + "Shows Markdown rendering, code highlighting, and commit history.\n\n" + "## Try it\n\n" + "```python\n" + "print('hello from xgit')\n" + "```\n\n" + "| Page | Purpose |\n" + "| --- | --- |\n" + "| Log | commits |\n" + "| Files | source tree |\n" + "| Refs | branches & tags |\n" + ), + "LICENSE": ( + "MIT License\n\nCopyright (c) 2026 polymath\n\n" + "Permission is hereby granted, free of charge, to any person obtaining a copy " + "of this software and associated documentation files (the \"Software\"), to deal " + "in the Software without restriction.\n" + ), + "hello.py": ( + '#!/usr/bin/env python3\n"""Say hello."""\n\n\n' + "def main() -> None:\n" + ' print("hello from xgit")\n\n\n' + 'if __name__ == "__main__":\n' + " main()\n" + ), + "docs/guide.md": ( + "# Guide\n\n" + "Welcome to the *hello* guide.\n\n" + "1. Open the Files tab\n" + "2. Browse `hello.py`\n" + "3. Switch Raw / Download\n" + ), + }, + second_commit=( + "hello.py", + ( + '#!/usr/bin/env python3\n"""Say hello — louder."""\n\n\n' + "def main() -> None:\n" + ' print("hello from xgit!")\n' + ' print("static git pages, auto-fetched.")\n\n\n' + 'if __name__ == "__main__":\n' + " main()\n" + ), + "make greeting a bit louder", + ), + tag="v0.1.0", + ) + + +def seed_notes(cfg: Config) -> Path: + return _seed_repo( + cfg, + name="notes", + files={ + "README.md": ( + "# notes\n\n" + "Second **xgit** demo — a tiny notes app sketch.\n\n" + "## Ideas\n\n" + "- Keep notes in Markdown\n" + "- Sync via git\n" + "- Browse with xgit\n\n" + "> Author: **polymath**\n" + ), + "notes.json": '{\n "notes": [\n {"id": 1, "title": "welcome", "body": "first note"}\n ]\n}\n', + "app.js": ( + "/** Tiny notes sketch */\n" + "export function addNote(list, title, body) {\n" + " const id = list.length ? list[list.length - 1].id + 1 : 1;\n" + " return [...list, { id, title, body }];\n" + "}\n" + ), + "style.css": ( + ":root { --ink: #1e1230; --king: #6b21a8; }\n" + "body { font-family: system-ui; color: var(--ink); }\n" + "h1 { color: var(--king); }\n" + ), + }, + second_commit=( + "app.js", + ( + "/** Tiny notes sketch */\n" + "export function addNote(list, title, body) {\n" + " const id = list.length ? list[list.length - 1].id + 1 : 1;\n" + " return [...list, { id, title, body: body.trim() }];\n" + "}\n\n" + "export function listTitles(list) {\n" + " return list.map((n) => n.title);\n" + "}\n" + ), + "add listTitles helper", + ), + tag="v0.1.0", + ) + + +def _seed_repo( + cfg: Config, + *, + name: str, + files: dict[str, str], + second_commit: tuple[str, str, str] | None = None, + tag: str | None = None, +) -> Path: + seed = cfg.root / "data" / "seed" / name if seed.exists() and gitutil.is_git_repo(seed): print(f"seed already exists: {seed}") return seed @@ -71,57 +184,24 @@ def seed_hello(cfg: Config) -> Path: gitutil.run_git(["config", "user.name", "polymath"], cwd=seed) gitutil.run_git(["config", "user.email", "polymath@localhost"], cwd=seed) - (seed / "README.md").write_text( - "# hello\n\n" - "Tiny demo repository generated by **xgit**.\n\n" - "This project mirrors [stagit](https://git.codemadness.org/stagit/) in Python:\n" - "static HTML for logs, files, refs, commits, and Atom feeds.\n\n" - "## Features\n\n" - "- Auto-fetch mirrors from `config.toml`\n" - "- Markdown rendering for `.md` files\n" - "- Raw source view with line anchors\n\n" - "```python\n" - "print('hello from xgit')\n" - "```\n\n" - "> Built by **polymath** · white + royal purple.\n\n" - "| Page | Description |\n" - "| --- | --- |\n" - "| Log | commits |\n" - "| Files | tree |\n" - "| Refs | branches & tags |\n", - encoding="utf-8", - ) - (seed / "docs").mkdir(exist_ok=True) - (seed / "docs" / "guide.md").write_text( - "# Guide\n\n" - "Welcome to the *hello* guide.\n\n" - "1. Clone the repo\n" - "2. Run `python hello.py`\n" - "3. Enjoy\n\n" - "Inline `code` and a [link](https://example.com).\n", - encoding="utf-8", - ) - (seed / "LICENSE").write_text( - "MIT License\n\nCopyright (c) 2026 polymath\n\n" - "Permission is hereby granted, free of charge, to any person obtaining a copy " - "of this software and associated documentation files (the \"Software\"), to deal " - "in the Software without restriction.\n", - encoding="utf-8", - ) - (seed / "hello.py").write_text( - '#!/usr/bin/env python3\n"""Say hello."""\n\n\ndef main() -> None:\n print("hello from xgit")\n\n\nif __name__ == "__main__":\n main()\n', - encoding="utf-8", - ) + for rel, content in files.items(): + path = seed / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + gitutil.run_git(["add", "."], cwd=seed) - gitutil.run_git(["commit", "-m", "initial commit: hello world"], cwd=seed) + gitutil.run_git(["commit", "-m", f"initial commit: {name}"], cwd=seed) - (seed / "hello.py").write_text( - '#!/usr/bin/env python3\n"""Say hello — louder."""\n\n\ndef main() -> None:\n print("hello from xgit!")\n print("static git pages, auto-fetched.")\n\n\nif __name__ == "__main__":\n main()\n', - encoding="utf-8", - ) - gitutil.run_git(["add", "hello.py"], cwd=seed) - gitutil.run_git(["commit", "-m", "make greeting a bit louder"], cwd=seed) + if second_commit: + rel, content, message = second_commit + path = seed / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + gitutil.run_git(["add", rel], cwd=seed) + gitutil.run_git(["commit", "-m", message], cwd=seed) + + if tag: + gitutil.run_git(["tag", "-a", tag, "-m", tag], cwd=seed) - gitutil.run_git(["tag", "-a", "v0.1.0", "-m", "first tag"], cwd=seed) print(f"seeded {seed}") return seed