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) }