Milestones 4 and 5: project layer, review gate and serial merge, all measured
This commit is contained in:
116
bin/lib/project.py
Normal file
116
bin/lib/project.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""The project convention: one project, one folder, one machine.
|
||||
|
||||
Paperclip owns the project record, its tasks, blockers, comments, runs, cost and
|
||||
worktrees. What it cannot know is which folder on which machine holds the work,
|
||||
which TUIOS session shows it, and which task the viewer is looking at. That is
|
||||
this file's whole job:
|
||||
|
||||
<project>/.harness/harness.toml
|
||||
|
||||
Nothing else is stored. The task list, the blocker edges and the worktrees are
|
||||
read from Paperclip every time.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import harness as h # noqa: E402
|
||||
|
||||
REL = os.path.join(".harness", "harness.toml")
|
||||
|
||||
|
||||
def path(root):
|
||||
return os.path.join(root, REL)
|
||||
|
||||
|
||||
def exists(root):
|
||||
return os.path.isfile(path(root))
|
||||
|
||||
|
||||
def load(root):
|
||||
p = path(root)
|
||||
if not os.path.isfile(p):
|
||||
raise h.Refused(2, "no %s here; run bin/office %s first" % (REL, root))
|
||||
with open(p, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def save(root, cfg):
|
||||
os.makedirs(os.path.dirname(path(root)), exist_ok=True)
|
||||
lines = [
|
||||
"# Written by bin/office. One project, one folder, one machine.",
|
||||
"# Paperclip owns the project, its tasks and its worktrees; this file only",
|
||||
"# remembers where the work is and what to show.",
|
||||
"",
|
||||
'session = "%s"' % cfg["session"],
|
||||
'watching = "%s"' % (cfg.get("watching") or ""),
|
||||
"",
|
||||
"[paperclip]",
|
||||
'company = "%s"' % cfg["paperclip"]["company"],
|
||||
'project = "%s"' % cfg["paperclip"]["project"],
|
||||
"",
|
||||
"[machine]",
|
||||
'host = "%s"' % cfg["machine"]["host"],
|
||||
'folder = "%s"' % cfg["machine"]["folder"],
|
||||
"",
|
||||
]
|
||||
with open(path(root), "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
|
||||
def this_machine():
|
||||
return socket.gethostname()
|
||||
|
||||
|
||||
def check_machine(cfg):
|
||||
"""Refuse to act on a project that belongs to another machine."""
|
||||
want = (cfg.get("machine") or {}).get("host") or ""
|
||||
here = this_machine()
|
||||
if want and want not in (here, here.split(".")[0]):
|
||||
raise h.Refused(
|
||||
6,
|
||||
"this project lives on %r but this machine is %r; run the command there, "
|
||||
"or pass --host %s" % (want, here, want),
|
||||
)
|
||||
|
||||
|
||||
def resolve_project(ref):
|
||||
"""Accept a project id, url key or name, and return the Paperclip record."""
|
||||
projects = h.api("GET", "/api/companies/%s/projects" % h.COMPANY)
|
||||
for p in projects:
|
||||
if ref in (p.get("id"), p.get("urlKey"), p.get("name")):
|
||||
return p
|
||||
raise h.Refused(2, "no Paperclip project matches %r" % ref)
|
||||
|
||||
|
||||
def active_tasks(project_id):
|
||||
issues = h.api("GET", "/api/companies/%s/issues" % h.COMPANY)
|
||||
mine = [i for i in issues if i.get("projectId") == project_id]
|
||||
order = {"in_progress": 0, "blocked": 1, "in_review": 2, "todo": 3, "backlog": 4, "done": 5, "cancelled": 6}
|
||||
mine.sort(key=lambda i: (order.get(i.get("status"), 9), i.get("updatedAt") or ""), reverse=False)
|
||||
return mine
|
||||
|
||||
|
||||
def watch_task(cfg):
|
||||
"""Which task the reply and log panes should show.
|
||||
|
||||
A person's last choice wins if it still exists; otherwise the most active
|
||||
task in the project, so the panes are never empty for no reason.
|
||||
"""
|
||||
tasks = active_tasks(cfg["paperclip"]["project"])
|
||||
wanted = cfg.get("watching") or ""
|
||||
for t in tasks:
|
||||
if t.get("identifier") == wanted or t.get("id") == wanted:
|
||||
return t["identifier"]
|
||||
for t in tasks:
|
||||
if t.get("status") not in ("done", "cancelled"):
|
||||
return t["identifier"]
|
||||
return tasks[0]["identifier"] if tasks else "none"
|
||||
|
||||
|
||||
def set_watch(root, cfg, identifier):
|
||||
cfg["watching"] = identifier
|
||||
save(root, cfg)
|
||||
Reference in New Issue
Block a user