package common import ( "fmt" "io" "os" "path/filepath" "slices" "strings" "time" transport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" apiclient "src.opensuse.org/autogits/common/gitea-generated/client" "src.opensuse.org/autogits/common/gitea-generated/client/notification" "src.opensuse.org/autogits/common/gitea-generated/client/organization" "src.opensuse.org/autogits/common/gitea-generated/client/repository" "src.opensuse.org/autogits/common/gitea-generated/models" ) const PrPattern = "PR: %s/%s#%d" const ( // from Gitea // ReviewStateApproved pr is approved ReviewStateApproved models.ReviewStateType = "APPROVED" // ReviewStatePending pr state is pending ReviewStatePending models.ReviewStateType = "PENDING" // ReviewStateComment is a comment review ReviewStateComment models.ReviewStateType = "COMMENT" // ReviewStateRequestChanges changes for pr are requested ReviewStateRequestChanges models.ReviewStateType = "REQUEST_CHANGES" // ReviewStateRequestReview review is requested from user ReviewStateRequestReview models.ReviewStateType = "REQUEST_REVIEW" // ReviewStateUnknown state of pr is unknown ReviewStateUnknown models.ReviewStateType = "" ) type GiteaTransport struct { transport *transport.Runtime client *apiclient.GiteaAPI } func AllocateGiteaTransport(host string) *GiteaTransport { var r GiteaTransport r.transport = transport.New(host, apiclient.DefaultBasePath, [](string){"https"}) r.transport.DefaultAuthentication = transport.BearerToken(giteaToken) r.client = apiclient.New(r.transport, nil) return &r } func (gitea *GiteaTransport) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) { pr, err := gitea.client.Repository.RepoGetPullRequest( repository.NewRepoGetPullRequestParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, nil, err } limit := int64(1000) reviews, err := gitea.client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num). WithLimit(&limit), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, nil, err } return pr.Payload, reviews.Payload, nil } func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) { bigLimit := int64(100000) params := notification.NewNotifyGetListParams(). WithDefaults(). WithSubjectType([]string{"Pull"}). WithStatusTypes([]string{"unread"}). WithLimit(&bigLimit) if since != nil { s := strfmt.DateTime(*since) params.SetSince(&s) } list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication) if err != nil { return nil, err } return list.Payload, nil } func (gitea *GiteaTransport) SetNotificationRead(notificationId int64) error { _, err := gitea.client.Notification.NotifyReadThread( notification.NewNotifyReadThreadParams(). WithDefaults(). WithID(fmt.Sprint(notificationId)), gitea.transport.DefaultAuthentication, ) if err != nil { return fmt.Errorf("Error setting notification: %d. Err: %w", notificationId, err) } return nil } func (gitea *GiteaTransport) GetOrganization(orgName string) (*models.Organization, error) { org, err := gitea.client.Organization.OrgGet( organization.NewOrgGetParams().WithOrg(orgName), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Error fetching org: '%s' data. Err: %w", orgName, err) } return org.Payload, nil } func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git *GitHandler, org Organization, repoName string) (*models.Repository, error) { repo, err := gitea.client.Repository.RepoGet( repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName), gitea.transport.DefaultAuthentication) if err != nil { switch err.(type) { case *repository.RepoGetNotFound: repo, err := gitea.client.Organization.CreateOrgRepo( organization.NewCreateOrgRepoParams().WithDefaults().WithBody( &models.CreateRepoOption{ AutoInit: false, Name: &repoName, ObjectFormatName: models.CreateRepoOptionObjectFormatNameSha256, }, ).WithOrg(org.Username), nil, ) if err != nil { switch err.(type) { case *organization.CreateOrgRepoCreated: // weird, but ok, repo created default: return nil, fmt.Errorf("error creating repo '%s' under '%s'. Err: %w", repoName, org.Username, err) } } // initialize repository if err = os.Mkdir(filepath.Join(git.GitPath, DefaultGitPrj), 0700); err != nil { return nil, err } git.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName) git.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch) readmeFilename := filepath.Join(git.GitPath, DefaultGitPrj, "README.md") { file, _ := os.Create(readmeFilename) defer file.Close() io.WriteString(file, ReadmeBoilerplate) } git.GitExec(DefaultGitPrj, "add", "README.md") git.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation") git.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL) return repo.Payload, nil default: return nil, fmt.Errorf("cannot fetch repo data for '%s' / '%s' : %w", org.Username, repoName, err) } } return repo.Payload, nil } func (gitea *GiteaTransport) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) { prOptions := models.CreatePullRequestOption{ Base: repo.DefaultBranch, Head: srcId, Title: title, Body: body, } pr, err := gitea.client.Repository.RepoCreatePullRequest( repository. NewRepoCreatePullRequestParams(). WithDefaults(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithBody(&prOptions), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Cannot create pull request. %w", err) } return pr.GetPayload(), nil } func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error) { reviewOptions := models.PullReviewRequestOptions{ Reviewers: []string{reviewer}, } review, err := gitea.client.Repository.RepoCreatePullReviewRequests( repository. NewRepoCreatePullReviewRequestsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&reviewOptions), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, fmt.Errorf("Cannot create pull request: %w", err) } return review.GetPayload(), nil } func (gitea *GiteaTransport) IsReviewed(pr *models.PullRequest) (bool, error) { // TODO: get review from project git reviewers := pr.RequestedReviewers var page, limit int64 var reviews []*models.PullReview page = 0 limit = 20 for { res, err := gitea.client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithPage(&page). WithLimit(&limit), gitea.transport.DefaultAuthentication) if err != nil { return false, err } if res.IsSuccess() { r := res.Payload if reviews == nil { reviews = r } else { reviews = append(reviews, r...) } if len(r) < int(limit) { break } } } slices.Reverse(reviews) for _, review := range reviews { if review.Stale || review.Dismissed { continue } next_review: for i, reviewer := range reviewers { if review.User.UserName == reviewer.UserName { switch review.State { case ReviewStateApproved: reviewers = slices.Delete(reviewers, i, i) break next_review case ReviewStateRequestChanges: return false, nil } } } } return len(reviewers) == 0, nil } func (gitea *GiteaTransport) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) { c, err := gitea.client.Repository.RepoCreatePullReview( repository.NewRepoCreatePullReviewParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&models.CreatePullReviewOptions{ Event: state, Body: comment, }), gitea.transport.DefaultAuthentication, ) /* c, err := client.Repository.RepoSubmitPullReview( repository.NewRepoSubmitPullReviewParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithID(review.ID). WithBody(&models.SubmitPullReviewOptions{ Event: state, Body: comment, }), transport.DefaultAuthentication, ) */ /* c, err := client.Issue.IssueCreateComment( issue.NewIssueCreateCommentParams(). WithDefaults(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&models.CreateIssueCommentOption{ Body: &comment, }), transport.DefaultAuthentication) */ if err != nil { return nil, err } return c.Payload, nil } func (gitea *GiteaTransport) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent) (*models.PullRequest, error) { var page, maxSize int64 page = 1 maxSize = 10000 state := "open" prs, err := gitea.client.Repository.RepoListPullRequests( repository. NewRepoListPullRequestsParams(). WithDefaults(). WithOwner(pr.Repository.Owner.Username). WithRepo(DefaultGitPrj). WithState(&state). WithLimit(&maxSize). WithPage(&page), gitea.transport.DefaultAuthentication) if err != nil { return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", pr.Repository.Owner.Username, pr.Repository.Name, err) } prLine := fmt.Sprintf(PrPattern, pr.Repository.Owner.Username, pr.Repository.Name, pr.Number) // h.StdLogger.Printf("attemping to match line: '%s'\n", prLine) // payload_processing: for _, pr := range prs.Payload { lines := strings.Split(pr.Body, "\n") for _, line := range lines { if strings.TrimSpace(line) == prLine { return pr, nil } } } return nil, nil } func (gitea *GiteaTransport) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) { var retData []byte dataOut := writeFunc(func(data []byte) (int, error) { if len(data) == 0 { return 0, nil } retData = data return len(data), nil }) _, err := gitea.client.Repository.RepoGetRawFile( repository.NewRepoGetRawFileParams(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithFilepath(path). WithRef(&hash), gitea.transport.DefaultAuthentication, dataOut, repository.WithContentTypeApplicationOctetStream, ) if err != nil { return nil, err } return retData, nil } func (gitea *GiteaTransport) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) { return gitea.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path) } func (gitea *GiteaTransport) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) { not := false commits, err := gitea.client.Repository.RepoGetAllCommits( repository.NewRepoGetAllCommitsParams(). WithStat(¬). WithFiles(¬). WithVerification(¬). WithLimit(&commitNo), gitea.transport.DefaultAuthentication, ) if err != nil { return nil, err } return commits.Payload, nil }