Files
teai/internal/workflow/judge.go
chenyunda218 5583a00bb5 internal/workflow + cli:gitea.py 工作流命令 Go 化(#6)
- 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 為基底
2026-09-10 08:39:35 +08:00

138 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}