diff --git a/.gitmodules b/.gitmodules index 9261291..517e6cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,6 +38,34 @@ path = examples/keyword/clangtidy/sillycode url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git branch = keyword/clangtidy/sillycode +[submodule "examples/healthcheck/asciifile"] + path = examples/healthcheck/asciifile + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/asciifile +[submodule "examples/healthcheck/asciimsg"] + path = examples/healthcheck/asciimsg + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/asciimsg +[submodule "examples/healthcheck/forbiddenfile"] + path = examples/healthcheck/forbiddenfile + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/forbiddenfile +[submodule "examples/healthcheck/meta"] + path = examples/healthcheck/meta + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/meta +[submodule "examples/healthcheck/release"] + path = examples/healthcheck/release + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/release +[submodule "examples/healthcheck/reposize"] + path = examples/healthcheck/reposize + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/reposize +[submodule "examples/healthcheck/repoverify"] + path = examples/healthcheck/repoverify + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = healthcheck/repoverify [submodule "examples/cppcheck/sillycode"] path = examples/cppcheck/sillycode url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git diff --git a/README.md b/README.md index 7416adf..7f5c3d3 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,7 @@ Check the `Result` at . - `Score int`: score of the stage. - `Comment string`: comment on the stage. + +### HealthCheck + +The repohealth check will return a json list to for check result. The structure of json file is in `pkg/healthcheck/util.go` diff --git a/cmd/healthcheck/main.go b/cmd/healthcheck/main.go new file mode 100644 index 0000000..a575046 --- /dev/null +++ b/cmd/healthcheck/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/pkg/healthcheck" +) + +// parseMultiValueFlag parses a multi-value command-line flag and appends its values to the provided slice. +// It registers a flag with the specified name and description, associating it with a multiStringValue receiver. +func parseMultiValueFlag(values *[]string, flagName, description string) { + flag.Var((*multiStringValue)(values), flagName, description) +} + +type multiStringValue []string + +// Set appends a new value to the multiStringValue slice. +// It satisfies the flag.Value interface, allowing multiStringValue to be used as a flag value. +func (m *multiStringValue) Set(value string) error { + *m = append(*m, value) + return nil +} + +func (m *multiStringValue) String() string { + return fmt.Sprintf("%v", *m) +} + +// Generally, err is used for runtime errors, and checkRes is used for the result of the checks. +func main() { + var info []healthcheck.CheckStage + var gitWhitelist, metaFile, releaseTags []string + var tmp healthcheck.CheckStage + + rootDir := flag.String("root", "", "") + repo := flag.String("repo", "", "") + droneBranch := flag.String("droneBranch", "", "") + releaseCategories := flag.String("releaseCategories", "", "") + releaseNumber := flag.Int("releaseNumber", 0, "") + // FIXME: for drone usage + adminDir := flag.String("admin", "", "") // adminDir is for config files + parseMultiValueFlag(&gitWhitelist, "whitelist", "") + parseMultiValueFlag(&metaFile, "meta", "") + parseMultiValueFlag(&releaseTags, "releaseTags", "") + flag.Parse() + + tmp = healthcheck.RepoSize() + info = append(info, tmp) + + tmp = healthcheck.ForbiddenCheck(*rootDir, gitWhitelist, *repo, *droneBranch) + info = append(info, tmp) + + tmp = healthcheck.MetaCheck(*rootDir, metaFile) + info = append(info, tmp) + + tmp = healthcheck.NonAsciiFiles(*rootDir) + info = append(info, tmp) + + tmp = healthcheck.NonAsciiMsg(*rootDir) + info = append(info, tmp) + + // TODO: find a way to test the release tag + tmp = healthcheck.CheckReleases(*rootDir, *releaseCategories, *releaseNumber) + info = append(info, tmp) + + // FIXME: for drone usage + tmp = healthcheck.Verify(*rootDir, *adminDir) + info = append(info, tmp) + + jsonData, err := json.Marshal(info) + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } + fmt.Printf("%s", jsonData) +} diff --git a/cmd/joj3/main_test.go b/cmd/joj3/main_test.go index 0f35e6b..6daad37 100644 --- a/cmd/joj3/main_test.go +++ b/cmd/joj3/main_test.go @@ -14,6 +14,18 @@ import ( func compareStageResults(t *testing.T, actual, expected []stage.StageResult, regex bool) { t.Helper() + + // For Test + fmt.Println("Actual:") + for _, result := range actual { + fmt.Println(result) + } + + fmt.Println("Expected:") + for _, result := range expected { + fmt.Println(result) + } + if len(actual) != len(expected) { t.Fatalf("len(actual) = %d, expected %d", len(actual), len(expected)) } diff --git a/examples/healthcheck/asciifile b/examples/healthcheck/asciifile new file mode 160000 index 0000000..46804af --- /dev/null +++ b/examples/healthcheck/asciifile @@ -0,0 +1 @@ +Subproject commit 46804afd2ebe8787a9d43711b191e424134ce25b diff --git a/examples/healthcheck/asciimsg b/examples/healthcheck/asciimsg new file mode 160000 index 0000000..e9ed6a4 --- /dev/null +++ b/examples/healthcheck/asciimsg @@ -0,0 +1 @@ +Subproject commit e9ed6a464a507730ef956bb8ea6dbe84fffea7ad diff --git a/examples/healthcheck/forbiddenfile b/examples/healthcheck/forbiddenfile new file mode 160000 index 0000000..245d036 --- /dev/null +++ b/examples/healthcheck/forbiddenfile @@ -0,0 +1 @@ +Subproject commit 245d036af0cbfe3745ef303d239a2d7225067d0b diff --git a/examples/healthcheck/meta b/examples/healthcheck/meta new file mode 160000 index 0000000..4f5e444 --- /dev/null +++ b/examples/healthcheck/meta @@ -0,0 +1 @@ +Subproject commit 4f5e444940d2c383a0b069405e2ef42b01878bc5 diff --git a/examples/healthcheck/release b/examples/healthcheck/release new file mode 160000 index 0000000..a4cedea --- /dev/null +++ b/examples/healthcheck/release @@ -0,0 +1 @@ +Subproject commit a4cedea002a198c2dae373f7ee6f6dc67753fae6 diff --git a/examples/healthcheck/reposize b/examples/healthcheck/reposize new file mode 160000 index 0000000..d78308b --- /dev/null +++ b/examples/healthcheck/reposize @@ -0,0 +1 @@ +Subproject commit d78308bcaaaeda9ead86069652288ae911503e33 diff --git a/examples/healthcheck/repoverify b/examples/healthcheck/repoverify new file mode 160000 index 0000000..a7a3bd0 --- /dev/null +++ b/examples/healthcheck/repoverify @@ -0,0 +1 @@ +Subproject commit a7a3bd0894c2e727e3ab3f9ddda3d438fbb86b30 diff --git a/internal/parsers/all.go b/internal/parsers/all.go index f1404a7..8e4e29a 100644 --- a/internal/parsers/all.go +++ b/internal/parsers/all.go @@ -5,6 +5,7 @@ import ( _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/cppcheck" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/cpplint" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/diff" + _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/healthcheck" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/keyword" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/resultstatus" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/sample" diff --git a/internal/parsers/healthcheck/meta.go b/internal/parsers/healthcheck/meta.go new file mode 100644 index 0000000..d6c89ae --- /dev/null +++ b/internal/parsers/healthcheck/meta.go @@ -0,0 +1,9 @@ +package healthcheck + +import "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" + +var name = "healthcheck" + +func init() { + stage.RegisterParser(name, &Healthcheck{}) +} diff --git a/internal/parsers/healthcheck/parser.go b/internal/parsers/healthcheck/parser.go new file mode 100644 index 0000000..046ff18 --- /dev/null +++ b/internal/parsers/healthcheck/parser.go @@ -0,0 +1,49 @@ +package healthcheck + +import ( + // "encoding/json" + "fmt" + + "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" + // "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/pkg/healthcheck" + "github.com/criyle/go-judge/envexec" +) + +type Conf struct { + Score int + Comment string +} + +type Healthcheck struct{} + +func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult { + stdout := executorResult.Files["stdout"] + stderr := executorResult.Files["stderr"] + if executorResult.Status != stage.Status(envexec.StatusAccepted) { + return stage.ParserResult{ + Score: 0, + Comment: fmt.Sprintf( + "Unexpected executor status: %s.\nStderr: %s", + executorResult.Status, stderr, + ), + } + } + return stage.ParserResult{ + Score: 0, + Comment: stdout, + } +} + +func (*Healthcheck) Run(results []stage.ExecutorResult, confAny any) ( + []stage.ParserResult, bool, error, +) { + conf, err := stage.DecodeConf[Conf](confAny) + if err != nil { + return nil, true, err + } + var res []stage.ParserResult + for _, result := range results { + res = append(res, Parse(result, *conf)) + } + return res, false, nil +} diff --git a/internal/parsers/keyword/parser.go b/internal/parsers/keyword/parser.go index 9671a78..d376977 100644 --- a/internal/parsers/keyword/parser.go +++ b/internal/parsers/keyword/parser.go @@ -57,13 +57,13 @@ func (*Keyword) Run(results []stage.ExecutorResult, confAny any) ( return nil, true, err } var res []stage.ParserResult - forceQuit := false + end := false for _, result := range results { tmp, matched := Parse(result, *conf) if matched && conf.EndOnMatch { - forceQuit = true + end = true } res = append(res, tmp) } - return res, forceQuit, nil + return res, end, nil } diff --git a/internal/parsers/resultstatus/parser.go b/internal/parsers/resultstatus/parser.go index 3b1aad3..b023a76 100644 --- a/internal/parsers/resultstatus/parser.go +++ b/internal/parsers/resultstatus/parser.go @@ -19,12 +19,12 @@ func (*ResultStatus) Run(results []stage.ExecutorResult, confAny any) ( if err != nil { return nil, true, err } - forceQuit := false + end := false var res []stage.ParserResult for _, result := range results { comment := "" if result.Status != stage.Status(envexec.StatusAccepted) { - forceQuit = true + end = true comment = fmt.Sprintf( "Unexpected executor status: %s.", result.Status, ) @@ -34,5 +34,5 @@ func (*ResultStatus) Run(results []stage.ExecutorResult, confAny any) ( Comment: comment, }) } - return res, forceQuit, nil + return res, end, nil } diff --git a/internal/stage/model.go b/internal/stage/model.go index cb33462..1b052d8 100644 --- a/internal/stage/model.go +++ b/internal/stage/model.go @@ -164,7 +164,6 @@ type ParserResult struct { } type StageResult struct { - Name string `json:"name"` - Results []ParserResult `json:"results"` - ForceQuit bool `json:"force_quit"` + Name string `json:"name"` + Results []ParserResult `json:"results"` } diff --git a/internal/stage/run.go b/internal/stage/run.go index 670f666..6c74758 100644 --- a/internal/stage/run.go +++ b/internal/stage/run.go @@ -26,18 +26,17 @@ func Run(stages []Stage) []StageResult { slog.Error("parser not found", "name", stage.ParserName) break } - parserResults, forceQuit, err := parser.Run(executorResults, stage.ParserConf) + parserResults, end, err := parser.Run(executorResults, stage.ParserConf) if err != nil { slog.Error("parser run error", "name", stage.ExecutorName, "error", err) break } slog.Debug("parser run done", "results", parserResults) stageResults = append(stageResults, StageResult{ - Name: stage.Name, - Results: parserResults, - ForceQuit: forceQuit, + Name: stage.Name, + Results: parserResults, }) - if forceQuit { + if end { break } } diff --git a/pkg/healthcheck/commit.go b/pkg/healthcheck/commit.go new file mode 100644 index 0000000..7c94fb2 --- /dev/null +++ b/pkg/healthcheck/commit.go @@ -0,0 +1,83 @@ +package healthcheck + +import ( + "fmt" + "strings" + "unicode" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" +) + +// nonAsciiMsg checks for non-ASCII characters in the commit message. +// If the message starts with "Merge pull request", it skips the non-ASCII characters check. +// Otherwise, it iterates over each character in the message and checks if it is a non-ASCII character. +// If a non-ASCII character is found, it returns an error indicating not to use non-ASCII characters in commit messages. +// Otherwise, it returns nil indicating that the commit message is valid. +func NonAsciiMsg(root string) (jsonOut CheckStage) { + jsonOut = CheckStage{ + Name: "NonAsciiMsg", + StdOut: "Checking for non-ASCII characters in commit message: ", + ExitCode: 0, + StdErr: "", + } + + // cmd := exec.Command("git", "log", "--encoding=UTF-8", "--format=%B") + repo, err := git.PlainOpen(root) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error openning git repo%w", err) + return jsonOut + } + + ref, err := repo.Head() + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error getting reference%w", err) + return jsonOut + } + commits, err := repo.Log(&git.LogOptions{From: ref.Hash()}) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error getting commits%w", err) + return jsonOut + } + + var msgs []string + err = commits.ForEach(func(c *object.Commit) error { + msgs = append(msgs, c.Message) + return nil + }) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error converting log to string%w", err) + return jsonOut + } + + var nonAsciiMsgs []string + for _, msg := range msgs { + if msg == "" { + continue + } + if strings.HasPrefix(msg, "Merge pull request") { + continue + } + for _, c := range msg { + if c > unicode.MaxASCII { + nonAsciiMsgs = append(nonAsciiMsgs, msg) + } + } + } + if len(nonAsciiMsgs) > 0 { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 105 + jsonOut.StdErr = "Non-ASCII characters in commit messages:\n" + strings.Join(nonAsciiMsgs, "\n") + return jsonOut + } + jsonOut.StdOut += "OK" + return jsonOut +} diff --git a/pkg/healthcheck/forbidden.go b/pkg/healthcheck/forbidden.go new file mode 100644 index 0000000..af98747 --- /dev/null +++ b/pkg/healthcheck/forbidden.go @@ -0,0 +1,94 @@ +package healthcheck + +import ( + "fmt" + "os" + "path/filepath" + "regexp" +) + +// getForbiddens retrieves a list of forbidden files in the specified root directory. +// It searches for files that do not match the specified regex patterns in the given file list. +func getForbiddens(root string, fileList []string) ([]string, error) { + var matches []string + + var regexList []*regexp.Regexp + regexList, err := getRegex(fileList) + if err != nil { + fmt.Println("Error compiling regex:", err) + return nil, err + } + + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if info.Name() == ".git" || info.Name() == ".gitea" || info.Name() == "ci" { + return filepath.SkipDir + } else { + return nil + } + } + + match := false + for _, regex := range regexList { + if regex.MatchString(info.Name()) { + match = true + break + } + } + + if !match { + matches = append(matches, path) + } + return nil + }) + + return matches, err +} + +// forbiddenCheck checks for forbidden files in the specified root directory. +// It prints the list of forbidden files found, along with instructions on how to fix them. +func ForbiddenCheck(rootDir string, regexList []string, repo string, droneBranch string) (jsonOut CheckStage) { + jsonOut = CheckStage{ + Name: "forbiddenFile", + StdOut: "Checking forbidden files: ", + ExitCode: 0, + StdErr: "", + } + + // INFO: test case + // regexList := []string{`\.$`, `\.git`, `\.drone.yml`, `Makefile`, `CMakeLists.txt`,`.*\.go`,`.*\.toml`, `.*\.c`, `.*\.cc`, `.*\.cpp`, `.*\.h`, `.*\.md`} + // rootDir = "/Users/zhouzhaojiacheng/Desktop/STUDENT_ORG/TechJI/Dev/joj/JOJ3" + + forbids, err := getForbiddens(rootDir, regexList) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = err + return jsonOut + } + + var message string + + if len(forbids) > 0 { + message += fmt.Sprint(103, "the following forbidden files were found: ") + for _, file := range forbids { + message += fmt.Sprint(file, ", ") + } + message += "\n\nTo fix it, first make a backup of your repository and then run the following commands:\nfor i in " + for _, file := range forbids { + message += fmt.Sprint(file, " ") + } + message += fmt.Sprint("; do git filter-repo --force --invert-paths --path \\\"\\$i\\\"; done\ngit remote add origin ", repo, "\ngit push --set-upstream origin ", droneBranch, " --force") + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 103 + jsonOut.StdErr = message + return jsonOut + } else { + jsonOut.StdOut += "OK" + return jsonOut + } +} diff --git a/pkg/healthcheck/main.go b/pkg/healthcheck/main.go deleted file mode 100644 index 9b4e3b7..0000000 --- a/pkg/healthcheck/main.go +++ /dev/null @@ -1 +0,0 @@ -package healthcheck diff --git a/pkg/healthcheck/meta.go b/pkg/healthcheck/meta.go new file mode 100644 index 0000000..e4bcfce --- /dev/null +++ b/pkg/healthcheck/meta.go @@ -0,0 +1,86 @@ +package healthcheck + +import ( + "fmt" + "os" +) + +// getMetas retrieves a list of metadata files that are expected to exist in the specified root directory. +// It checks for the existence of each file in the fileList and provides instructions if any file is missing. +func getMetas(rootDir string, fileList []string) ([]string, string, error) { + addExt(fileList, "\\.*") + regexList, err := getRegex(fileList) + var unmatchedList []string + + if err != nil { + return nil, "", err + } + + files, err := os.ReadDir(rootDir) + if err != nil { + return nil, "", fmt.Errorf("Error reading directory:%w", err) + } + + matched := false + umatchedRes := "" + + // TODO: it seems that there is no good find subsitution now + // modify current code if exist a better solution + for i, regex := range regexList { + for _, file := range files { + if file.IsDir() { + continue + } + + if regex.MatchString(file.Name()) { + matched = true + break + } + } + if !matched { + unmatchedList = append(unmatchedList, fileList[i]) + str := fmt.Sprint("\tno ", fileList[i], " file found") + switch fileList[i] { + case "readme\\.*": + str += ", please refer to https://www.makeareadme.com/ for more information" + case "changelog\\.*": + str += ", please refer to https://keepachangelog.com/en/1.1.0/ for more information" + default: + str += "" + } + str += "\n" + + umatchedRes += str + } + } + + return unmatchedList, umatchedRes, nil +} + +// metaCheck performs a check for metadata files in the specified root directory. +// It prints a message if any required metadata files are missing. +func MetaCheck(rootDir string, fileList []string) (jsonOut CheckStage) { + unmatchedList, umatchedRes, err := getMetas(rootDir, fileList) + jsonOut = CheckStage{ + Name: "metaFile", + StdOut: "Checking the existence of meta file: ", + ExitCode: 0, + StdErr: "", + } + + if err != nil { + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = err + return jsonOut + } + + if len(unmatchedList) == 0 { + jsonOut.StdOut += "OK" + return jsonOut + } else { + jsonOut.ExitCode = 104 + jsonOut.StdOut += "Failed" + jsonOut.StdErr = fmt.Sprintf("%d important project files missing\n"+umatchedRes, len(unmatchedList)) + return jsonOut + } +} diff --git a/pkg/healthcheck/nonascii.go b/pkg/healthcheck/nonascii.go new file mode 100644 index 0000000..eb4d6b7 --- /dev/null +++ b/pkg/healthcheck/nonascii.go @@ -0,0 +1,90 @@ +package healthcheck + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "unicode" +) + +// getNonAscii retrieves a list of files in the specified root directory that contain non-ASCII characters. +// It searches for non-ASCII characters in each file's content and returns a list of paths to files containing non-ASCII characters. +func getNonAscii(root string) ([]string, error) { + var nonAscii []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if info.Name() == ".git" || info.Name() == ".gitea" || info.Name() == "ci" { + return filepath.SkipDir + } else { + return nil + } + } + + if info.Name() == "healthcheck" { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + cont := true + for _, c := range scanner.Text() { + if c > unicode.MaxASCII { + nonAscii = append(nonAscii, "\t"+path) + cont = false + break + } + } + if !cont { + break + } + } + + return nil + }) + + return nonAscii, err +} + +// nonAsciiFiles checks for non-ASCII characters in files within the specified root directory. +// It prints a message with the paths to files containing non-ASCII characters, if any. +func NonAsciiFiles(root string) (jsonOut CheckStage) { + jsonOut = CheckStage{ + Name: "NonAsciiFiles", + StdOut: "Checking for non-ascii files: ", + ExitCode: 0, + StdErr: "", + } + nonAscii, err := getNonAscii(root) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = err + return jsonOut + } + + var nonAsciiRes string + if len(nonAscii) > 0 { + nonAsciiRes = fmt.Sprintf("Non-ASCII characters found in the following files:\n" + strings.Join(nonAscii, "\n")) + jsonOut.ExitCode = 105 + jsonOut.StdOut += "Failed" + } else { + jsonOut.StdOut += "OK" + } + + jsonOut.StdErr = nonAsciiRes + + return jsonOut +} diff --git a/pkg/healthcheck/release.go b/pkg/healthcheck/release.go new file mode 100644 index 0000000..43e35e8 --- /dev/null +++ b/pkg/healthcheck/release.go @@ -0,0 +1,88 @@ +package healthcheck + +import ( + "fmt" + "log" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +func catTags(all []string) (out string) { + out = "" + for _, str := range all { + out += str + " " + } + return out +} + +func getTagsFromRepo(repoPath string) ([]string, error) { + repo, err := git.PlainOpen(repoPath) + if err != nil { + return nil, fmt.Errorf("Cannot open repo: %v", err) + } + + refs, err := repo.Tags() + if err != nil { + return nil, fmt.Errorf("Cannot get tags: %v", err) + } + + var tags []string + err = refs.ForEach(func(ref *plumbing.Reference) error { + tags = append(tags, ref.Name().Short()) + return nil + }) + if err != nil { + return nil, fmt.Errorf("Error while iterating tags: %v", err) + } + + return tags, nil +} + +func CheckReleases(repoPath string, category string, n int) (jsonOut CheckStage) { + jsonOut = CheckStage{ + Name: "ReleaseCheck", + StdOut: "Checking release tag: ", + ExitCode: 0, + StdErr: "", + } + + tags, err := getTagsFromRepo(repoPath) + if err != nil { + log.Fatalf("Error in getting tags: %v", err) + } + + var prefix string + + switch category { + case "exam": + prefix = "e" + case "project": + prefix = "p" + case "homework": + prefix = "h" + default: + prefix = "a" + } + + target := prefix + fmt.Sprintf("%d", n) + found := false + for _, tag := range tags { + if tag == target { + found = true + break + } + } + if !found { + tagList := catTags(tags) + jsonOut.ExitCode = 107 + jsonOut.StdOut = "Failed" + jsonOut.StdErr = fmt.Sprintf("wrong release tag '%s', please use one of %s aborting", target, tagList) + return jsonOut + } + + jsonOut.StdOut += "OK" + jsonOut.ExitCode = 0 + jsonOut.StdErr = "Fine" + return jsonOut +} diff --git a/pkg/healthcheck/reposize.go b/pkg/healthcheck/reposize.go new file mode 100644 index 0000000..3c362b2 --- /dev/null +++ b/pkg/healthcheck/reposize.go @@ -0,0 +1,56 @@ +package healthcheck + +import ( + "fmt" + "os/exec" + "strconv" + "strings" +) + +// RepoSize checks the size of the repository to determine if it is oversized. +// It executes the 'git count-objects -v' command to obtain the size information, +func RepoSize() (jsonOut CheckStage) { + jsonOut = CheckStage{ + Name: "RepoSize", + StdOut: "Checking repository size: ", + ExitCode: 0, + StdErr: "", + } + + // TODO: reimplement here when go-git is available + // https://github.com/go-git/go-git/blob/master/COMPATIBILITY.md + cmd := exec.Command("git", "count-objects", "-v") + output, err := cmd.CombinedOutput() + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error running git command:%w", err) + return jsonOut + } + + lines := strings.Split(string(output), "\n") + var sum int + for _, line := range lines { + if strings.Contains(line, "size") { + fields := strings.Fields(line) + sizeStr := fields[1] + size, err := strconv.Atoi(sizeStr) + if err != nil { + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 1 + jsonOut.ErrorLog = fmt.Errorf("Error running git command:%w", err) + return jsonOut + } + sum += size + } + } + + if sum <= 2048 { + jsonOut.StdOut += "OK" + return jsonOut + } + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 100 + jsonOut.StdErr = "repository larger than 2MB, please clean up or contact the teaching team." + return jsonOut +} diff --git a/pkg/healthcheck/utils.go b/pkg/healthcheck/utils.go new file mode 100644 index 0000000..21bb256 --- /dev/null +++ b/pkg/healthcheck/utils.go @@ -0,0 +1,39 @@ +package healthcheck + +import ( + "fmt" + "regexp" +) + +// For ExitCode, see https://focs.ji.sjtu.edu.cn/git/TAs/resources/src/branch/drone/dronelib.checks +// 1 for unrecoverable error and 0 for succeses +type CheckStage struct { + Name string `json:"name of check"` + StdOut string `json:"stdout"` + ExitCode int `json:"exit code"` + StdErr string `json:"stderr"` + ErrorLog error `json:"errorLog"` +} + +// addExt appends the specified extension to each file name in the given fileList. +// It modifies the original fileList in place. +func addExt(fileList []string, ext string) { + for i, file := range fileList { + fileList[i] = file + ext + } +} + +// getRegex compiles each regex pattern in the fileList into a []*regexp.Regexp slice. +// It returns a slice containing compiled regular expressions. +func getRegex(fileList []string) ([]*regexp.Regexp, error) { + var regexList []*regexp.Regexp + for _, pattern := range fileList { + regex, err := regexp.Compile("(?i)" + pattern) + if err != nil { + return nil, fmt.Errorf("Error compiling regex:%w", err) + } + regexList = append(regexList, regex) + } + + return regexList, nil +} diff --git a/pkg/healthcheck/verify.go b/pkg/healthcheck/verify.go new file mode 100644 index 0000000..b201b53 --- /dev/null +++ b/pkg/healthcheck/verify.go @@ -0,0 +1,114 @@ +package healthcheck + +import ( + "bufio" + "fmt" + "os" + "path/filepath" +) + +// fileExists checks if a file exists at the specified path. +func fileExists(filePath string) bool { + _, err := os.Stat(filePath) + return !os.IsNotExist(err) +} + +// filesMatch checks if two files are identical. +func filesMatch(file1, file2 string) (bool, error) { + f1, err := os.Open(file1) + if err != nil { + return false, err + } + defer f1.Close() + + f2, err := os.Open(file2) + if err != nil { + return false, err + } + defer f2.Close() + + scanner1 := bufio.NewScanner(f1) + scanner2 := bufio.NewScanner(f2) + + for scanner1.Scan() && scanner2.Scan() { + line1 := scanner1.Text() + line2 := scanner2.Text() + + if line1 != line2 { + return false, nil + } + } + + if scanner1.Scan() || scanner2.Scan() { + // One file has more lines than the other + return false, nil + } + + return true, nil +} + +// compareDirectories compares the contents of two directories. +func compareDirectories(dir1, dir2 string, jsonOut *CheckStage) error { + allMatch := true + var message string + err := filepath.Walk(dir1, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + relPath, _ := filepath.Rel(dir1, path) + file2 := filepath.Join(dir2, relPath) + + if !fileExists(file2) { + // fmt.Printf("File %s in %s is missing in %s\n, please immediately revert your changes!\n", relPath, dir1, dir2) + message += "File missing" + allMatch = false + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 101 + jsonOut.StdErr = message + return nil + } + + // fmt.Printf("Checking integrity of %s:\n", path) + match, err := filesMatch(path, file2) + if err != nil { + return err + } + + if !match { + // fmt.Printf("File %s in %s is not identical to %s\nPlease revert your changes or contact the teaching team if you have a valid reason for adjusting them.\n", relPath, dir1, dir2) + message += "File is not identical" + allMatch = false + jsonOut.StdOut += "Failed" + jsonOut.ExitCode = 101 + jsonOut.StdErr = message + } + } + return nil + }) + + if allMatch { + jsonOut.StdOut += "OK!" + } else { + jsonOut.StdOut += "Failed!" + } + + return err +} + +// Verify checks if the contents of two directories are identical. +func Verify(rootDir, compareDir string) CheckStage { + jsonOut := CheckStage{ + Name: "verifyFile", + StdOut: "Checking files to be verified: ", + ExitCode: 0, + StdErr: "", + } + err := compareDirectories(rootDir, compareDir, &jsonOut) + // fmt.Println("Comparison finished ") + if err != nil { + fmt.Println("Error:", err) + } + return jsonOut +}