package common import ( "errors" "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" func (h *RequestHandler) allocateGiteaTransport() (*transport.Runtime, *apiclient.GiteaAPI) { r := transport.New("src.opensuse.org", apiclient.DefaultBasePath, [](string){"https"}) r.DefaultAuthentication = transport.BearerToken(giteaToken) // r.SetDebug(true) return r, apiclient.New(r, nil) } 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 = "" ) func (h *RequestHandler) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) { if h.HasError() { return nil, nil, h.Error } transport, client := h.allocateGiteaTransport() pr, err := client.Repository.RepoGetPullRequest( repository.NewRepoGetPullRequestParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num), transport.DefaultAuthentication, ) if err != nil { h.Error = err h.ErrLogger.Println(err.Error()) return nil, nil, err } limit := int64(1000) reviews, err := client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithDefaults(). WithOwner(org). WithRepo(project). WithIndex(num). WithLimit(&limit), transport.DefaultAuthentication, ) if err != nil { h.Error = err h.ErrLogger.Println(err.Error()) return nil, nil, err } return pr.Payload, reviews.Payload, nil } func (h *RequestHandler) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) { if h.HasError() { return nil, h.Error } bigLimit := int64(100000) transport, client := h.allocateGiteaTransport() params := notification.NewNotifyGetListParams(). WithDefaults(). WithSubjectType([]string{"Pull"}). WithStatusTypes([]string{"unread"}). WithLimit(&bigLimit) if since != nil { s := strfmt.DateTime(*since) params.SetSince(&s) } list, err := client.Notification.NotifyGetList(params, transport.DefaultAuthentication) if err != nil { h.Error = err return nil, err } if !list.IsSuccess() { h.Error = fmt.Errorf("Cannot fetch notifications: %s", list.Error()) return nil, h.Error } return list.Payload, nil } func (h *RequestHandler) SetNotificationRead(notificationId int64) error { if h.HasError() { return h.Error } transport, client := h.allocateGiteaTransport() list, err := client.Notification.NotifyReadThread( notification.NewNotifyReadThreadParams(). WithDefaults(). WithID(fmt.Sprint(notificationId)), transport.DefaultAuthentication, ) if err != nil { h.ErrLogger.Printf("Error setting notification: %d: %v\n", notificationId, err) h.Error = err return err } if !list.IsSuccess() { h.Error = fmt.Errorf("Cannot update notifications: %d", notificationId) return h.Error } return nil } func (h *RequestHandler) CreateRepositoryIfNotExist(org Organization, repoName string) *models.Repository { if h.HasError() { return nil } transport, client := h.allocateGiteaTransport() repo, err := client.Repository.RepoGet( repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName), transport.DefaultAuthentication) if err != nil { switch err.(type) { case *repository.RepoGetNotFound: h.StdLogger.Printf("repo '%s' does not exist. Trying to create it ....\n", repoName) repo, err := 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: h.StdLogger.Printf("repo '%s' created, with notification error?\n", repoName) default: h.Error = err h.ErrLogger.Printf("error creating repo '%s' under '%s': %s\n", repoName, org.Username, err.Error()) return nil } } else { h.StdLogger.Printf("repo '%s' created\n", repoName) } // initialize repository h.Error = os.Mkdir(filepath.Join(h.GitPath, DefaultGitPrj), 0700) if h.HasError() { return nil } h.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName) h.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch) if h.HasError() { return nil } readmeFilename := filepath.Join(h.GitPath, DefaultGitPrj, "README.md") { file, _ := os.Create(readmeFilename) defer file.Close() io.WriteString(file, ReadmeBoilerplate) } h.GitExec(DefaultGitPrj, "add", "README.md") h.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation") h.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL) return repo.Payload default: h.Error = err h.ErrLogger.Printf("cannot fetch repo data for '%s' / '%s' : %v\n", org.Username, repoName, err) } } return repo.Payload } func (h *RequestHandler) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) *models.PullRequest { if h.HasError() { return nil } transport, client := h.allocateGiteaTransport() prOptions := models.CreatePullRequestOption{ Base: repo.DefaultBranch, Head: srcId, Title: title, Body: body, } pr, err := client.Repository.RepoCreatePullRequest( repository. NewRepoCreatePullRequestParams(). WithDefaults(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithBody(&prOptions), transport.DefaultAuthentication, ) if err != nil { h.ErrLogger.Printf("Cannot create pull request: %v\n", err) h.Error = err return nil } if !pr.IsSuccess() { h.ErrLogger.Printf("PR creation failed: %s\n", pr.Error()) h.Error = errors.New(pr.Error()) return nil } return pr.GetPayload() } func (h *RequestHandler) RequestReviews(pr *models.PullRequest, reviewer string) []*models.PullReview { if h.HasError() { return nil } transport, client := h.allocateGiteaTransport() reviewOptions := models.PullReviewRequestOptions{ Reviewers: []string{reviewer}, } review, err := client.Repository.RepoCreatePullReviewRequests( repository. NewRepoCreatePullReviewRequestsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithIndex(pr.Index). WithBody(&reviewOptions), transport.DefaultAuthentication, ) if err != nil { h.ErrLogger.Printf("Cannot create pull request: %v\n", err) h.Error = err return nil } if !review.IsSuccess() { h.ErrLogger.Printf("PR creation failed: %s\n", review.Error()) h.Error = errors.New(review.Error()) return nil } return review.GetPayload() } func (h *RequestHandler) IsReviewed(pr *models.PullRequest) (bool, error) { if h.HasError() { return false, h.Error } transport, client := h.allocateGiteaTransport() // TODO: get review from project git reviewers := pr.RequestedReviewers var page, limit int64 var reviews []*models.PullReview page = 0 limit = 20 for { res, err := client.Repository.RepoListPullReviews( repository.NewRepoListPullReviewsParams(). WithOwner(pr.Base.Repo.Owner.UserName). WithRepo(pr.Base.Repo.Name). WithPage(&page). WithLimit(&limit), 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 (h *RequestHandler) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) { transport, client := h.allocateGiteaTransport() h.StdLogger.Printf("%#v", *pr) c, err := 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, }), 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 (h *RequestHandler) GetAssociatedPrjGitPR(pr *PullRequestAction) *models.PullRequest { if h.HasError() { return nil } transport, client := h.allocateGiteaTransport() var page, maxSize int64 page = 1 maxSize = 10000 state := "open" prs, err := client.Repository.RepoListPullRequests( repository. NewRepoListPullRequestsParams(). WithDefaults(). WithOwner(pr.Repository.Owner.Username). WithRepo(DefaultGitPrj). WithState(&state). WithLimit(&maxSize). WithPage(&page), transport.DefaultAuthentication) if err != nil { h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %v", pr.Repository.Owner.Username, pr.Repository.Name, err) return nil } if !prs.IsSuccess() { h.Error = fmt.Errorf("cannot fetch PR list for %s / %s : %s", pr.Repository.Owner.Username, pr.Repository.Name, prs.Error()) } 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 } } } return nil } func (h *RequestHandler) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) { if h.HasError() { return nil, h.Error } transport, client := h.allocateGiteaTransport() var retData []byte dataOut := writeFunc(func(data []byte) (int, error) { if len(data) == 0 { return 0, nil } retData = data return len(data), nil }) file, err := client.Repository.RepoGetRawFile( repository.NewRepoGetRawFileParams(). WithOwner(repo.Owner.UserName). WithRepo(repo.Name). WithFilepath(path). WithRef(&hash), transport.DefaultAuthentication, dataOut, repository.WithContentTypeApplicationOctetStream, ) if err != nil { return nil, err } if !file.IsSuccess() { return nil, fmt.Errorf("Invalid response from server (%d): %s", file.Code(), file.Error()) } return retData, nil } func (h *RequestHandler) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) { return h.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path) }