github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/assignments/pull_requests.go (about) 1 package assignments 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/quickfeed/quickfeed/database" 8 "github.com/quickfeed/quickfeed/qf" 9 "github.com/quickfeed/quickfeed/scm" 10 ) 11 12 // countMap maps a (courseID/groupID, userID)-pair to the number reviews 13 // the user has been assigned for the given course/group. 14 type countMap map[uint64]map[uint64]int 15 16 // Creates a new map if none exists for the given course/group id. 17 func (m countMap) initialize(id uint64) { 18 if _, ok := m[id]; !ok { 19 m[id] = make(map[uint64]int) // [id][userID] -> count 20 } 21 } 22 23 var ( 24 teacherReviewCounter = make(countMap) // [courseID][userID] -> count 25 groupReviewCounter = make(countMap) // [groupID][userID] -> count 26 ) 27 28 // AssignReviewers assigns reviewers to a group repository pull request. 29 // It assigns one other group member and one course teacher as reviewers. 30 func AssignReviewers(ctx context.Context, sc scm.SCM, db database.Database, course *qf.Course, repo *qf.Repository, pullRequest *qf.PullRequest) error { 31 teacherReviewer, err := getNextTeacherReviewer(db, course) 32 if err != nil { 33 return err 34 } 35 studentReviewer, err := getNextStudentReviewer(db, repo.GetGroupID(), pullRequest.GetUserID()) 36 if err != nil { 37 return err 38 } 39 40 opt := &scm.RequestReviewersOptions{ 41 Organization: course.GetScmOrganizationName(), 42 Repository: repo.Name(), 43 Number: int(pullRequest.GetNumber()), 44 Reviewers: []string{ 45 teacherReviewer.GetLogin(), 46 studentReviewer.GetLogin(), 47 }, 48 } 49 if err := sc.RequestReviewers(ctx, opt); err != nil { 50 return err 51 } 52 // Change pull request stage to review 53 pullRequest.SetReview() 54 return db.UpdatePullRequest(pullRequest) 55 } 56 57 // getNextReviewer gets the next reviewer from either teacherReviewCounter or studentReviewCounter, 58 // based on whoever in total has been assigned to the least amount of pull requests. 59 // It is simple, and does not account for how many current review requests any user has. 60 func getNextReviewer(users []*qf.User, reviewCounter map[uint64]int) *qf.User { 61 userWithLowestCount := users[0] 62 lowestCount := reviewCounter[userWithLowestCount.GetID()] 63 for _, user := range users { 64 count, ok := reviewCounter[user.GetID()] 65 if !ok { 66 // Found user with no prior reviews; assign as the next reviewer. 67 reviewCounter[user.GetID()] = 1 68 return user 69 } 70 if count < lowestCount { 71 userWithLowestCount = user 72 lowestCount = count 73 } 74 } 75 reviewCounter[userWithLowestCount.GetID()]++ 76 return userWithLowestCount 77 } 78 79 // getNextTeacherReviewer gets the teacher with the least total reviews. 80 func getNextTeacherReviewer(db database.Database, course *qf.Course) (*qf.User, error) { 81 teachers, err := db.GetCourseTeachers(course) 82 if err != nil { 83 return nil, fmt.Errorf("failed to get teachers from database: %w", err) 84 } 85 teacherReviewCounter.initialize(course.GetID()) 86 teacherReviewer := getNextReviewer(teachers, teacherReviewCounter[course.GetID()]) 87 return teacherReviewer, nil 88 } 89 90 // getNextStudentReviewer gets the student in a group with the least total reviews. 91 func getNextStudentReviewer(db database.Database, groupID, ownerID uint64) (*qf.User, error) { 92 group, err := db.GetGroup(groupID) 93 if err != nil { 94 return nil, fmt.Errorf("failed to get group from database: %w", err) 95 } 96 groupReviewCounter.initialize(group.GetID()) 97 // We exclude the PR owner from the search. 98 studentReviewer := getNextReviewer(group.GetUsersExcept(ownerID), groupReviewCounter[group.GetID()]) 99 return studentReviewer, nil 100 }