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  }