596cf853e8
`labels.py` handed `_gitea.api` the issue store as a place to put the request file, and on a checkout without a store that quietly created `tmp/issues/.payload/`. Bootstrapping a repository's labels touches no issue at all, so the one rule the store has — nothing materializes it as a side effect of a write — was broken by an operation that has no business knowing the store exists. Where a request body goes was never the caller's decision to make. It is now the transport's: `tmp/payload/`, resolved from `_gitea.py`'s own location the way both domains resolve theirs, so every caller — sync and wiki alike — writes to one directory whatever it was invoked from, and `out_root` is gone from `api`, `add_dependency` and all six call sites. The directory is created by the first write of a run and not before: a `--dry-run` leaves nothing behind. `tmp/` is already gitignored. The name carries the distinction the old path lost. A store holds the only copy of something; this holds debris kept for a retry or a post-mortem, and deleting it costs nothing. A dotdir sitting among an issue's files claimed otherwise, and `ls tmp/issues` started lying about what existed. tests/test_payload_root.py runs the real `labels.py` in a throwaway repo against a fake `tea` on PATH: no store appears, the payloads land in tmp/payload/, a dry run writes nothing, and a run from a subdirectory still resolves to the repo root. Two source checks keep the callers from drifting apart again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
5.7 KiB
Python
152 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
wiki_push.py — publish a local space to a wiki.
|
|
|
|
wiki_push.py -m "Import the simple-chains discussion"
|
|
wiki_push.py --prefix "Simple Chains/Ideas" -m "Rework B-04"
|
|
wiki_push.py -m "Fix the send-timing table" Simple-Chains/Ideas/Send-timing.md
|
|
|
|
Every page in the selection is compared against `pushed` — the hash of what was
|
|
last published — and only the ones that differ are sent. That is the whole of
|
|
change detection: no timestamps, no drift model.
|
|
|
|
A page with no `sub_url` is created; a page with one is edited in place. The
|
|
title comes from the manifest, never re-derived from the file, because sending
|
|
a different title to the edit endpoint is a RENAME and leaves nothing behind at
|
|
the old address.
|
|
|
|
Pushing is additive. A page deleted locally is NOT deleted in the wiki — the
|
|
manifest simply stops mentioning it. Removing a published page is an explicit
|
|
act; do it in the web UI or with a DELETE through /tea:use.
|
|
|
|
Usage:
|
|
wiki_push.py -m MESSAGE [--space SPACE] [--repo owner/repo]
|
|
[--prefix TITLE] [--dry-run] [--out DIR] [PATH-or-TITLE ...]
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, _HERE)
|
|
sys.path.insert(0, os.path.join(_HERE, "..", "..", "sync", "scripts"))
|
|
sys.path.insert(0, os.path.join(_HERE, "..", "..", "page", "scripts"))
|
|
import _gitea # noqa: E402
|
|
import page # noqa: E402
|
|
import wikimap # noqa: E402
|
|
|
|
|
|
def select(manifest, prefix, targets):
|
|
"""The pages to consider, in tree order.
|
|
|
|
A positional argument matches a manifest path or a title, exactly. Exact
|
|
because a near-miss that silently selects nothing is indistinguishable from
|
|
a clean no-op run, and the operator finds out only when the page never
|
|
appears."""
|
|
pages = (page.children_of(manifest, prefix) if prefix
|
|
else page.sorted_pages(manifest))
|
|
if not targets:
|
|
return pages, []
|
|
want, chosen, hit = set(targets), [], set()
|
|
for p, e in pages:
|
|
if p in want or e.get("title") in want:
|
|
chosen.append((p, e))
|
|
hit.add(p if p in want else e.get("title"))
|
|
return chosen, sorted(want - hit)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("targets", nargs="*", metavar="PATH-or-TITLE")
|
|
ap.add_argument("-m", "--message", required=True,
|
|
help="wiki commit message for this push")
|
|
ap.add_argument("--repo", help="owner/repo (default: the repo in CWD)")
|
|
ap.add_argument("--space", help="local space (default: the owner/repo slug)")
|
|
ap.add_argument("--prefix", default="", help="only titles at or under this")
|
|
ap.add_argument("--dry-run", action="store_true")
|
|
ap.add_argument("--out", help="wiki cache root (default: <repo>/tmp/wiki)")
|
|
a = ap.parse_args()
|
|
|
|
login = _gitea.require_login()
|
|
base = _gitea.repo_base(a.repo)
|
|
slug = _gitea.repo_slug(login, a.repo)
|
|
space = a.space or slug
|
|
root = a.out or page.WIKI_ROOT
|
|
space_dir = page.space_root(space, root)
|
|
if not os.path.isdir(space_dir):
|
|
_gitea.die("no such space: %s (looked in %s). Import or pull first."
|
|
% (space, space_dir))
|
|
|
|
manifest = page.load_manifest(space, root)
|
|
if not manifest["pages"]:
|
|
_gitea.die("space %s has no pages in its manifest" % space)
|
|
|
|
chosen, missing = select(manifest, a.prefix, a.targets)
|
|
for t in missing:
|
|
_gitea.warn("not in the manifest: %s" % t)
|
|
if not chosen:
|
|
_gitea.die("nothing selected")
|
|
|
|
created = updated = skipped = 0
|
|
for relpath, e in chosen:
|
|
title = e.get("title")
|
|
full = os.path.join(space_dir, relpath)
|
|
if not title:
|
|
_gitea.warn("%s has no title in the manifest; skipped" % relpath)
|
|
continue
|
|
if not os.path.isfile(full):
|
|
_gitea.warn("%s is in the manifest but not on disk; skipped" % relpath)
|
|
continue
|
|
with open(full, encoding="utf-8") as f:
|
|
text = f.read()
|
|
h = page.body_hash(text)
|
|
|
|
if e.get("sub_url") and h == e.get("pushed"):
|
|
skipped += 1
|
|
continue
|
|
|
|
verb = "create" if not e.get("sub_url") else "update"
|
|
print("%-7s %-44s %s" % (verb, relpath, title))
|
|
if a.dry_run:
|
|
created += verb == "create"
|
|
updated += verb == "update"
|
|
continue
|
|
|
|
if verb == "create":
|
|
payload = wikimap.new_payload(title, text, a.message)
|
|
got = _gitea.api(login, "%s/wiki/new" % base, method="POST",
|
|
payload=payload, payload_name="wiki-new")
|
|
else:
|
|
payload = wikimap.edit_payload(title, text, a.message)
|
|
got = _gitea.api(login, wikimap.page_endpoint(base, e["sub_url"]),
|
|
method="PATCH", payload=payload,
|
|
payload_name="wiki-edit")
|
|
|
|
if not isinstance(got, dict) or not got.get("sub_url"):
|
|
_gitea.warn("%s: no page returned; the manifest is unchanged for it"
|
|
% title)
|
|
continue
|
|
|
|
# sub_url comes back from Gitea and is stored as given. It is the only
|
|
# address this page has, and it is not something we could have computed.
|
|
e.update(wikimap.from_payload(got))
|
|
e["synced"] = _gitea.now_iso()
|
|
e["pushed"] = h
|
|
manifest["pages"][relpath] = e
|
|
created += verb == "create"
|
|
updated += verb == "update"
|
|
|
|
if a.dry_run:
|
|
print("\ndry run — %d to create, %d to update, %d unchanged"
|
|
% (created, updated, skipped))
|
|
return 0
|
|
|
|
page.save_manifest(manifest, root)
|
|
print("\n%d created, %d updated, %d unchanged -> %s wiki"
|
|
% (created, updated, skipped, slug))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|