All checks were successful
go-generate-check / go-generate-check (pull_request) Successful in 25s
315 lines
9.6 KiB
Go
315 lines
9.6 KiB
Go
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
|
|
}
|