Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: [PL-24913]: Add the support for create and update file in bitbucket server #177

Merged
merged 8 commits into from
May 9, 2022
26 changes: 24 additions & 2 deletions scm/driver/stash/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,26 @@ func (s *contentService) Find(ctx context.Context, repo, path, ref string) (*scm
}

func (s *contentService) Create(ctx context.Context, repo, path string, params *scm.ContentParams) (*scm.Response, error) {
return nil, scm.ErrNotSupported
namespace, repoName := scm.Split(repo)
endpoint := fmt.Sprintf("rest/api/1.0/projects/%s/repos/%s/browse/%s", namespace, repoName, path)
in := &contentCreateUpdate{
Message: params.Message,
Branch: params.Branch,
Content: params.Data,
}
return s.client.do(ctx, "PUT", endpoint, in, nil)
}

func (s *contentService) Update(ctx context.Context, repo, path string, params *scm.ContentParams) (*scm.Response, error) {
return nil, scm.ErrNotSupported
namespace, repoName := scm.Split(repo)
endpoint := fmt.Sprintf("rest/api/1.0/projects/%s/repos/%s/browse/%s", namespace, repoName, path)
in := &contentCreateUpdate{
Message: params.Message,
Branch: params.Branch,
Content: params.Data,
Sha: params.Sha,
}
return s.client.do(ctx, "PUT", endpoint, in, nil)
}

func (s *contentService) Delete(ctx context.Context, repo, path string, params *scm.ContentParams) (*scm.Response, error) {
Expand All @@ -53,6 +68,13 @@ type contents struct {
Values []string `json:"values"`
}

type contentCreateUpdate struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would a name like "multipartContent" or "multipartUploadContent" be better here?

Also suggest removing the json tags as it's a little confusing to the reader; the purpose of this struct appears to be in the case where json is not used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the current design in scm service is that whenever we are calling the client.do method, we pass it the json value and the client.do method converts it into the multipart data.
Because of which we cannot name it to multipartContent

Branch string `json:"branch"`
Message string `json:"message"`
Content []byte `json:"content"`
Sha string `json:"sourceCommitId"`
}

