- internal/workflow:Client 介面+判定語義(最後留言、作者/審核者跟進、 assignee 交集、最久未更新優先),含假 client 單元測試 - internal/workflow/giteaapi.go:以 internal/gitea(#5)實作 Client - internal/cli:註冊 whoami/orgs/members/mine/pulls/next;全域選項允許 出現在命令之後;結束碼 0/2/3 - 與 gitea.py 交叉驗證:next/mine/pulls/members(--has-work) 拾取一致 - 依 #5 分支 feature/issue5-gitea-client 為基底
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
package workflow
|
||
|
||
import (
|
||
"errors"
|
||
"sort"
|
||
)
|
||
|
||
// errIncompatibleFlags 對應 gitea.py 的「--mine 與 --reviewer 不能併用」。
|
||
var errIncompatibleFlags = errors.New("--mine 與 --reviewer 不能併用")
|
||
|
||
// Work 是 next 命令的輸出物件:Type 為 "pull" 或 "issue";Role 僅 pull 帶
|
||
// ("author" 或 "reviewer"),對應 gitea.py 的 _work_dict。
|
||
type Work struct {
|
||
Type string `json:"type"`
|
||
Role string `json:"role,omitempty"`
|
||
Repo string `json:"repo"`
|
||
Number int64 `json:"number"`
|
||
Title string `json:"title"`
|
||
URL string `json:"url"`
|
||
}
|
||
|
||
// issueNumber 回傳 issue 編號:number 缺漏時回退 index(gitea.py 語義)。
|
||
func (i *Issue) issueNumber() int64 {
|
||
if i.Number != 0 {
|
||
return i.Number
|
||
}
|
||
return i.Index
|
||
}
|
||
|
||
// Num 是 issueNumber 的公開介面(CLI 輸出用)。
|
||
func (i *Issue) Num() int64 { return i.issueNumber() }
|
||
|
||
// Next 回傳下一件未完成工作:先自己發起的待跟進 PR(role=author),再待審核
|
||
// 的 PR(role=reviewer,含尚無留言),再分派給自己的 open issue。各層皆以
|
||
// 「最久未更新優先」排序後取第一件(gitea.py 的 find_next_work)。
|
||
// 沒有任何工作時回傳 nil。
|
||
func (s *Scanner) Next() (*Work, error) {
|
||
username, err := s.Whoami()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
_ = username
|
||
|
||
// 第一層:自己發起、最後一則留言不是自己的 open PR。
|
||
minePulls, err := s.Pulls(true, false)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(minePulls) > 0 {
|
||
sorted := sortPullItemsByUpdated(minePulls)
|
||
return workFromPull(sorted[0], "author"), nil
|
||
}
|
||
|
||
// 第二層:自己是審核者、最後一則留言不是自己的 open PR。
|
||
reviewerPulls, err := s.Pulls(false, true)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(reviewerPulls) > 0 {
|
||
sorted := sortPullItemsByUpdated(reviewerPulls)
|
||
return workFromPull(sorted[0], "reviewer"), nil
|
||
}
|
||
|
||
// 第三層:分派給自己、最後一則留言不是 assignee 的 open issue。
|
||
issues, err := s.Mine()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(issues) > 0 {
|
||
sorted := sortIssuesByUpdated(issues)
|
||
return workFromIssue(&sorted[0]), nil
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// sortPullItemsByUpdated 依 updatedKey 升冪(最久未更新在前)穩定排序。
|
||
func sortPullItemsByUpdated(items []PullItem) []PullItem {
|
||
out := append([]PullItem(nil), items...)
|
||
sort.SliceStable(out, func(a, b int) bool {
|
||
return out[a].Pull.updatedKey() < out[b].Pull.updatedKey()
|
||
})
|
||
return out
|
||
}
|
||
|
||
// sortIssuesByUpdated 依 updatedKey 升冪(最久未更新在前)穩定排序。
|
||
func sortIssuesByUpdated(items []Issue) []Issue {
|
||
out := append([]Issue(nil), items...)
|
||
sort.SliceStable(out, func(a, b int) bool {
|
||
return out[a].updatedKey() < out[b].updatedKey()
|
||
})
|
||
return out
|
||
}
|
||
|
||
// workFromPull 由 PR 組出 Work 物件。
|
||
func workFromPull(item PullItem, role string) *Work {
|
||
return &Work{
|
||
Type: "pull",
|
||
Role: role,
|
||
Repo: item.Owner + "/" + item.Repo,
|
||
Number: item.Pull.Number,
|
||
Title: item.Pull.Title,
|
||
URL: item.Pull.WebURL(),
|
||
}
|
||
}
|
||
|
||
// workFromIssue 由 issue 組出 Work 物件。
|
||
func workFromIssue(issue *Issue) *Work {
|
||
return &Work{
|
||
Type: "issue",
|
||
Repo: issue.FullName(),
|
||
Number: issue.issueNumber(),
|
||
Title: issue.Title,
|
||
URL: issue.WebURL(),
|
||
}
|
||
}
|