Files
kanbante/app/code_links.py
T
Spark 675ce09083 feat(code): cartes liées au code — branches, PRs, commits (issue #1)
app/code_links.py: appariement pur (#n dans titres/corps/messages,
numéro en jeton de branche). Client: list_branches/list_pulls/
list_commits. Badge sur les cartes (board + fragments live),
section Code lié du panneau détail, /api/issues/{o}/{r}/{n}/code.
2026-10-11 14:21:11 -04:00

107 lines
3.5 KiB
Python

"""Code links (roadmap #1): what code belongs to a card?
Pure matching helpers — a branch, pull request or commit is linked
to an issue when it references the issue number the way developers
actually write it: `#12` in a title/body/message, or the number as
a token in a branch name (`feature/12-login`, `issue-12`, `12-fix`).
"""
import re
_REF_RE = re.compile(r"(?<![\w#])#(\d+)\b")
_BRANCH_TOKEN_RE = re.compile(r"(?:^|[-_/])(\d+)(?:[-_/]|$)")
def extract_issue_refs(text: str | None) -> set[int]:
"""All #n references in a text (titles, bodies, messages)."""
if not text:
return set()
return {int(m.group(1)) for m in _REF_RE.finditer(text)}
def branch_issue_number(branch_name: str | None) -> int | None:
"""The issue number encoded in a branch name, if any."""
if not branch_name:
return None
m = _BRANCH_TOKEN_RE.search(branch_name)
return int(m.group(1)) if m else None
def find_code_links(
branches: list[dict],
pulls: list[dict],
commits: list[dict],
number: int,
) -> dict:
"""Linked branches/PRs/commits for one issue number."""
linked_branches = [
b.get("name", "")
for b in branches
if branch_issue_number(b.get("name")) == number
]
linked_pulls = []
for pr in pulls:
head_ref = ((pr.get("head") or {}).get("ref")) or ""
refs = extract_issue_refs(pr.get("title")) | extract_issue_refs(
pr.get("body")
)
if number in refs or branch_issue_number(head_ref) == number:
linked_pulls.append(
{
"number": pr.get("number"),
"title": pr.get("title", ""),
"state": pr.get("state", "open"),
"merged": bool(pr.get("merged")),
"html_url": pr.get("html_url", ""),
"branch": head_ref,
}
)
linked_commits = []
for commit in commits:
message = ((commit.get("commit") or {}).get("message")) or ""
if number in extract_issue_refs(message):
linked_commits.append(
{
"sha": (commit.get("sha") or "")[:7],
"message": message.splitlines()[0] if message else "",
"html_url": commit.get("html_url", ""),
"date": (
((commit.get("commit") or {}).get("author") or {}).get(
"date", ""
)
or ""
)[:10],
}
)
# Best PR to badge: a merged one beats an open one beats closed.
pr_badge = None
for wanted in ("merged", "open", "closed"):
for pr in linked_pulls:
state = (
"merged" if pr["merged"]
else "open" if pr["state"] == "open" else "closed"
)
if state == wanted:
pr_badge = {
"number": pr["number"],
"state": state,
"label": {
"merged": "fusionnée",
"open": "ouverte",
"closed": "fermée",
}[state],
}
break
if pr_badge:
break
return {
"branches": linked_branches,
"pulls": linked_pulls,
"commits": linked_commits,
"summary": {
"branches": len(linked_branches),
"commits": len(linked_commits),
"pr": pr_badge,
},
}