internal/cli:stalled 與 xrefs 命令接線,完成 #7 CLI 層(堆疊於 #23)
- workflow 新增 StalledSource 介面(Source+CurrentUser+GetIssue)與 高階入口 RunStalled/ResolveIssueXrefs(xrefs 輸出附 source 出處) - APIClient 補上 StalledSource 面向(GetIssue/TryGetIssue 等) - CLI 新增 teai stalled [--hours N](JSON 與 gitea.py 位元組相容: 停用 HTML 轉義、縮排 2)與 teai xrefs <owner>/<repo> <number> (404 → exit 3);table 模式 stalled_hours 定點一位小數 - 接線層與 workflow 層單元測試(注入假資料源、固定時刻) - README 狀態行更新 驗證:go build/vet/test 全綠;對真實 API 並行掃描,stalled 輸出 與 gitea.py 位元組一致(2169 bytes);xrefs 邊界案例(PR#50/ issue#49 略過、CJK 緊鄰、跨倉庫 owner/repo#N)與 Python 版一致。
This commit is contained in:
@@ -526,3 +526,111 @@ func round1(v float64) float64 {
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ── CLI 接線用的高階入口(#7:teai stalled/teai xrefs)─────────────────
|
||||
|
||||
// StalledSource 是 stalled/xrefs 命令需要的完整資料源:
|
||||
// Source(掃描用讀取)加上目前帳號與單一 issue 查詢。
|
||||
// APIClient(giteaapi.go)實作此介面;測試以假資料源注入。
|
||||
type StalledSource interface {
|
||||
Source
|
||||
// CurrentUser 回傳目前帳號名(催促冷卻判定用)。
|
||||
CurrentUser() (string, error)
|
||||
// GetIssue 嚴格版單一 issue/PR 查詢;失敗回錯誤(xrefs 命令用,
|
||||
// 404 應回 ErrAPI 由 CLI 映射結束碼 3)。
|
||||
GetIssue(owner, repo string, number int64) (Issue, error)
|
||||
}
|
||||
|
||||
// RunStalled 是 teai stalled 的核心:解析目前帳號後跑 ScanStalled。
|
||||
// hours <= 0 時採 StalledDefaultHours(對應 gitea.py stalled [--hours N]
|
||||
// 的預設 4 小時);旗標層已先攔下非正數,這裡是防禦。
|
||||
func RunStalled(src StalledSource, hours float64, now time.Time) ([]Item, error) {
|
||||
if hours <= 0 {
|
||||
hours = StalledDefaultHours
|
||||
}
|
||||
username, err := src.CurrentUser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ScanStalled(src, username, hours, now)
|
||||
}
|
||||
|
||||
// XrefHit 是 teai xrefs 的單筆輸出:解析後的目標加上出處
|
||||
// (README 規格:"body"=內文、"comment"=留言)。
|
||||
type XrefHit struct {
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Number int64 `json:"number"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// ResolveIssueXrefs 是 teai xrefs 的核心:取回單一 issue/PR 的內文與
|
||||
// 全部留言,解析其中的 #N、repo#N、owner/repo#N 引用(規則同
|
||||
// ResolveXrefTargets:單段 repo 僅在父項所屬組織有該倉庫時解析、
|
||||
// owner/repo#N 僅在 owner 為所屬組織時解析),去重並保留出現順序,
|
||||
// 每筆附首次出現的出處(內文先於留言)。stalled 的父追蹤項判定
|
||||
// 內部用同一套解析,不重複實作。
|
||||
func ResolveIssueXrefs(src StalledSource, owner, repo string, number int64) ([]XrefHit, error) {
|
||||
issue, err := src.GetIssue(owner, repo, number)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
comments, err := src.IssueComments(owner, repo, number)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgs, err := src.MyOrganizations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
myOrgs := make(map[string]bool, len(orgs))
|
||||
for _, o := range orgs {
|
||||
myOrgs[o] = true
|
||||
}
|
||||
// 組織倉庫清單跨呼叫快取,避免逐則留言重打 API。
|
||||
orgRepoCache := map[string][]string{}
|
||||
orgRepos := func(org string) ([]string, error) {
|
||||
if names, ok := orgRepoCache[org]; ok {
|
||||
return names, nil
|
||||
}
|
||||
repos, err := src.OrganizationRepositories(org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, len(repos))
|
||||
for _, r := range repos {
|
||||
if r.Name != "" {
|
||||
names = append(names, r.Name)
|
||||
}
|
||||
}
|
||||
orgRepoCache[org] = names
|
||||
return names, nil
|
||||
}
|
||||
type sourceText struct {
|
||||
text string
|
||||
source string
|
||||
}
|
||||
texts := []sourceText{{issue.Body, "body"}}
|
||||
for _, c := range comments {
|
||||
texts = append(texts, sourceText{c.Body, "comment"})
|
||||
}
|
||||
var hits []XrefHit
|
||||
seen := map[XrefTarget]bool{}
|
||||
for _, t := range texts {
|
||||
targets, err := ResolveXrefTargets(owner, repo, []string{t.text}, myOrgs, orgRepos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, target := range targets {
|
||||
if seen[target] {
|
||||
continue
|
||||
}
|
||||
seen[target] = true
|
||||
hits = append(hits, XrefHit{
|
||||
Owner: target.Owner, Repo: target.Repo,
|
||||
Number: target.Number, Source: t.source,
|
||||
})
|
||||
}
|
||||
}
|
||||
return hits, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user