package main import ( "fmt" "slices" "strings" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) func FindSourceRepository(org, repo string) (*models.Repository, error) { srcRepo, err := Gitea.GetRepository(org, repo) if err != nil { return nil, err } if srcRepo == nil { return nil, fmt.Errorf("Source repository not found: %s/%s", org, repo) } if srcRepo.Parent == nil { return nil, fmt.Errorf("Source has no parents: %s/%s", org, repo) } return srcRepo, nil } func createEmptyBranch(git common.Git, PackageName, Branch string) { git.GitExecOrPanic(PackageName, "checkout", "--detach") git.GitExec(PackageName, "branch", "-D", Branch) git.GitExecOrPanic(PackageName, "checkout", "-f", "--orphan", Branch) git.GitExecOrPanic(PackageName, "rm", "-rf", ".") git.GitExecOrPanic(PackageName, "commit", "--allow-empty", "-m", "Initial empty branch") } type TimelineInterface interface { FindPullRequestReferences(org, repo string, idx int64, creator []string) []*models.TimelineComment } type Timeline []*models.TimelineComment func (timeline *Timeline) FindIssuePullRequestRererences(org, repo string, idx int64, creator []string) []*models.TimelineComment { ret := make([]*models.TimelineComment, 0, 1) for _, t := range *timeline { if t.Type == common.TimelineCommentType_PullRequestRef && t.RefIssue != nil && t.RefIssue.Repository.Owner == org && t.RefIssue.Repository.Name == repo && (idx == 0 || t.RefIssue.Index == idx) && (len(creator) == 0 || slices.Contains(creator, t.User.UserName)) { ret = append(ret, t) } } return ret } type IssueProcessorInterface interface { IsAddIssue() bool IsRmIssue() bool GetTargetBranch() string } type IssueProcessor struct { issue *models.Issue IssueTimeline Timeline TargetBranch string } func (i *IssueProcessor) GetTargetBranch() string { const BranchPrefix = "refs/heads/" branch := i.issue.Ref if branch, found := strings.CutPrefix(branch, BranchPrefix); found { return branch } else { common.LogDebug("Invalid branch specified:", branch, ". Using default.") branch = "" } return branch } func ProcessIssue(issue *models.Issue, configs common.AutogitConfigs) error { i := &IssueProcessor{issue: issue} return i.ProcessIssue(configs) } func (i *IssueProcessor) IsAddIssue() bool { if i == nil || i.issue == nil { return false } title := i.issue.Title return len(title) > 5 && strings.EqualFold(title[0:5], "[ADD]") } func (i *IssueProcessor) IsRmIssue() bool { if i == nil || i.issue == nil { return false } title := i.issue.Title return len(title) > 4 && strings.EqualFold(title[0:4], "[RM]") } func (i *IssueProcessor) ProcessAddIssue(config *common.AutogitConfig) error { issue := i.issue org := issue.Repository.Owner repo := issue.Repository.Name // idx := issue.Index // we need "New Package" label and "Approval Required" label, unless already approved // either via Label "Approved" or via review comment. NewIssues := common.FindNewReposInIssueBody(issue.Body) if NewIssues == nil { common.LogDebug("No new repos found in issue body") return nil } git, err := GitHandler.CreateGitHandler(config.Organization) if err != nil { return err } defer git.Close() for _, nr := range NewIssues.Repos { common.LogDebug(" - Processing new repository src:", nr.Organization+"/"+nr.PackageName+"#"+nr.Branch) targetRepo, err := Gitea.GetRepository(config.Organization, nr.PackageName) if err != nil { return err } if targetRepo == nil { common.LogInfo(" - Repository", config.Organization+"/"+nr.PackageName, "does not exist. Labeling issue.") if !common.IsDryRun && issue.State == "open" { Gitea.SetLabels(org, repo, issue.Index, []string{config.Label(common.Label_NewRepository)}) } common.LogDebug(" # Done for now with this repo") continue } // check if we already have created a PR here // TODO, we need to filter by project config permissions of target project, not just assume bot here. users := []string{CurrentUser.UserName} prs := i.IssueTimeline.FindIssuePullRequestRererences(config.Organization, nr.PackageName, 0, users) for _, t := range prs { pr, err := Gitea.GetPullRequest(config.Organization, nr.PackageName, t.RefIssue.Index) if err != nil { common.LogError("Failed to fetch PR", common.PRtoString(pr), ":", err) } if issue.State == "open" { // PR already created, we just need to update it now common.LogInfo("Update PR ", common.PRtoString(pr), "only... Nothing to do now") return nil } // so, issue is closed .... close associated package PR _, err = Gitea.UpdateIssue(config.Organization, nr.PackageName, t.RefIssue.Index, &models.EditIssueOption{State: "closed"}) if err != nil { common.LogError("Failed to close associated PR", common.PRtoString(pr), ":", err) } // remove branch if it's a new repository. return err } srcRepo, err := FindSourceRepository(nr.Organization, nr.Repository) if err != nil { continue } if len(nr.Branch) == 0 { nr.Branch = srcRepo.DefaultBranch } srcRemoteName, err := git.GitClone(nr.PackageName, nr.Branch, srcRepo.SSHURL) if err != nil { return err } remoteName, err := git.GitClone(nr.PackageName, nr.Branch, targetRepo.SSHURL) if err != nil { return err } // Check that fork/parent repository relationship exists if srcRepo.Parent.Name != targetRepo.Name || srcRepo.Parent.Owner.UserName != targetRepo.Owner.UserName { common.LogError("Source repository is not fork of the Target repository. Fork of:", srcRepo.Parent.Owner.UserName+"/"+srcRepo.Parent.Name) continue } srcBranch := nr.Branch if srcBranch == "" { srcBranch = srcRepo.DefaultBranch } // We are ready to setup a pending PR. // 1. empty target branch with empty commit, this will be discarded no merge // 2. create PR from source to target // a) if source is not branch, create a source branch in target repo that contains the relevant commit SourceCommitList := common.SplitLines(git.GitExecWithOutputOrPanic(nr.PackageName, "rev-list", "--first-parent", srcRemoteName+"/"+nr.Branch)) CommitLength := len(SourceCommitList) SourceCommitId := SourceCommitList[CommitLength-1] if CommitLength > 20 { SourceCommitId = SourceCommitList[20] } if CommitLength < 2 { // only 1 commit, then we need empty branch on target if dl, err := git.GitDirectoryContentList(nr.PackageName, nr.Branch); err == nil && len(dl) > 0 { createEmptyBranch(git, nr.PackageName, nr.Branch) } } else { git.GitExecOrPanic(nr.PackageName, "checkout", "-B", nr.Branch, SourceCommitId) } if !common.IsDryRun { git.GitExecOrPanic(nr.PackageName, "push", "-f", remoteName, nr.Branch) } head := nr.Organization + ":" + srcBranch isBranch := false // Hash can be branch name! Check if it's a branch or tag on the remote out, err := git.GitExecWithOutput(nr.PackageName, "ls-remote", "--heads", srcRepo.SSHURL, srcBranch) if err == nil && strings.Contains(out, "refs/heads/"+srcBranch) { isBranch = true } if !isBranch { tempBranch := fmt.Sprintf("new_package_%d_%s", issue.Index, nr.PackageName) // Re-clone or use existing if branch check was done above remoteName, err := git.GitClone(nr.PackageName, srcBranch, targetRepo.SSHURL) if err != nil { return err } git.GitExecOrPanic(nr.PackageName, "remote", "add", "source", srcRepo.SSHURL) git.GitExecOrPanic(nr.PackageName, "fetch", "source", srcBranch) git.GitExecOrPanic(nr.PackageName, "checkout", "-B", tempBranch, "FETCH_HEAD") if !common.IsDryRun { git.GitExecOrPanic(nr.PackageName, "push", "-f", remoteName, tempBranch) } head = tempBranch } title := fmt.Sprintf("Add package %s", nr.PackageName) prjGitOrg, prjGitRepo, _ := config.GetPrjGit() body := fmt.Sprintf("See issue %s/%s#%d", prjGitOrg, prjGitRepo, issue.Index) br := i.TargetBranch if len(br) == 0 { br = targetRepo.DefaultBranch } pr, err, isNew := Gitea.CreatePullRequestIfNotExist(targetRepo, head, br, title, body) if err != nil { common.LogError(targetRepo.Name, head, i.TargetBranch, title, body) return err } if !isNew && (pr.Body != body || !pr.AllowMaintainerEdit) { Gitea.UpdatePullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index, &models.EditPullRequestOption{ AllowMaintainerEdit: true, Body: body, }) } if isNew { if _, err := Gitea.SetLabels(config.Organization, nr.PackageName, pr.Index, []string{config.Label(common.Label_NewRepository)}); err != nil { common.LogError("Failed to set label:", common.Label_NewRepository, err) } } } return nil } func (i *IssueProcessor) ProcessIssue(configs common.AutogitConfigs) error { issue := i.issue org := issue.Repository.Owner repo := issue.Repository.Name idx := issue.Index // out, _ := json.MarshalIndent(issue, "", " ") // common.LogDebug(string(out)) var err error i.IssueTimeline, err = Gitea.GetTimeline(org, repo, idx) if err != nil { common.LogError(" timeline fetch failed:", err) return err } i.TargetBranch = i.GetTargetBranch() config := configs.GetPrjGitConfig(org, repo, i.TargetBranch) if config == nil { return fmt.Errorf("Cannot find config for %s/%s#%s", org, repo, i.TargetBranch) } common.LogDebug("issue processing:", common.IssueToString(issue), "@", i.TargetBranch) if i.IsAddIssue() { i.ProcessAddIssue(config) } else if i.IsRmIssue() { // to remove a package, no approval is required. This should happen via // project git PR reviews } else { common.LogError("Non-standard issue created. Ignoring", common.IssueToString(issue)) return nil } return nil }