- 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 為基底
138 lines
4.2 KiB
Go
138 lines
4.2 KiB
Go
package workflow
|
||
|
||
import "sort"
|
||
|
||
// sortComments 依 created_at 字典序穩定排序(ISO8601 字串字典序與時間序一致),
|
||
// 對應 gitea.py 各處的 comments.sort(key=created_at)。
|
||
func sortComments(comments []Comment) []Comment {
|
||
out := append([]Comment(nil), comments...)
|
||
sort.SliceStable(out, func(a, b int) bool { return out[a].CreatedAt < out[b].CreatedAt })
|
||
return out
|
||
}
|
||
|
||
// LastComment 回傳依 created_at 排序後的最後一則留言;沒有留言回 nil。
|
||
func LastComment(comments []Comment) *Comment {
|
||
ordered := sortComments(comments)
|
||
if len(ordered) == 0 {
|
||
return nil
|
||
}
|
||
return &ordered[len(ordered)-1]
|
||
}
|
||
|
||
// lastCommentAuthor 回傳最後一則留言的作者;無留言或作者未知回空字串。
|
||
func lastCommentAuthor(comments []Comment) string {
|
||
if last := LastComment(comments); last != nil {
|
||
return last.Author()
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func containsString(items []string, target string) bool {
|
||
for _, item := range items {
|
||
if item == target {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// NeedsAuthorFollowup 判定 PR 是否需要發起者跟進:有留言、且最後一則留言
|
||
// 不是 PR 作者(gitea.py 的 _pr_needs_author_followup)。
|
||
// 無留言或作者未知 → false(尚無留言的 PR 不列入跟進清單)。
|
||
func NeedsAuthorFollowup(pr *PullRequest, comments []Comment) bool {
|
||
author := pr.Author()
|
||
if author == "" {
|
||
return false
|
||
}
|
||
last := lastCommentAuthor(comments)
|
||
return last != "" && last != author
|
||
}
|
||
|
||
// NeedsReviewerFollowup 判定使用者是否需要審核此 PR:被指定為審核者、
|
||
// 不是作者、最後一則留言不是自己(含尚無留言)
|
||
// (gitea.py 的 _pr_needs_reviewer_followup)。
|
||
func NeedsReviewerFollowup(pr *PullRequest, comments []Comment, username string) bool {
|
||
if username == "" {
|
||
return false
|
||
}
|
||
if pr.Author() == username {
|
||
return false
|
||
}
|
||
if !containsString(ReviewerNames(pr), username) {
|
||
return false
|
||
}
|
||
return lastCommentAuthor(comments) != username
|
||
}
|
||
|
||
// ReviewerNames 回傳 PR 的 requested reviewers(去重、排序)。
|
||
func ReviewerNames(pr *PullRequest) []string {
|
||
seen := map[string]bool{}
|
||
names := make([]string, 0, len(pr.RequestedReviewers))
|
||
for i := range pr.RequestedReviewers {
|
||
name := pr.RequestedReviewers[i].Name()
|
||
if name != "" && !seen[name] {
|
||
seen[name] = true
|
||
names = append(names, name)
|
||
}
|
||
}
|
||
sort.Strings(names)
|
||
return names
|
||
}
|
||
|
||
// AssigneeNames 回傳 issue 的全部 assignees(assignee 欄位+assignees 陣列,
|
||
// 去重、排序),對應 gitea.py 的 _assignee_usernames。
|
||
func AssigneeNames(issue *Issue) []string {
|
||
seen := map[string]bool{}
|
||
add := func(u *User) {
|
||
if u == nil {
|
||
return
|
||
}
|
||
if name := u.Name(); name != "" && !seen[name] {
|
||
seen[name] = true
|
||
}
|
||
}
|
||
add(issue.Assignee)
|
||
for i := range issue.Assignees {
|
||
add(&issue.Assignees[i])
|
||
}
|
||
names := make([]string, 0, len(seen))
|
||
for name := range seen {
|
||
names = append(names, name)
|
||
}
|
||
sort.Strings(names)
|
||
return names
|
||
}
|
||
|
||
// AssigneeOverlaps 判定 issue 是否分派給 members 中至少一人
|
||
// (members 為空 → false,同 gitea.py filter_issues_by_assignee 的空成員集行為)。
|
||
func AssigneeOverlaps(issue *Issue, members []string) bool {
|
||
if len(members) == 0 {
|
||
return false
|
||
}
|
||
assignees := AssigneeNames(issue)
|
||
for _, member := range members {
|
||
if member != "" && containsString(assignees, member) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// LastCommentNotByAssignee 判定 issue 的最後一則留言是否不是任何 assignee
|
||
// 留的(無留言 → true,視為需要處理)。
|
||
// 注意:判定對象是該 issue 的**全部** assignees,不只是 members 中的成員
|
||
// (gitea.py _last_comment_is_from_assignee 的語義)。
|
||
func LastCommentNotByAssignee(issue *Issue, comments []Comment) bool {
|
||
last := lastCommentAuthor(comments)
|
||
if last == "" {
|
||
return true
|
||
}
|
||
return !containsString(AssigneeNames(issue), last)
|
||
}
|
||
|
||
// IssueNeedsWork 組合判定:分派給 members 中至少一人,且最後一則留言
|
||
// 不是 assignee(gitea.py filter_issues_by_assignee 保留的項目)。
|
||
func IssueNeedsWork(issue *Issue, comments []Comment, members []string) bool {
|
||
return AssigneeOverlaps(issue, members) && LastCommentNotByAssignee(issue, comments)
|
||
}
|