package main import ( "errors" "fmt" "math/rand" "path" "runtime/debug" "strings" "time" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) //go:generate mockgen -source=repo_check.go -destination=mock/repo_check.go -typed type StateChecker interface { VerifyProjectState(configs *common.AutogitConfig) error CheckRepos() error ConsistencyCheckProcess() error } type DefaultStateChecker struct { exitCheckLoop bool checkOnStart bool checkInterval time.Duration processor *RequestProcessor i StateChecker } func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker { var s = &DefaultStateChecker{ checkInterval: interval, checkOnStart: checkOnStart, processor: processor, } s.i = s return s } func (s *DefaultStateChecker) ProcessPR(git common.Git, pr *models.PullRequest, config *common.AutogitConfig) error { var event common.PullRequestWebhookEvent event.Pull_Request = common.PullRequestFromModel(pr) event.Action = string(pr.State) + "ed" event.Number = pr.Index event.Repository = common.RepositoryFromModel(pr.Base.Repo) event.Sender = *common.UserFromModel(pr.User) event.Requested_reviewer = nil return ProcesPullRequest(&event, common.AutogitConfigs{config}) } func (s *DefaultStateChecker) VerifyProjectState(config *common.AutogitConfig) error { defer func() { if r := recover(); r != nil { common.LogError("panic caught") if err, ok := r.(error); !ok { common.LogError(err) } common.LogError(string(debug.Stack())) } }() prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit() common.LogInfo(" checking", prjGitOrg+"/"+prjGitRepo+"#"+prjGitBranch) git, err := GitHandler.CreateGitHandler(config.Organization) common.LogDebug("Git Path:", git.GetPath()) if err != nil { return fmt.Errorf("Cannot create git handler: %w", err) } defer git.Close() repo, err := Gitea.CreateRepositoryIfNotExist(git, prjGitOrg, prjGitRepo) if err != nil { return fmt.Errorf("Error fetching or creating '%s/%s#%s' -- aborting verifyProjectState(). Err: %w", prjGitBranch, prjGitRepo, prjGitBranch, err) } _, err = git.GitClone(prjGitRepo, prjGitBranch, repo.SSHURL) common.PanicOnError(err) prs, err := Gitea.GetRecentPullRequests(prjGitOrg, prjGitRepo, prjGitBranch) if err != nil { return fmt.Errorf("Error fetching PrjGit Prs for %s/%s#%s: %w", prjGitOrg, prjGitRepo, prjGitBranch, err) } for _, pr := range prs { s.ProcessPR(git, pr, config) } common.LogDebug(" - # of PRs to check in PrjGit:", len(prs)) submodules, err := git.GitSubmoduleList(prjGitRepo, "HEAD") nextSubmodule: for sub, commitID := range submodules { common.LogDebug(" + checking", sub, commitID) submoduleName := sub if n := strings.LastIndex(sub, "/"); n != -1 { submoduleName = sub[n+1:] } // check if open PR have PR against project branch := config.Branch if branch == "" { repo, err := Gitea.GetRepository(config.Organization, submoduleName) if err != nil || repo == nil { common.LogError("Cannot fetch repository:", config.Organization, "/", submoduleName, "... submodule skipped.") continue } branch = repo.DefaultBranch } prs, err := Gitea.GetRecentPullRequests(config.Organization, submoduleName, branch) if err != nil { return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", config.Organization, submoduleName, branch, err) } common.LogDebug(" - # of PRs to check:", len(prs)) for _, pr := range prs { s.ProcessPR(git, pr, config) } // check if the commited changes are syned with branches commits, err := Gitea.GetRecentCommits(config.Organization, submoduleName, branch, 10) if err != nil { return fmt.Errorf("Error fetching recent commits for %s/%s. Err: %w", config.Organization, submoduleName, err) } for idx, commit := range commits { if commit.SHA == commitID { if idx != 0 { // commit in past ... common.LogError(" -", submoduleName, " is behind the branch by", idx, "This should not happen in PR workflow alone") } continue nextSubmodule } } // not found in past, check if we should advance the branch label ... pull the submodule git.GitExecOrPanic(prjGitRepo, "submodule", "update", "--init", "--filter", "blob:none", "--", sub) subDir := path.Join(prjGitRepo, sub) newCommits := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(subDir, "rev-list", "^origin/"+branch, commitID), "\n") if len(newCommits) >= 1 { common.LogDebug(" - updating branch", branch, "to new head", commitID, " - len:", len(newCommits)) git.GitExecOrPanic(subDir, "checkout", "-B", branch, commitID) url := git.GitExecWithOutputOrPanic(subDir, "remote", "get-url", "origin", "--push") sshUrl, err := common.TranslateHttpsToSshUrl(strings.TrimSpace(url)) if err != nil { return fmt.Errorf("Cannot traslate HTTPS git URL to SSH_URL. %w", err) } git.GitExecOrPanic(subDir, "remote", "set-url", "origin", "--push", sshUrl) git.GitExecOrPanic(subDir, "push", "origin", branch) } } // forward any package-gits referred by the project git, but don't go back return nil } func (s *DefaultStateChecker) CheckRepos() error { errorList := make([]error, 0, 10) for org, configs := range s.processor.configuredRepos { for _, config := range configs { if s.checkInterval > 0 { sleepInterval := (s.checkInterval - s.checkInterval/2) + time.Duration(rand.Int63n(int64(s.checkInterval))) common.LogInfo(" - sleep interval", sleepInterval, "until next check") time.Sleep(sleepInterval) } common.LogInfo(" ++ starting verification, org:", org, "config:", config.GitProjectName) if err := s.i.VerifyProjectState(config); err != nil { common.LogError(" *** verification failed, org:", org, err) errorList = append(errorList, err) } common.LogInfo(" ++ verification complete, org:", org, "config:", config.GitProjectName) } } return errors.Join(errorList...) } func (s *DefaultStateChecker) ConsistencyCheckProcess() error { if s.checkOnStart { savedCheckInterval := s.checkInterval s.checkInterval = 0 common.LogInfo("== Startup consistency check begin...") s.i.CheckRepos() common.LogInfo("== Startup consistency check done...") s.checkInterval = savedCheckInterval } for { if s.exitCheckLoop { break } s.i.CheckRepos() } return nil }