package main import ( "errors" "fmt" "log" "math/rand" "path" "strings" "time" "src.opensuse.org/autogits/common" ) //go:generate mockgen -source=repo_check.go -destination=mock/repo_check.go -typed type StateChecker interface { VerifyProjectState(orgName string, configs []*common.AutogitConfig, idx int) error CheckRepos() error ConsistencyCheckProcess() error } type DefaultStateChecker struct { exitCheckLoop bool checkOnStart bool checkInterval time.Duration gitea common.Gitea git common.GitHandlerGenerator processor *RequestProcessor i StateChecker } func CreateDefaultStateChecker(checkOnStart bool, processor *RequestProcessor, gitea common.Gitea, interval time.Duration) *DefaultStateChecker { var s = &DefaultStateChecker{ git: &common.GitHandlerImpl{}, gitea: gitea, checkInterval: interval, checkOnStart: checkOnStart, processor: processor, } s.i = s return s } func (s *DefaultStateChecker) VerifyProjectState(orgName string, configs []*common.AutogitConfig, idx int) error { org := common.Organization{ Username: orgName, } git, err := s.git.CreateGitHandler(GitAuthor, GitEmail, AppName) if err != nil { return fmt.Errorf("Cannot create git handler: %w", err) } config := configs[idx] repo, err := s.gitea.CreateRepositoryIfNotExist(git, org, config.GitProjectName) if err != nil { return fmt.Errorf("Error fetching or creating '%s/%s' -- aborting verifyProjectState(). Err: %w", orgName, config.GitProjectName, err) } common.PanicOnError(git.GitExec("", "clone", "--depth", "1", repo.SSHURL, config.GitProjectName)) log.Println("getting submodule list") submodules, err := git.GitSubmoduleList(config.GitProjectName, "HEAD") nextSubmodule: for sub, commitID := range submodules { log.Println(" + 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) if err != nil { return fmt.Errorf("Error fetching pull requests for %s/%s. Err: %w", config.Organization, submoduleName, err) } if DebugMode { log.Println(" - # of PRs to check:", len(prs)) } for _, pr := range prs { var event common.PullRequestWebhookEvent event.Pull_Request = common.PullRequestFromModel(pr) event.Action = string(pr.State) event.Number = int(pr.Index) event.Repository = common.RepositoryFromModel(pr.Base.Repo) event.Sender = *common.UserFromModel(pr.User) event.Requested_reviewer = nil git, err := s.git.CreateGitHandler(GitAuthor, GitEmail, AppName) if err != nil { return fmt.Errorf("Error allocating GitHandler. Err: %w", err) } if !DebugMode { defer git.Close() } switch pr.State { case "open": s.processor.Opened.Process(&event, git, config) case "closed": s.processor.Closed.Process(&event, git, config) default: return fmt.Errorf("Unhandled pull request state: '%s'. %s/%s/%d", pr.State, config.Organization, submoduleName, pr.Index) } } // 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 ... log.Println(" W -", 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 { if DebugMode { log.Println(" - 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 configIdx, config := range configs { if s.checkInterval > 0 { sleepInterval := (s.checkInterval - s.checkInterval/2) + time.Duration(rand.Int63n(int64(s.checkInterval))) log.Println(" - sleep interval", sleepInterval, "until next check") time.Sleep(sleepInterval) } log.Printf(" ++ starting verification, org: `%s` config: `%s`\n", org, config.GitProjectName) if err := s.i.VerifyProjectState(org, configs, configIdx); err != nil { log.Printf(" *** verification failed, org: `%s`, err: %#v\n", org, err) errorList = append(errorList, err) } log.Printf(" ++ verification complete, org: `%s` config: `%s`\n", org, config.GitProjectName) } } return errors.Join(errorList...) } func (s *DefaultStateChecker) ConsistencyCheckProcess() error { if s.checkOnStart { savedCheckInterval := s.checkInterval s.checkInterval = 0 log.Println("== Startup consistency check begin...") s.i.CheckRepos() log.Println("== Startup consistency check done...") s.checkInterval = savedCheckInterval } for { if s.exitCheckLoop { break } s.i.CheckRepos() } return nil }