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.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user