xgit simple git

xgit

this project — static git page generator by polymath

kb.hax.al
clone git clone https://kb.hax.al/xgit

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.
MREADME.md+111-68
Mconfig.toml+41-12
Msync+1-0
Mxgit/cli.py+13-6
Mxgit/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