add github
This commit is contained in:
@@ -247,29 +247,87 @@ func githubGetUser(token string) (login string, err error) {
|
||||
}
|
||||
|
||||
func githubGetRepo(token, owner, repoName string) (htmlURL string, found bool) {
|
||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repoName)
|
||||
meta, ok := githubFetchRepo(token, owner, repoName)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return meta.HTMLURL, true
|
||||
}
|
||||
|
||||
func githubFetchRepo(token, owner, repoName string) (meta struct {
|
||||
HTMLURL string
|
||||
DefaultBranch string
|
||||
}, ok bool) {
|
||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repoName))
|
||||
req, _ := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", false
|
||||
return meta, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", false
|
||||
return meta, false
|
||||
}
|
||||
var repo struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&repo)
|
||||
if repo.HTMLURL == "" {
|
||||
return "", false
|
||||
return meta, false
|
||||
}
|
||||
return repo.HTMLURL, true
|
||||
meta.HTMLURL = repo.HTMLURL
|
||||
meta.DefaultBranch = strings.TrimSpace(repo.DefaultBranch)
|
||||
if meta.DefaultBranch == "" {
|
||||
meta.DefaultBranch = "main"
|
||||
}
|
||||
return meta, true
|
||||
}
|
||||
|
||||
func githubCreateRepo(token, owner, repoName string, private bool) (htmlURL string, err error) {
|
||||
func clearStaleExamRoomGitURLs(db *gorm.DB, room *models.ExamRoom) {
|
||||
room.GitRepoURL = ""
|
||||
room.GitPublishURL = ""
|
||||
_ = db.Model(room).Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
}
|
||||
|
||||
func clearStaleSubmissionGitURLs(db *gorm.DB, row *submissionRow) {
|
||||
row.GitRepoURL = ""
|
||||
row.GitPublishURL = ""
|
||||
_ = db.Model(&models.ExamSubmission{}).Where("id = ?", row.ID).
|
||||
Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
}
|
||||
|
||||
func githubRepoTrackedForRoom(db *gorm.DB, staffID uint, owner, repoName string, roomID uint) bool {
|
||||
var count int64
|
||||
_ = db.Model(&models.StaffGitHubRepo{}).
|
||||
Where("staff_id = ? AND owner_login = ? AND repo_name = ? AND exam_room_id = ? AND publish_mode = ?",
|
||||
staffID, owner, repoName, roomID, models.GitPublishModeRoom).
|
||||
Count(&count).Error
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func githubRepoTrackedForSubmission(db *gorm.DB, staffID uint, owner, repoName string, submissionID uint) bool {
|
||||
var count int64
|
||||
_ = db.Model(&models.StaffGitHubRepo{}).
|
||||
Where("staff_id = ? AND owner_login = ? AND repo_name = ? AND submission_id = ? AND publish_mode = ?",
|
||||
staffID, owner, repoName, submissionID, models.GitPublishModeStudent).
|
||||
Count(&count).Error
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func repoNameMatchesSavedURL(savedURL, owner, repoName string) bool {
|
||||
if strings.TrimSpace(savedURL) == "" {
|
||||
return false
|
||||
}
|
||||
o, r, err := parseGitHubRepo(savedURL)
|
||||
return err == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, repoName)
|
||||
}
|
||||
|
||||
func githubCreateRepo(token, owner, repoName string, private bool) (actualName, htmlURL string, err error) {
|
||||
repoName = trimGitHubRepoName(repoName)
|
||||
body := map[string]any{
|
||||
"name": repoName,
|
||||
"private": private,
|
||||
@@ -279,31 +337,46 @@ func githubCreateRepo(token, owner, repoName string, private bool) (htmlURL stri
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest(http.MethodPost, "https://api.github.com/user/repos", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("tạo repo thất bại: %s", strings.TrimSpace(string(raw)))
|
||||
return "", "", fmt.Errorf("tạo repo thất bại: %s", strings.TrimSpace(string(raw)))
|
||||
}
|
||||
var created struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &created); err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
if created.HTMLURL == "" {
|
||||
return "", errors.New("GitHub không trả URL repo")
|
||||
return "", "", errors.New("GitHub không trả URL repo")
|
||||
}
|
||||
return created.HTMLURL, nil
|
||||
actualName = strings.TrimSpace(created.Name)
|
||||
if actualName == "" {
|
||||
if _, r, perr := parseGitHubRepo(created.HTMLURL); perr == nil {
|
||||
actualName = r
|
||||
} else {
|
||||
actualName = repoName
|
||||
}
|
||||
}
|
||||
return actualName, created.HTMLURL, nil
|
||||
}
|
||||
|
||||
func githubOwnerRepoFromURL(htmlURL string) (owner, repo string, ok bool) {
|
||||
o, r, err := parseGitHubRepo(htmlURL)
|
||||
return o, r, err == nil
|
||||
}
|
||||
|
||||
func isGitHubRepoNameTaken(err error) bool {
|
||||
@@ -316,6 +389,20 @@ func isGitHubRepoNameTaken(err error) bool {
|
||||
|
||||
func trimGitHubRepoName(name string) string {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
name = strings.NewReplacer(" ", "-", "_", "-").Replace(name)
|
||||
var b strings.Builder
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-', r == '.':
|
||||
b.WriteRune(r)
|
||||
case r == '_':
|
||||
b.WriteRune('-')
|
||||
}
|
||||
}
|
||||
name = b.String()
|
||||
for strings.Contains(name, "--") {
|
||||
name = strings.ReplaceAll(name, "--", "-")
|
||||
}
|
||||
name = strings.Trim(name, "-.")
|
||||
if len(name) > 90 {
|
||||
name = strings.Trim(name[:90], "-.")
|
||||
@@ -365,36 +452,58 @@ func examGitRepoNameCandidates(room *models.ExamRoom) []string {
|
||||
}
|
||||
|
||||
// githubEnsureRepoForRoom — dùng repo đã gắn phòng thi, hoặc tạo mới; trùng tên thì tự đổi tên.
|
||||
func githubEnsureRepoForRoom(token, owner string, room *models.ExamRoom) (repoName, htmlURL string, err error) {
|
||||
func githubEnsureRepoForRoom(db *gorm.DB, staffID uint, token, owner string, room *models.ExamRoom) (repoName, htmlURL string, err error) {
|
||||
if u := strings.TrimSpace(room.GitRepoURL); u != "" {
|
||||
o, r, parseErr := parseGitHubRepo(u)
|
||||
if parseErr == nil && strings.EqualFold(o, owner) {
|
||||
if url, ok := githubGetRepo(token, owner, r); ok {
|
||||
return r, url, nil
|
||||
if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
|
||||
if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
|
||||
return name, url, nil
|
||||
}
|
||||
return trimGitHubRepoName(r), url, nil
|
||||
}
|
||||
clearStaleExamRoomGitURLs(db, room)
|
||||
}
|
||||
}
|
||||
|
||||
var tracked models.StaffGitHubRepo
|
||||
if err := db.Where("staff_id = ? AND exam_room_id = ? AND publish_mode = ?", staffID, room.ID, models.GitPublishModeRoom).
|
||||
Order("created_at desc").First(&tracked).Error; err == nil {
|
||||
if strings.EqualFold(tracked.OwnerLogin, owner) {
|
||||
trackedName := trimGitHubRepoName(tracked.RepoName)
|
||||
if url, ok := githubGetRepo(token, owner, trackedName); ok {
|
||||
if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
|
||||
return name, url, nil
|
||||
}
|
||||
return trackedName, url, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range examGitRepoNameCandidates(room) {
|
||||
if url, ok := githubGetRepo(token, owner, candidate); ok {
|
||||
// Repo đã tồn tại — chỉ dùng lại nếu đúng phòng thi này đã lưu URL
|
||||
if u := strings.TrimSpace(room.GitRepoURL); u != "" {
|
||||
if o, r, parseErr := parseGitHubRepo(u); parseErr == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, candidate) {
|
||||
return candidate, url, nil
|
||||
}
|
||||
if repoNameMatchesSavedURL(room.GitRepoURL, owner, candidate) ||
|
||||
githubRepoTrackedForRoom(db, staffID, owner, candidate, room.ID) {
|
||||
return candidate, url, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||
actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||
if createErr == nil {
|
||||
return candidate, url, nil
|
||||
return actualName, url, nil
|
||||
}
|
||||
if isGitHubRepoNameTaken(createErr) {
|
||||
continue
|
||||
}
|
||||
return "", "", createErr
|
||||
}
|
||||
return "", "", errors.New("không tạo được repo GitHub — đã thử nhiều tên khác nhau")
|
||||
|
||||
fallback := trimGitHubRepoName(fmt.Sprintf("%s-sc-%d-%d", examGitRepoBaseName(room), room.ID, time.Now().Unix()))
|
||||
actualName, url, createErr := githubCreateRepo(token, owner, fallback, true)
|
||||
if createErr != nil {
|
||||
return "", "", fmt.Errorf("không tạo được repo GitHub — đã thử nhiều tên khác nhau: %w", createErr)
|
||||
}
|
||||
return actualName, url, nil
|
||||
}
|
||||
|
||||
func examGitRepoName(room *models.ExamRoom) string {
|
||||
@@ -406,13 +515,16 @@ func examGitRepoName(room *models.ExamRoom) string {
|
||||
return examGitRepoBaseName(room)
|
||||
}
|
||||
|
||||
func resolvePublishRepo(token, ghLogin string, room *models.ExamRoom) (owner, repo, branch, repoHTML string, err error) {
|
||||
branch = "main"
|
||||
func resolvePublishRepo(db *gorm.DB, staffID uint, token, ghLogin string, room *models.ExamRoom) (owner, repo, branch, repoHTML string, err error) {
|
||||
owner = ghLogin
|
||||
repo, htmlURL, err := githubEnsureRepoForRoom(token, owner, room)
|
||||
repo, htmlURL, err := githubEnsureRepoForRoom(db, staffID, token, owner, room)
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
branch = "main"
|
||||
if meta, ok := githubFetchRepo(token, owner, repo); ok && meta.DefaultBranch != "" {
|
||||
branch = meta.DefaultBranch
|
||||
}
|
||||
return owner, repo, branch, htmlURL, nil
|
||||
}
|
||||
|
||||
@@ -444,32 +556,56 @@ func studentGitRepoNameCandidates(row submissionRow, room *models.ExamRoom) []st
|
||||
return out
|
||||
}
|
||||
|
||||
func githubEnsureRepoForStudent(token, owner string, room *models.ExamRoom, row *submissionRow) (repoName, htmlURL string, err error) {
|
||||
func githubEnsureRepoForStudent(db *gorm.DB, staffID uint, token, owner string, room *models.ExamRoom, row *submissionRow) (repoName, htmlURL string, err error) {
|
||||
if u := strings.TrimSpace(row.GitRepoURL); u != "" {
|
||||
o, r, parseErr := parseGitHubRepo(u)
|
||||
if parseErr == nil && strings.EqualFold(o, owner) {
|
||||
if url, ok := githubGetRepo(token, owner, r); ok {
|
||||
return r, url, nil
|
||||
if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
|
||||
if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
|
||||
return name, url, nil
|
||||
}
|
||||
return trimGitHubRepoName(r), url, nil
|
||||
}
|
||||
clearStaleSubmissionGitURLs(db, row)
|
||||
}
|
||||
}
|
||||
|
||||
var tracked models.StaffGitHubRepo
|
||||
if err := db.Where("staff_id = ? AND submission_id = ? AND publish_mode = ?", staffID, row.ID, models.GitPublishModeStudent).
|
||||
Order("created_at desc").First(&tracked).Error; err == nil {
|
||||
if strings.EqualFold(tracked.OwnerLogin, owner) {
|
||||
trackedName := trimGitHubRepoName(tracked.RepoName)
|
||||
if url, ok := githubGetRepo(token, owner, trackedName); ok {
|
||||
if _, name, perr := parseGitHubRepo(url); perr == nil && name != "" {
|
||||
return name, url, nil
|
||||
}
|
||||
return trackedName, url, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range studentGitRepoNameCandidates(*row, room) {
|
||||
if url, ok := githubGetRepo(token, owner, candidate); ok {
|
||||
if u := strings.TrimSpace(row.GitRepoURL); u != "" {
|
||||
if o, r, parseErr := parseGitHubRepo(u); parseErr == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, candidate) {
|
||||
return candidate, url, nil
|
||||
}
|
||||
if repoNameMatchesSavedURL(row.GitRepoURL, owner, candidate) ||
|
||||
githubRepoTrackedForSubmission(db, staffID, owner, candidate, row.ID) {
|
||||
return candidate, url, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||
actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||
if createErr == nil {
|
||||
return candidate, url, nil
|
||||
return actualName, url, nil
|
||||
}
|
||||
if isGitHubRepoNameTaken(createErr) {
|
||||
continue
|
||||
}
|
||||
return "", "", createErr
|
||||
}
|
||||
return "", "", errors.New("không tạo được repo GitHub cho sinh viên")
|
||||
|
||||
fallback := trimGitHubRepoName(fmt.Sprintf("%s-sc-%d-%d", studentSubmissionFolder(row.StudentCode, row.FullName), row.StudentRkID, time.Now().Unix()))
|
||||
actualName, url, createErr := githubCreateRepo(token, owner, fallback, true)
|
||||
if createErr != nil {
|
||||
return "", "", fmt.Errorf("không tạo được repo GitHub cho sinh viên: %w", createErr)
|
||||
}
|
||||
return actualName, url, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user