add github
This commit is contained in:
@@ -838,12 +838,22 @@ export const apiExam = {
|
|||||||
},
|
},
|
||||||
publishSubmissionsGit: async (id: number) => {
|
publishSubmissionsGit: async (id: number) => {
|
||||||
const res = await staffFetch(`/exam-rooms/${id}/submissions/publish-git`, { method: 'POST' });
|
const res = await staffFetch(`/exam-rooms/${id}/submissions/publish-git`, { method: 'POST' });
|
||||||
if (!res.ok) await parseError(res, 'Đẩy lên Git thất bại');
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({} as { error?: string; warnings?: string[] }));
|
||||||
|
const msg = errData.error || 'Đẩy lên Git thất bại';
|
||||||
|
if (errData.warnings?.length) throw new Error(`${msg} — ${errData.warnings.join('; ')}`);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
return res.json() as Promise<{ ok: boolean; url: string; gitPublishUrl: string; openUrl?: string; message: string }>;
|
return res.json() as Promise<{ ok: boolean; url: string; gitPublishUrl: string; openUrl?: string; message: string }>;
|
||||||
},
|
},
|
||||||
publishSubmissionsGitStudents: async (id: number) => {
|
publishSubmissionsGitStudents: async (id: number) => {
|
||||||
const res = await staffFetch(`/exam-rooms/${id}/submissions/publish-git-students`, { method: 'POST' });
|
const res = await staffFetch(`/exam-rooms/${id}/submissions/publish-git-students`, { method: 'POST' });
|
||||||
if (!res.ok) await parseError(res, 'Đẩy repo từng SV thất bại');
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({} as { error?: string; warnings?: string[] }));
|
||||||
|
const msg = errData.error || 'Đẩy repo từng SV thất bại';
|
||||||
|
if (errData.warnings?.length) throw new Error(`${msg} — ${errData.warnings.join('; ')}`);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
return res.json() as Promise<{
|
return res.json() as Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
published: number;
|
published: number;
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ func PublishExamSubmissionsGitHandler(db *gorm.DB) fiber.Handler {
|
|||||||
return c.Status(500).JSON(fiber.Map{"error": err.Error(), "warnings": unpackWarn})
|
return c.Status(500).JSON(fiber.Map{"error": err.Error(), "warnings": unpackWarn})
|
||||||
}
|
}
|
||||||
|
|
||||||
owner, repo, branch, repoHTML, err := resolvePublishRepo(token, ghLogin, &room)
|
_, repo, branch, repoHTML, err := resolvePublishRepo(db, staffID, token, ghLogin, &room)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -448,9 +448,10 @@ func PublishExamSubmissionsGitHandler(db *gorm.DB) fiber.Handler {
|
|||||||
room.GitRepoURL = repoHTML
|
room.GitRepoURL = repoHTML
|
||||||
room.GitBranch = branch
|
room.GitBranch = branch
|
||||||
}
|
}
|
||||||
|
pushOwner, pushRepo := resolveGitPushTarget(token, ghLogin, repo, repoHTML)
|
||||||
|
|
||||||
commitMsg := fmt.Sprintf("Simple Care: bài nộp phòng thi %s (#%d)", room.Name, roomID)
|
commitMsg := fmt.Sprintf("Simple Care: bài nộp phòng thi %s (#%d)", room.Name, roomID)
|
||||||
viewURL, pushWarn, err := githubPushFiles(token, owner, repo, branch, files, commitMsg)
|
viewURL, pushWarn, err := githubPushFiles(token, pushOwner, pushRepo, branch, files, commitMsg)
|
||||||
allWarn := append(unpackWarn, pushWarn...)
|
allWarn := append(unpackWarn, pushWarn...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(502).JSON(fiber.Map{
|
return c.Status(502).JSON(fiber.Map{
|
||||||
@@ -462,7 +463,7 @@ func PublishExamSubmissionsGitHandler(db *gorm.DB) fiber.Handler {
|
|||||||
room.GitPublishURL = viewURL
|
room.GitPublishURL = viewURL
|
||||||
_ = db.Save(&room).Error
|
_ = db.Save(&room).Error
|
||||||
roomIDCopy := roomID
|
roomIDCopy := roomID
|
||||||
recordStaffGitHubRepo(db, staffID, owner, repo, repoHTML, models.GitPublishModeRoom, room.Name, &roomIDCopy, nil)
|
recordStaffGitHubRepo(db, staffID, pushOwner, pushRepo, repoHTML, models.GitPublishModeRoom, room.Name, &roomIDCopy, nil)
|
||||||
|
|
||||||
msg := fmt.Sprintf("Đã đẩy %d bài lên GitHub — mỗi sinh viên một folder.", len(rows))
|
msg := fmt.Sprintf("Đã đẩy %d bài lên GitHub — mỗi sinh viên một folder.", len(rows))
|
||||||
if len(allWarn) > 0 {
|
if len(allWarn) > 0 {
|
||||||
@@ -517,7 +518,6 @@ func PublishExamSubmissionsGitPerStudentHandler(db *gorm.DB) fiber.Handler {
|
|||||||
return c.Status(400).JSON(fiber.Map{"error": "Chưa có bài nộp"})
|
return c.Status(400).JSON(fiber.Map{"error": "Chưa có bài nộp"})
|
||||||
}
|
}
|
||||||
|
|
||||||
branch := "main"
|
|
||||||
published := 0
|
published := 0
|
||||||
var allWarn []string
|
var allWarn []string
|
||||||
var repoURLs []string
|
var repoURLs []string
|
||||||
@@ -536,14 +536,19 @@ func PublishExamSubmissionsGitPerStudentHandler(db *gorm.DB) fiber.Handler {
|
|||||||
allWarn = append(allWarn, fmt.Sprintf("%s: không có file để đẩy", studentSubmissionFolder(row.StudentCode, row.FullName)))
|
allWarn = append(allWarn, fmt.Sprintf("%s: không có file để đẩy", studentSubmissionFolder(row.StudentCode, row.FullName)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
repoName, repoHTML, err := githubEnsureRepoForStudent(token, ghLogin, &room, &row)
|
repoName, repoHTML, err := githubEnsureRepoForStudent(db, staffID, token, ghLogin, &room, &row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
allWarn = append(allWarn, row.StudentCode+": "+err.Error())
|
allWarn = append(allWarn, row.StudentCode+": "+err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
pushOwner, pushRepo := resolveGitPushTarget(token, ghLogin, repoName, repoHTML)
|
||||||
|
branch := "main"
|
||||||
|
if meta, ok := githubFetchRepo(token, pushOwner, pushRepo); ok && meta.DefaultBranch != "" {
|
||||||
|
branch = meta.DefaultBranch
|
||||||
|
}
|
||||||
label := studentSubmissionFolder(row.StudentCode, row.FullName)
|
label := studentSubmissionFolder(row.StudentCode, row.FullName)
|
||||||
commitMsg := fmt.Sprintf("Simple Care: bài nộp %s — %s", label, room.Name)
|
commitMsg := fmt.Sprintf("Simple Care: bài nộp %s — %s", label, room.Name)
|
||||||
viewURL, pushWarn, err := githubPushFiles(token, ghLogin, repoName, branch, files, commitMsg)
|
viewURL, pushWarn, err := githubPushFiles(token, pushOwner, pushRepo, branch, files, commitMsg)
|
||||||
allWarn = append(allWarn, pushWarn...)
|
allWarn = append(allWarn, pushWarn...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
allWarn = append(allWarn, label+": "+err.Error())
|
allWarn = append(allWarn, label+": "+err.Error())
|
||||||
@@ -553,7 +558,7 @@ func PublishExamSubmissionsGitPerStudentHandler(db *gorm.DB) fiber.Handler {
|
|||||||
Updates(map[string]any{"git_repo_url": repoHTML, "git_publish_url": viewURL}).Error
|
Updates(map[string]any{"git_repo_url": repoHTML, "git_publish_url": viewURL}).Error
|
||||||
subID := row.ID
|
subID := row.ID
|
||||||
roomIDCopy := roomID
|
roomIDCopy := roomID
|
||||||
recordStaffGitHubRepo(db, staffID, ghLogin, repoName, repoHTML, models.GitPublishModeStudent, label, &roomIDCopy, &subID)
|
recordStaffGitHubRepo(db, staffID, pushOwner, pushRepo, repoHTML, models.GitPublishModeStudent, label, &roomIDCopy, &subID)
|
||||||
published++
|
published++
|
||||||
repoURLs = append(repoURLs, viewURL)
|
repoURLs = append(repoURLs, viewURL)
|
||||||
studentRepos = append(studentRepos, studentRepo{
|
studentRepos = append(studentRepos, studentRepo{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -65,8 +66,27 @@ type gitPathFile struct {
|
|||||||
content []byte
|
content []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func githubRepoAPI(owner, repo, suffix string) string {
|
||||||
|
return fmt.Sprintf("https://api.github.com/repos/%s/%s%s",
|
||||||
|
url.PathEscape(strings.TrimSpace(owner)),
|
||||||
|
url.PathEscape(strings.TrimSpace(repo)),
|
||||||
|
suffix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveGitPushTarget(token, fallbackOwner, repoName, repoHTML string) (owner, repo string) {
|
||||||
|
if o, r, err := parseGitHubRepo(repoHTML); err == nil && o != "" && r != "" {
|
||||||
|
return o, r
|
||||||
|
}
|
||||||
|
owner = strings.TrimSpace(fallbackOwner)
|
||||||
|
if live, err := githubGetUser(token); err == nil && live != "" {
|
||||||
|
owner = live
|
||||||
|
}
|
||||||
|
return owner, trimGitHubRepoName(repoName)
|
||||||
|
}
|
||||||
|
|
||||||
func githubCreateBlob(token, owner, repo string, content []byte) (string, error) {
|
func githubCreateBlob(token, owner, repo string, content []byte) (string, error) {
|
||||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/blobs", owner, repo)
|
apiURL := githubRepoAPI(owner, repo, "/git/blobs")
|
||||||
body := map[string]string{
|
body := map[string]string{
|
||||||
"content": base64.StdEncoding.EncodeToString(content),
|
"content": base64.StdEncoding.EncodeToString(content),
|
||||||
"encoding": "base64",
|
"encoding": "base64",
|
||||||
@@ -84,7 +104,7 @@ func githubCreateBlob(token, owner, repo string, content []byte) (string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[string]string) (string, error) {
|
func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[string]string) (string, error) {
|
||||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees", owner, repo)
|
apiURL := githubRepoAPI(owner, repo, "/git/trees")
|
||||||
body := map[string]any{"tree": entries}
|
body := map[string]any{"tree": entries}
|
||||||
if baseTreeSHA != "" {
|
if baseTreeSHA != "" {
|
||||||
body["base_tree"] = baseTreeSHA
|
body["base_tree"] = baseTreeSHA
|
||||||
@@ -102,7 +122,7 @@ func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string) (string, error) {
|
func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string) (string, error) {
|
||||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits", owner, repo)
|
apiURL := githubRepoAPI(owner, repo, "/git/commits")
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"message": message,
|
"message": message,
|
||||||
"tree": treeSHA,
|
"tree": treeSHA,
|
||||||
@@ -123,22 +143,30 @@ func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func githubUpdateBranchRef(token, owner, repo, branch, commitSHA string) error {
|
func githubUpdateBranchRef(token, owner, repo, branch, commitSHA string) error {
|
||||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/heads/%s", owner, repo, branch)
|
apiURL := githubRepoAPI(owner, repo, "/git/refs/heads/"+url.PathEscape(branch))
|
||||||
body := map[string]any{"sha": commitSHA, "force": false}
|
body := map[string]any{"sha": commitSHA, "force": false}
|
||||||
err := githubJSON(token, http.MethodPatch, apiURL, body, nil)
|
err := githubJSON(token, http.MethodPatch, apiURL, body, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
createURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs", owner, repo)
|
// Branch chưa tồn tại — tạo mới
|
||||||
|
createURL := githubRepoAPI(owner, repo, "/git/refs")
|
||||||
createBody := map[string]string{
|
createBody := map[string]string{
|
||||||
"ref": "refs/heads/" + branch,
|
"ref": "refs/heads/" + branch,
|
||||||
"sha": commitSHA,
|
"sha": commitSHA,
|
||||||
}
|
}
|
||||||
return githubJSON(token, http.MethodPost, createURL, createBody, nil)
|
if createErr := githubJSON(token, http.MethodPost, createURL, createBody, nil); createErr == nil {
|
||||||
|
return nil
|
||||||
|
} else if !strings.Contains(strings.ToLower(createErr.Error()), "already exists") {
|
||||||
|
return createErr
|
||||||
|
}
|
||||||
|
// Ref đã tồn tại nhưng PATCH thất bại (non-fast-forward) — cập nhật force
|
||||||
|
forceBody := map[string]any{"sha": commitSHA, "force": true}
|
||||||
|
return githubJSON(token, http.MethodPatch, apiURL, forceBody, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA string, err error) {
|
func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA string, err error) {
|
||||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/ref/heads/%s", owner, repo, branch)
|
apiURL := githubRepoAPI(owner, repo, "/git/ref/heads/"+url.PathEscape(branch))
|
||||||
var refResp struct {
|
var refResp struct {
|
||||||
Object struct {
|
Object struct {
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
@@ -151,7 +179,7 @@ func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA stri
|
|||||||
if commitSHA == "" {
|
if commitSHA == "" {
|
||||||
return "", "", errors.New("không lấy được commit hiện tại")
|
return "", "", errors.New("không lấy được commit hiện tại")
|
||||||
}
|
}
|
||||||
commitURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits/%s", owner, repo, commitSHA)
|
commitURL := githubRepoAPI(owner, repo, "/git/commits/"+url.PathEscape(commitSHA))
|
||||||
var commitResp struct {
|
var commitResp struct {
|
||||||
Tree struct {
|
Tree struct {
|
||||||
SHA string `json:"sha"`
|
SHA string `json:"sha"`
|
||||||
@@ -178,6 +206,7 @@ func githubJSON(token, method, apiURL string, body any, out any) error {
|
|||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Accept", "application/vnd.github+json")
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,29 +247,87 @@ func githubGetUser(token string) (login string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func githubGetRepo(token, owner, repoName string) (htmlURL string, found bool) {
|
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, _ := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Accept", "application/vnd.github+json")
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return meta, false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", false
|
return meta, false
|
||||||
}
|
}
|
||||||
var repo struct {
|
var repo struct {
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
|
DefaultBranch string `json:"default_branch"`
|
||||||
}
|
}
|
||||||
_ = json.NewDecoder(resp.Body).Decode(&repo)
|
_ = json.NewDecoder(resp.Body).Decode(&repo)
|
||||||
if repo.HTMLURL == "" {
|
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{
|
body := map[string]any{
|
||||||
"name": repoName,
|
"name": repoName,
|
||||||
"private": private,
|
"private": private,
|
||||||
@@ -279,31 +337,46 @@ func githubCreateRepo(token, owner, repoName string, private bool) (htmlURL stri
|
|||||||
payload, _ := json.Marshal(body)
|
payload, _ := json.Marshal(body)
|
||||||
req, err := http.NewRequest(http.MethodPost, "https://api.github.com/user/repos", bytes.NewReader(payload))
|
req, err := http.NewRequest(http.MethodPost, "https://api.github.com/user/repos", bytes.NewReader(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Accept", "application/vnd.github+json")
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
raw, _ := io.ReadAll(resp.Body)
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode >= 300 {
|
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 {
|
var created struct {
|
||||||
HTMLURL string `json:"html_url"`
|
HTMLURL string `json:"html_url"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &created); err != nil {
|
if err := json.Unmarshal(raw, &created); err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if created.HTMLURL == "" {
|
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 {
|
func isGitHubRepoNameTaken(err error) bool {
|
||||||
@@ -316,6 +389,20 @@ func isGitHubRepoNameTaken(err error) bool {
|
|||||||
|
|
||||||
func trimGitHubRepoName(name string) string {
|
func trimGitHubRepoName(name string) string {
|
||||||
name = strings.ToLower(strings.TrimSpace(name))
|
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, "-.")
|
name = strings.Trim(name, "-.")
|
||||||
if len(name) > 90 {
|
if len(name) > 90 {
|
||||||
name = strings.Trim(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.
|
// 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 != "" {
|
if u := strings.TrimSpace(room.GitRepoURL); u != "" {
|
||||||
o, r, parseErr := parseGitHubRepo(u)
|
o, r, parseErr := parseGitHubRepo(u)
|
||||||
if parseErr == nil && strings.EqualFold(o, owner) {
|
if parseErr == nil && strings.EqualFold(o, owner) {
|
||||||
if url, ok := githubGetRepo(token, owner, r); ok {
|
if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
|
||||||
return r, url, nil
|
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) {
|
for _, candidate := range examGitRepoNameCandidates(room) {
|
||||||
if url, ok := githubGetRepo(token, owner, candidate); ok {
|
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 repoNameMatchesSavedURL(room.GitRepoURL, owner, candidate) ||
|
||||||
if u := strings.TrimSpace(room.GitRepoURL); u != "" {
|
githubRepoTrackedForRoom(db, staffID, owner, candidate, room.ID) {
|
||||||
if o, r, parseErr := parseGitHubRepo(u); parseErr == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, candidate) {
|
return candidate, url, nil
|
||||||
return candidate, url, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
url, createErr := githubCreateRepo(token, owner, candidate, true)
|
actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||||
if createErr == nil {
|
if createErr == nil {
|
||||||
return candidate, url, nil
|
return actualName, url, nil
|
||||||
}
|
}
|
||||||
if isGitHubRepoNameTaken(createErr) {
|
if isGitHubRepoNameTaken(createErr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return "", "", createErr
|
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 {
|
func examGitRepoName(room *models.ExamRoom) string {
|
||||||
@@ -406,13 +515,16 @@ func examGitRepoName(room *models.ExamRoom) string {
|
|||||||
return examGitRepoBaseName(room)
|
return examGitRepoBaseName(room)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePublishRepo(token, ghLogin string, room *models.ExamRoom) (owner, repo, branch, repoHTML string, err error) {
|
func resolvePublishRepo(db *gorm.DB, staffID uint, token, ghLogin string, room *models.ExamRoom) (owner, repo, branch, repoHTML string, err error) {
|
||||||
branch = "main"
|
|
||||||
owner = ghLogin
|
owner = ghLogin
|
||||||
repo, htmlURL, err := githubEnsureRepoForRoom(token, owner, room)
|
repo, htmlURL, err := githubEnsureRepoForRoom(db, staffID, token, owner, room)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", err
|
return "", "", "", "", err
|
||||||
}
|
}
|
||||||
|
branch = "main"
|
||||||
|
if meta, ok := githubFetchRepo(token, owner, repo); ok && meta.DefaultBranch != "" {
|
||||||
|
branch = meta.DefaultBranch
|
||||||
|
}
|
||||||
return owner, repo, branch, htmlURL, nil
|
return owner, repo, branch, htmlURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,32 +556,56 @@ func studentGitRepoNameCandidates(row submissionRow, room *models.ExamRoom) []st
|
|||||||
return out
|
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 != "" {
|
if u := strings.TrimSpace(row.GitRepoURL); u != "" {
|
||||||
o, r, parseErr := parseGitHubRepo(u)
|
o, r, parseErr := parseGitHubRepo(u)
|
||||||
if parseErr == nil && strings.EqualFold(o, owner) {
|
if parseErr == nil && strings.EqualFold(o, owner) {
|
||||||
if url, ok := githubGetRepo(token, owner, r); ok {
|
if url, ok := githubGetRepo(token, owner, trimGitHubRepoName(r)); ok {
|
||||||
return r, url, nil
|
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) {
|
for _, candidate := range studentGitRepoNameCandidates(*row, room) {
|
||||||
if url, ok := githubGetRepo(token, owner, candidate); ok {
|
if url, ok := githubGetRepo(token, owner, candidate); ok {
|
||||||
if u := strings.TrimSpace(row.GitRepoURL); u != "" {
|
if repoNameMatchesSavedURL(row.GitRepoURL, owner, candidate) ||
|
||||||
if o, r, parseErr := parseGitHubRepo(u); parseErr == nil && strings.EqualFold(o, owner) && strings.EqualFold(r, candidate) {
|
githubRepoTrackedForSubmission(db, staffID, owner, candidate, row.ID) {
|
||||||
return candidate, url, nil
|
return candidate, url, nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
url, createErr := githubCreateRepo(token, owner, candidate, true)
|
actualName, url, createErr := githubCreateRepo(token, owner, candidate, true)
|
||||||
if createErr == nil {
|
if createErr == nil {
|
||||||
return candidate, url, nil
|
return actualName, url, nil
|
||||||
}
|
}
|
||||||
if isGitHubRepoNameTaken(createErr) {
|
if isGitHubRepoNameTaken(createErr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return "", "", createErr
|
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