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.
107 lines
3.5 KiB
Python
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,
|
|
},
|
|
}
|