Skip to content

Commit

Permalink
Add API for editing pull requests
Browse files Browse the repository at this point in the history
This commit adds an `Edit` method to the gitprovider.PullRequestClient
interface. Since the APIs for editing PRs/MRs vary widely between
providers, an `EditOptions` type is introduced for determining
specific fields to be edited.

Currently, only a pull request's title can be edited.

closes #164

Signed-off-by: Max Jonas Werner <[email protected]>
  • Loading branch information
makkes committed Nov 8, 2022
1 parent fe9beee commit 9dffaae
Show file tree
Hide file tree
Showing 21 changed files with 174 additions and 49 deletions.
4 changes: 2 additions & 2 deletions github/client_repository_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package github
import (
"context"
"fmt"
"io/ioutil"
"io"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/google/go-github/v47/github"
Expand Down Expand Up @@ -65,7 +65,7 @@ func (c *FileClient) Get(ctx context.Context, path, branch string, optFns ...git
if err != nil {
return nil, err
}
content, err := ioutil.ReadAll(output)
content, err := io.ReadAll(output)
if err != nil {
return nil, err
}
Expand Down
11 changes: 11 additions & 0 deletions github/client_repository_pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ func (c *PullRequestClient) Create(ctx context.Context, title, branch, baseBranc
return newPullRequest(c.clientContext, pr), nil
}

// Edit modifies an existing PR. Please refer to "EditOptions" for details on which data can be edited.
func (c *PullRequestClient) Edit(ctx context.Context, number int, opts gitprovider.EditOptions) (gitprovider.PullRequest, error) {
editPR := &github.PullRequest{}
editPR.Title = opts.Title
editedPR, _, err := c.c.Client().PullRequests.Edit(ctx, c.ref.GetIdentity(), c.ref.GetRepository(), number, editPR)
if err != nil {
return nil, err
}
return newPullRequest(c.clientContext, editedPR), nil
}

// Get retrieves an existing pull request by number
func (c *PullRequestClient) Get(ctx context.Context, number int) (gitprovider.PullRequest, error) {

Expand Down
18 changes: 15 additions & 3 deletions github/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"math/rand"
"net/http"
"os"
"reflect"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -427,7 +428,7 @@ var _ = Describe("GitHub Provider", func() {
Expect(hasPermission).To(Equal(true))
})

It("should be possible to create a pr for a user repository", func() {
It("should be possible to create and edit a pr for a user repository", func() {

userRepoRef := newUserRepoRef(testUser, testUserRepoName)

Expand Down Expand Up @@ -480,6 +481,7 @@ var _ = Describe("GitHub Provider", func() {
Expect(pr.Get().Merged).To(BeFalse())

prs, err := userRepo.PullRequests().List(ctx)
Expect(err).NotTo(HaveOccurred())
Expect(len(prs)).To(Equal(1))
Expect(prs[0].Get().WebURL).To(Equal(pr.Get().WebURL))

Expand Down Expand Up @@ -508,13 +510,23 @@ var _ = Describe("GitHub Provider", func() {
Expect(pr.Get().WebURL).ToNot(BeEmpty())
Expect(pr.Get().Merged).To(BeFalse())

err = userRepo.PullRequests().Merge(ctx, pr.Get().Number, gitprovider.MergeMethodMerge, "merged")
editedPR, err := userRepo.PullRequests().Edit(ctx, pr.Get().Number, gitprovider.EditOptions{
Title: gitprovider.StringVar("a new title"),
})
Expect(err).ToNot(HaveOccurred())

err = userRepo.PullRequests().Merge(ctx, editedPR.Get().Number, gitprovider.MergeMethodMerge, "merged")
Expect(err).ToNot(HaveOccurred())

getPR, err = userRepo.PullRequests().Get(ctx, pr.Get().Number)
getPR, err = userRepo.PullRequests().Get(ctx, editedPR.Get().Number)
Expect(err).ToNot(HaveOccurred())

Expect(getPR.Get().Merged).To(BeTrue())
apiObject := getPR.APIObject()
githubPR, ok := apiObject.(*github.PullRequest)
Expect(ok).To(BeTrue(), "API object of PullRequest has unexpected type %q", reflect.TypeOf(apiObject))
Expect(githubPR.Title).ToNot(BeNil())
Expect(*githubPR.Title).To(Equal("a new title"))
})

It("should be possible to download files from path and branch specified", func() {
Expand Down
2 changes: 1 addition & 1 deletion gitlab/client_repositories_org.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (c *OrgRepositoriesClient) Reconcile(ctx context.Context, ref gitprovider.O
return actual, actionTaken, err
}

//nolint
// nolint
func createProject(ctx context.Context, c gitlabClient, ref gitprovider.RepositoryRef, groupName string, req gitprovider.RepositoryInfo, opts ...gitprovider.RepositoryCreateOption) (*gitlab.Project, error) {
// First thing, validate and default the request to ensure a valid and fully-populated object
// (to minimize any possible diffs between desired and actual state)
Expand Down
4 changes: 2 additions & 2 deletions gitlab/client_repository_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package gitlab
import (
"context"
"encoding/base64"
"io/ioutil"
"io"
"strings"

"github.com/fluxcd/go-git-providers/gitprovider"
Expand Down Expand Up @@ -72,7 +72,7 @@ func (c *FileClient) Get(ctx context.Context, path, branch string, optFns ...git
}
filePath := fileDownloaded.FilePath
fileContentDecoded := base64.NewDecoder(base64.RawStdEncoding, strings.NewReader(fileDownloaded.Content))
fileBytes, err := ioutil.ReadAll(fileContentDecoded)
fileBytes, err := io.ReadAll(fileContentDecoded)
if err != nil {
return nil, err
}
Expand Down
12 changes: 12 additions & 0 deletions gitlab/client_repository_pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ func (c *PullRequestClient) Create(_ context.Context, title, branch, baseBranch,
return newPullRequest(c.clientContext, mr), nil
}

// Edit modifies an existing MR. Please refer to "EditOptions" for details on which data can be edited.
func (c *PullRequestClient) Edit(ctx context.Context, number int, opts gitprovider.EditOptions) (gitprovider.PullRequest, error) {
mrUpdate := &gitlab.UpdateMergeRequestOptions{
Title: opts.Title,
}
editedMR, _, err := c.c.Client().MergeRequests.UpdateMergeRequest(getRepoPath(c.ref), number, mrUpdate)
if err != nil {
return nil, err
}
return newPullRequest(c.clientContext, editedMR), nil
}

// Get retrieves an existing pull request by number
func (c *PullRequestClient) Get(_ context.Context, number int) (gitprovider.PullRequest, error) {

Expand Down
19 changes: 16 additions & 3 deletions gitlab/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"math/rand"
"net/http"
"os"
"reflect"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -548,7 +549,7 @@ var _ = Describe("GitLab Provider", func() {
Permission: &pushPermission,
}

ta, actionTaken, err = repo.TeamAccess().Reconcile(ctx, teamInfo)
_, actionTaken, err = repo.TeamAccess().Reconcile(ctx, teamInfo)
Expect(err).ToNot(HaveOccurred())
Expect(actionTaken).To(Equal(false))
})
Expand Down Expand Up @@ -752,7 +753,7 @@ var _ = Describe("GitLab Provider", func() {
validateUserRepo(newRepo, repoRef)
})

It("should be possible to create a pr for a user repository", func() {
It("should be possible to create and edit a pr for a user repository", func() {

testRepoName = fmt.Sprintf("test-repo2-%03d", rand.Intn(1000))
repoRef := newUserRepoRef(testUserName, testRepoName)
Expand Down Expand Up @@ -865,8 +866,20 @@ var _ = Describe("GitLab Provider", func() {
Expect(pr.Get().WebURL).ToNot(BeEmpty())
Expect(pr.Get().Merged).To(BeFalse())

err = userRepo.PullRequests().Merge(ctx, pr.Get().Number, gitprovider.MergeMethodMerge, "merged")
editedPR, err := userRepo.PullRequests().Edit(ctx, pr.Get().Number, gitprovider.EditOptions{
Title: gitprovider.StringVar("a new title"),
})
Expect(err).ToNot(HaveOccurred())

err = userRepo.PullRequests().Merge(ctx, editedPR.Get().Number, gitprovider.MergeMethodMerge, "merged")
Expect(err).ToNot(HaveOccurred())

getPR, err := userRepo.PullRequests().Get(ctx, editedPR.Get().Number)
Expect(err).ToNot(HaveOccurred())
apiObject := getPR.APIObject()
gitlabMR, ok := apiObject.(*gitlab.MergeRequest)
Expect(ok).To(BeTrue(), "API object of PullRequest has unexpected type %q", reflect.TypeOf(apiObject))
Expect(gitlabMR.Title).To(Equal("a new title"))

expectPRToBeMerged(ctx, userRepo, pr.Get().Number)

Expand Down
2 changes: 1 addition & 1 deletion gitlab/resource_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (s *gitlabProjectSpec) Equals(other *gitlabProjectSpec) bool {
return cmp.Equal(s, other)
}

//nolint
// nolint
var gitlabVisibilityMap = map[gitprovider.RepositoryVisibility]gogitlab.VisibilityValue{
gitprovider.RepositoryVisibilityInternal: gogitlab.InternalVisibility,
gitprovider.RepositoryVisibilityPrivate: gogitlab.PrivateVisibility,
Expand Down
2 changes: 1 addition & 1 deletion gitlab/resource_teamaccess.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (ta *teamAccess) Reconcile(ctx context.Context) (bool, error) {
return true, ta.Update(ctx)
}

//nolint
// nolint
var permissionPriority = map[int]gitprovider.RepositoryPermission{
10: gitprovider.RepositoryPermissionPull,
20: gitprovider.RepositoryPermissionTriage,
Expand Down
9 changes: 9 additions & 0 deletions gitprovider/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,21 @@ type PullRequestClient interface {
List(ctx context.Context) ([]PullRequest, error)
// Create creates a pull request with the given specifications.
Create(ctx context.Context, title, branch, baseBranch, description string) (PullRequest, error)
// Edit allows for changing an existing pull request using the given options. Please refer to "EditOptions" for details on which data can be
// edited.
Edit(ctx context.Context, number int, opts EditOptions) (PullRequest, error)
// Get retrieves an existing pull request by number
Get(ctx context.Context, number int) (PullRequest, error)
// Merge merges a pull request with via either the "Squash" or "Merge" method
Merge(ctx context.Context, number int, mergeMethod MergeMethod, message string) error
}

// EditOptions is provided to a PullRequestClient's "Edit" method for updating an existing pull request.
type EditOptions struct {
// Title is set to a non-nil value to request a pull request's title to be changed.
Title *string
}

// FileClient operates on the branches for a specific repository.
// This client can be accessed through Repository.Branches().
type FileClient interface {
Expand Down
3 changes: 3 additions & 0 deletions gitprovider/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
)

// knownRepositoryVisibilityValues is a map of known RepositoryVisibility values, used for validation.
//
//nolint:gochecknoglobals
var knownRepositoryVisibilityValues = map[RepositoryVisibility]struct{}{
RepositoryVisibilityPublic: {},
Expand Down Expand Up @@ -98,6 +99,7 @@ const (
)

// knownRepositoryVisibilityValues is a map of known RepositoryPermission values, used for validation.
//
//nolint:gochecknoglobals
var knownRepositoryPermissionValues = map[RepositoryPermission]struct{}{
RepositoryPermissionPull: {},
Expand Down Expand Up @@ -140,6 +142,7 @@ const (
)

// knownLicenseTemplateValues is a map of known LicenseTemplate values, used for validation
//
//nolint:gochecknoglobals
var knownLicenseTemplateValues = map[LicenseTemplate]struct{}{
LicenseTemplateApache2: {},
Expand Down
25 changes: 13 additions & 12 deletions stash/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,19 @@ func WithAuth(username string, token string) ClientOptionsFunc {
// If the http.Header is nil, a default http.Header is used.
// ClientOptionsFunc is an optional function and can be used to configure the client.
// Example:
// c, err := NewClient(
// &http.Client {
// Transport: defaultTransport,
// Timeout: defaultTimeout,
// }, "https://github.com",
// &http.Header {
// "Content-Type": []string{"application/json"},
// },
// logr.Logger{},
// func(c *Client) {
// c.DisableRetries = true
// })
//
// c, err := NewClient(
// &http.Client {
// Transport: defaultTransport,
// Timeout: defaultTimeout,
// }, "https://github.com",
// &http.Header {
// "Content-Type": []string{"application/json"},
// },
// logr.Logger{},
// func(c *Client) {
// c.DisableRetries = true
// })
func NewClient(httpClient *http.Client, host string, header *http.Header, logger logr.Logger, opts ...ClientOptionsFunc) (*Client, error) {
if host == "" {
return nil, errors.New("host is required")
Expand Down
34 changes: 34 additions & 0 deletions stash/client_repository_pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ func (c *PullRequestClient) Create(ctx context.Context, title, branch, baseBranc
return newPullRequest(created), nil
}

// Edit modifies an existing PR. Please refer to "EditOptions" for details on which data can be edited.
func (c *PullRequestClient) Edit(ctx context.Context, number int, opts gitprovider.EditOptions) (gitprovider.PullRequest, error) {
projectKey, repoSlug := getStashRefs(c.ref)

// check if it is a user repository
// if yes, we need to add a tilde to the user login and use it as the project key
if r, ok := c.ref.(gitprovider.UserRepositoryRef); ok {
projectKey = addTilde(r.UserLogin)
}

// need to fetch the PR first to get the right version number
pr, err := c.Get(ctx, number)
if err != nil {
return nil, fmt.Errorf("failed to get PR %d: %w", number, err)
}
apiObject, ok := pr.APIObject().(*PullRequest)
if !ok {
return nil, fmt.Errorf("API object is of unexpected type") // this should never happen!
}

if opts.Title != nil {
apiObject.Title = *opts.Title
}
// the REST API doesn't accept the following fields to be set for update requests
apiObject.Author = nil
apiObject.Participants = nil
edited, err := c.client.PullRequests.Update(ctx, projectKey, repoSlug, apiObject)
if err != nil {
return nil, fmt.Errorf("failed to edit pull request: %w", err)
}

return newPullRequest(edited), nil
}

func validatePullRequestsAPI(apiObj *PullRequest) error {
return validateAPIObject("Stash.PullRequest", func(validator validation.Validator) {
// Make sure there is a version and a title
Expand Down
2 changes: 1 addition & 1 deletion stash/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}

//NewTestClient returns a Client with Transport replaced to avoid making real calls
// NewTestClient returns a Client with Transport replaced to avoid making real calls
func NewTestClient(t *testing.T, fn RoundTripFunc, opts ...ClientOptionsFunc) *Client {
c, err := NewClient(nil, defaultHost, nil, initLogger(t))
if err != nil {
Expand Down
1 change: 0 additions & 1 deletion stash/deploy_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ func (s *DeployKeysService) Delete(ctx context.Context, projectKey, repositorySl

// UpdateKeyPermission updates the given access key permission
// UpdateKeyPermission uses the endpoint "PUT /rest/keys/1.0/projects/{projectKey}/ssh/{keyId}/permission/{permission}".
//
func (s *DeployKeysService) UpdateKeyPermission(ctx context.Context, projectKey, repositorySlug string, keyID int, permission string) (*DeployKey, error) {
req, err := s.Client.NewRequest(ctx, http.MethodPut, newKeysURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, deployKeysURI, strconv.Itoa(keyID),
keyPermisionsURI, permission))
Expand Down
20 changes: 20 additions & 0 deletions stash/integration_repositories_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"math/rand"
"reflect"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/go-git-providers/gitprovider/testutils"
Expand Down Expand Up @@ -220,11 +221,30 @@ var _ = Describe("Stash Provider", func() {
Expect(err).ToNot(HaveOccurred())
Expect(pr.Get().WebURL).ToNot(BeEmpty())

editedPR, err := userRepo.PullRequests().Edit(ctx, pr.Get().Number, gitprovider.EditOptions{
Title: gitprovider.StringVar("a new title"),
})
Expect(err).ToNot(HaveOccurred(), "error editing PR")
Expect(editedPR).ToNot(BeNil(), "returned PR should never be nil if no error was returned")

// edit one more time to make sure the version number is taken into account

editedPR, err = userRepo.PullRequests().Edit(ctx, pr.Get().Number, gitprovider.EditOptions{
Title: gitprovider.StringVar("another new title"),
})
Expect(err).ToNot(HaveOccurred(), "error editing PR a second time")
Expect(editedPR).ToNot(BeNil(), "returned PR should never be nil if no error was returned")

// List PRs
prs, err := userRepo.PullRequests().List(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(len(prs)).To(Equal(1))

apiObject := prs[0].APIObject()
stashPR, ok := apiObject.(*PullRequest)
Expect(ok).To(BeTrue(), "API object of PullRequest has unexpected type %q", reflect.TypeOf(apiObject))
Expect(stashPR.Title).To(Equal("another new title"))

// Merge PR
id := pr.APIObject().(*PullRequest).ID
err = userRepo.PullRequests().Merge(ctx, id, "merge", "merged")
Expand Down
Loading

0 comments on commit 9dffaae

Please sign in to comment.