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:
@@ -0,0 +1,170 @@
|
||||
// stalled_commands.go — 管理者巡邏命令的 CLI 接線(#7):
|
||||
// stalled [--hours N] 與 xrefs <owner>/<repo> <number>。
|
||||
//
|
||||
// 核心邏輯在 internal/workflow(ScanStalled/ResolveIssueXrefs),
|
||||
// 本檔只做旗標剖析、資料源接線與輸出格式化。輸出走得體架構的
|
||||
// Printer(--output json|table);錯誤經 cli.exitCodeFor 映射結束碼
|
||||
// (用法 2/API 3)。token 解析選序見 internal/gitea/auth.go。
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.alterminal.com/alterminal/teai/internal/gitea"
|
||||
"gitea.alterminal.com/alterminal/teai/internal/workflow"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["stalled"] = &command{
|
||||
name: "stalled",
|
||||
usage: "停滯的 open issues/PR(管理者巡邏;--hours N 門檻,預設 4)",
|
||||
run: runStalled,
|
||||
}
|
||||
commands["xrefs"] = &command{
|
||||
name: "xrefs",
|
||||
usage: "解析 issue/PR 內文與留言中的 #N/repo#N/owner/repo#N 引用",
|
||||
run: runXrefs,
|
||||
}
|
||||
}
|
||||
|
||||
// newStalledSourceFunc 建立 stalled/xrefs 的資料源;宣告為變數讓
|
||||
// 接線層測試可注入假資料源(不走網路),與 newScannerFunc 同模式。
|
||||
var newStalledSourceFunc = newStalledSource
|
||||
|
||||
// newStalledSource 依全域選項建立 workflow.StalledSource(API 實作)。
|
||||
func newStalledSource(env *Env) (workflow.StalledSource, error) {
|
||||
token, _, err := gitea.ResolveToken(gitea.AuthOptions{
|
||||
FlagToken: env.Globals.Token,
|
||||
SiteURL: env.Globals.URL,
|
||||
ConfigPath: env.Globals.ConfigPath,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := gitea.New(env.Globals.URL, token, env.Globals.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &workflow.APIClient{Client: client}, nil
|
||||
}
|
||||
|
||||
// stalledFields 是 stalled 的 table 欄位(JSON 模式輸出完整 Item,
|
||||
// 欄位序與 gitea.py 一致,見 workflow.Item.MarshalJSON)。
|
||||
func stalledFields() []gitea.Field {
|
||||
return []gitea.Field{
|
||||
{Name: "type"}, {Name: "repo"}, {Name: "number"}, {Name: "title"},
|
||||
{Name: "last_comment_by"}, {Name: "last_activity_at"},
|
||||
{Name: "stalled_hours"}, {Name: "reason"},
|
||||
}
|
||||
}
|
||||
|
||||
// stalledRow 把 Item 轉成 table 列(assignees/reviewers/child_activity
|
||||
// 僅 JSON 模式輸出;table 給人工抽查用,取共同欄位)。
|
||||
// stalled_hours 定點一位小數(fmt.Sprint(9.0) 會印 "9",與 JSON 的 9.0 不一致)。
|
||||
func stalledRow(item workflow.Item) gitea.Values {
|
||||
return gitea.Values{
|
||||
item.Type, item.Repo, item.Number, item.Title,
|
||||
item.LastCommentBy, workflow.FormatMoment(item.LastActivityAt),
|
||||
strconv.FormatFloat(item.StalledHours, 'f', 1, 64), item.Reason,
|
||||
}
|
||||
}
|
||||
|
||||
// runStalled:teai stalled [--hours N]。
|
||||
// 預設門檻 4 小時;--hours 需為正數(同 gitea.py _parse_hours)。
|
||||
// JSON 輸出與 gitea.py stalled 位元組相容(workflow.Item 自訂 MarshalJSON;
|
||||
// 這裡輸出前停用 HTML 轉義並縮排 2,對應 Python json.dumps(indent=2))。
|
||||
func runStalled(env *Env, args []string) error {
|
||||
fs := flag.NewFlagSet("teai stalled", flag.ContinueOnError)
|
||||
fs.SetOutput(env.Err)
|
||||
hours := fs.Float64("hours", workflow.StalledDefaultHours, "停滯門檻(小時,正數)")
|
||||
if err := parseFlags(fs, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
return &ErrUsage{Msg: "stalled 不接受位置參數"}
|
||||
}
|
||||
if *hours <= 0 {
|
||||
return &ErrUsage{Msg: "--hours 需為正數(小時),例如 --hours 4"}
|
||||
}
|
||||
src, err := newStalledSourceFunc(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := workflow.RunStalled(src, *hours, nowFunc())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer(env)
|
||||
if p.Format == gitea.FormatTable {
|
||||
rows := make([]gitea.Values, 0, len(items))
|
||||
for _, item := range items {
|
||||
rows = append(rows, stalledRow(item))
|
||||
}
|
||||
return p.PrintList(stalledFields(), rows)
|
||||
}
|
||||
// JSON:空清單輸出 [](README:無停滯項目 → 空陣列);有項目時
|
||||
// 縮排 2 且停用 HTML 轉義,與 gitea.py 的 _dump 位元組相容。
|
||||
if len(items) == 0 {
|
||||
_, err := fmt.Fprintln(env.Out, "[]")
|
||||
return err
|
||||
}
|
||||
return printJSON(env.Out, items)
|
||||
}
|
||||
|
||||
// runXrefs:teai xrefs <owner>/<repo> <number>。
|
||||
// 編號不存在 → API 錯誤(結束碼 3);無引用 → []。
|
||||
func runXrefs(env *Env, args []string) error {
|
||||
fs := flag.NewFlagSet("teai xrefs", flag.ContinueOnError)
|
||||
fs.SetOutput(env.Err)
|
||||
if err := parseFlags(fs, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 2 {
|
||||
return &ErrUsage{Msg: "xrefs 需要 <owner>/<repo> <number> 兩個參數"}
|
||||
}
|
||||
owner, name, err := splitRepoFlag(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := strconv.ParseInt(args[1], 10, 64)
|
||||
if err != nil || number <= 0 {
|
||||
return &ErrUsage{Msg: fmt.Sprintf("編號需為正整數,得到 %q", args[1])}
|
||||
}
|
||||
src, err := newStalledSourceFunc(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hits, err := workflow.ResolveIssueXrefs(src, owner, name, number)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := printer(env)
|
||||
rows := make([]gitea.Values, 0, len(hits))
|
||||
for _, h := range hits {
|
||||
rows = append(rows, gitea.Values{h.Owner, h.Repo, h.Number, h.Source})
|
||||
}
|
||||
return p.PrintList(
|
||||
[]gitea.Field{
|
||||
{Name: "owner"}, {Name: "repo"}, {Name: "number"}, {Name: "source"},
|
||||
},
|
||||
rows,
|
||||
)
|
||||
}
|
||||
|
||||
// nowFunc 回傳目前時刻;宣告為變數讓測試可固定時刻(stalled 判定用)。
|
||||
var nowFunc = time.Now
|
||||
|
||||
// printJSON 以縮排 2、停用 HTML 轉義輸出 JSON(與 gitea.py 的
|
||||
// json.dumps(ensure_ascii=False, indent=2) 位元組相容;HTML 轉義會把
|
||||
// <、>、& 變 \u003c 等,破壞相容性)。
|
||||
func printJSON(w io.Writer, v any) error {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
Reference in New Issue
Block a user