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 gitea common.Gitea processor *RequestProcessor i StateChecker } func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker { var s = &DefaultStateChecker{ gitea: gitea, 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) event.Number = pr.Index event.Repository = common.RepositoryFromModel(pr.Base.Repo) event.Sender = *common.UserFromModel(pr.User) event.Requested_reviewer = nil var err error switch pr.State { case "open": err = s.processor.Opened.Process(&event, git, config) case "closed": err = s.processor.Closed.Process(&event, git, config) default: return fmt.Errorf("Unhandled pull request state: '%s'. %s/%s/%d", pr.State, config.Organization, "", pr.Index) } common.LogError(" * processor error returned:", err) return nil } 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(prjGitOrg) if err != nil { return fmt.Errorf("Cannot create git handler: %w", err) } defer git.Close() repo, err := s.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(config.GitProjectName, prjGitBranch, repo.SSHURL) common.PanicOnError(err) prs, err := s.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(config.GitProjectName, "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 prs, err := s.gitea.GetRecentPullRequests(config.Organization, submoduleName, config.Branch) if err != nil { return fmt.Errorf("Error fetching pull requests for %s/%s#%s. Err: %w", config.Organization, submoduleName, config.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 := s.gitea.GetRecentCommits(config.Organization, submoduleName, config.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(config.GitProjectName, "submodule", "update", "--init", "--filter", "blob:none", "--", sub) subDir := path.Join(config.GitProjectName, sub) newCommits := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(subDir, "rev-list", "^origin/"+config.Branch, commitID), "\n") if len(newCommits) >= 1 { common.LogDebug(" - updating branch", config.Branch, "to new head", commitID, " - len:", len(newCommits)) git.GitExecOrPanic(subDir, "checkout", "-B", config.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", config.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 }