func convertContentInfoList(from *contents) []*scm.ContentInfo {
to := []*scm.ContentInfo{}
for _, v := range from.Values {
Expand Down
73 changes: 65 additions & 8 deletions scm/driver/stash/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,75 @@ func TestContentFind(t *testing.T) {
}

func TestContentCreate(t *testing.T) {
content := new(contentService)
_, err := content.Create(context.Background(), "atlassian/atlaskit", "README", nil)
if err != scm.ErrNotSupported {
t.Errorf("Expect Not Supported error")
defer gock.Off()

gock.New("http://localhost:7990").
Put("/rest/api/1.0/projects/octocat/repos/hello-world/browse/README").
Reply(200).
Type("application/json").
File("testdata/content_create.json")

params := &scm.ContentParams{
Message: "my commit message",
Data: []byte("bXkgbmV3IGZpbGUgY29udGVudHM="),
Signature: scm.Signature{
Name: "Monalisa Octocat",
Email: "octocat@github.com",
},
}

client := NewDefault()
res, err := client.Contents.Create(
context.Background(),
"octocat/hello-world",
"README",
params,
)

if err != nil {
t.Error(err)
return
}

if res.Status != 200 {
t.Errorf("Unexpected Results")
}
}

func TestContentUpdate(t *testing.T) {
content := new(contentService)
_, err := content.Update(context.Background(), "atlassian/atlaskit", "README", nil)
if err != scm.ErrNotSupported {
t.Errorf("Expect Not Supported error")
defer gock.Off()

gock.New("http://localhost:7990").
Put("/rest/api/1.0/projects/octocat/repos/hello-world/browse/README").
Reply(200).
Type("application/json").
File("testdata/content_update.json")

params := &scm.ContentParams{
Message: "a new commit message",
Data: []byte("bXkgdXBkYXRlZCBmaWxlIGNvbnRlbnRz"),
BlobID: "95b966ae1c166bd92f8ae7d1c313e738c731dfc3",
Signature: scm.Signature{
Name: "Monalisa Octocat",
Email: "octocat@github.com",
},
}

client := NewDefault()
res, err := client.Contents.Update(
context.Background(),
"octocat/hello-world",
"README",
params,
)

if err != nil {
t.Error(err)
return
}

if res.Status != 200 {
t.Errorf("Unexpected Results")
}
}

Expand Down
62 changes: 62 additions & 0 deletions scm/driver/stash/integration/content_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package integration

import (
"context"
"net/http"
"testing"

"github.com/drone/go-scm/scm"
"github.com/drone/go-scm/scm/driver/stash"
"github.com/drone/go-scm/scm/transport"
)

func TestCreateUpdateDeleteFileStash(t *testing.T) {
if token == "" {
t.Skip("Skipping, Acceptance test")
}
client, _ = stash.New(endpoint)
client.Client = &http.Client{
Transport: &transport.BasicAuth{
Username: username,
Password: token,
},
}
// get latest commit first
currentCommit, commitErr := GetCurrentCommitOfBranch(client, "master")
if commitErr != nil {
t.Errorf("we got an error %v", commitErr)
}
// create a new file
createParams := scm.ContentParams{
Message: "go-scm create crud file",
Data: []byte("hello"),
Branch: "master",
Sha: currentCommit,
}
createResponse, createErr := client.Contents.Create(context.Background(), repoID, "README5", &createParams)
if createErr != nil {
t.Errorf("Contents.Create we got an error %v", createErr)
}
if createResponse.Status != http.StatusOK {
t.Errorf("Contents.Create we did not get a 201 back %v", createResponse.Status)
}
// get latest commit first
currentCommit, commitErr = GetCurrentCommitOfBranch(client, "main")
if commitErr != nil {
t.Errorf("we got an error %v", commitErr)
}
// update the file
updateParams := scm.ContentParams{
Message: "go-scm update crud file",
Data: []byte("updated test data"),
Branch: "master",
Sha: currentCommit,
}
updateResponse, updateErr := client.Contents.Update(context.Background(), repoID, "README5", &updateParams)
if updateErr != nil {
t.Errorf("Contents.Update we got an error %v", updateErr)
}
if updateResponse.Status != http.StatusOK {
t.Errorf("Contents.Update we did not get a 201 back %v", updateResponse.Status)
}
}
14 changes: 12 additions & 2 deletions scm/driver/stash/integration/testSettings.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package integration

import (
"context"
"os"

"github.com/drone/go-scm/scm"
)

var (
client *scm.Client
token = os.Getenv("BITBUCKET_SERVER_TOKEN")
client *scm.Client
token = os.Getenv("BITBUCKET_SERVER_TOKEN")

endpoint = "https://bitbucket.dev.harness.io/"
repoID = "har/scm-integration-test-repo"
username = "harnessadmin"
commitId = "f675c4b55841908d7c338c500c8f4cb844fd9be7"
)

func GetCurrentCommitOfBranch(client *scm.Client, branch string) (string, error) {
commits, _, err := client.Git.ListCommits(context.Background(), repoID, scm.CommitListOptions{Ref: branch})
if err != nil {
return "", err
}
return commits[0].Sha, nil
}
36 changes: 32 additions & 4 deletions scm/driver/stash/stash.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/url"
"strings"

Expand Down Expand Up @@ -74,10 +75,37 @@ func (c *wrapper) do(ctx context.Context, method, path string, in, out interface
// if we are posting or putting data, we need to
// write it to the body of the request.
if in != nil {
buf := new(bytes.Buffer)
json.NewEncoder(buf).Encode(in)
req.Header["Content-Type"] = []string{"application/json"}
req.Body = buf
switch content := in.(type) {
case *contentCreateUpdate:
// add the content to the multipart
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the other fields
if content.Message != "" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to check for empty value? Or in the name of readability might it be easier to pass on the empty fields? (there might be, I'm unfamiliar with the API)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some of the cases this values are not event required for the bitbucket API, hence I added this check that if the value is not null then don't add this value to multipart data

_ = w.WriteField("content", string(content.Content))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please handle these errors. Aware they're repetitive but could be more palatable with something like:

type multipartWriter struct {
    w mime. Writer
    err error
}

func (multipartWriter *mw) Write(f, v string) {
    if mw.err == nil {
        return
    }
    if v == "" {
        return
    }
    _, err = mw.WriteField(f, v)    
}
case *contentCreateUpdate:
    var b bytes.Buffer
    mw := multipartWriter{w: multipart.NewWriter(&b)}
    mw.write("content", string(content.Content))
    mw.write("message", content.Message)
    mw.write("branch", content.Branch)
    mw.write("sha", content.Sha)
    if mw.err != nil {
        return nil, fmt.Errorf("error writing multipart-content. err: %s", mw.err)
    }

see https://go.dev/blog/errors-are-values for background

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this change in another PR

}
if content.Message != "" {
_ = w.WriteField("message", content.Message)
}
if content.Branch != "" {
_ = w.WriteField("branch", content.Branch)
}
if content.Sha != "" {
_ = w.WriteField("sourceCommitId", content.Sha)
}
w.Close()
// write the multipart response to the body
req.Body = &b
// write the content type that contains the length of the multipart
req.Header = map[string][]string{
"Content-Type": {w.FormDataContentType()},
}
default:
buf := new(bytes.Buffer)
json.NewEncoder(buf).Encode(in)
req.Header["Content-Type"] = []string{"application/json"}
req.Body = buf
}
}

// execute the http request
Expand Down
21 changes: 21 additions & 0 deletions scm/driver/stash/testdata/content_create.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "abcdef0123abcdef4567abcdef8987abcdef6543",
"displayId": "abcdef0123a",
"author": {
"name": "charlie",
"emailAddress": "charlie@example.com"
},
"authorTimestamp": 1636089306104,
"committer": {
"name": "charlie",
"emailAddress": "charlie@example.com"
},
"committerTimestamp": 1636089306104,
"message": "WIP on feature 1",
"parents": [
{
"id": "abcdef0123abcdef4567abcdef8987abcdef6543",
"displayId": "abcdef0"
}
]
}
21 changes: 21 additions & 0 deletions scm/driver/stash/testdata/content_update.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"id": "abcdef0123abcdef4567abcdef8987abcdef6543",
"displayId": "abcdef0123a",
"author": {
"name": "charlie",
"emailAddress": "charlie@example.com"
},
"authorTimestamp": 1636089306104,
"committer": {
"name": "charlie",
"emailAddress": "charlie@example.com"
},
"committerTimestamp": 1636089306104,
"message": "WIP on feature 1",
"parents": [
{
"id": "abcdef0123abcdef4567abcdef8987abcdef6543",
"displayId": "abcdef0"
}
]
}