Skip to content

fix: handle changes/<change>/patch JSON marshalling error #182

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions changes_revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gerrit

import (
"context"
"errors"
"fmt"
"net/url"
)
Expand Down Expand Up @@ -140,6 +141,35 @@ type PatchOptions struct {
Path string `url:"path,omitempty"`
}

// StringPointerWriter is a new type based on *string.
// We would like it to implement Writer interface so that we can avoid
// unmarshalling of non JSON responses by Client.Do
type StringPointerWriter struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is an implementation detail of this package, it shouldn't be made exported. Once it's exported, it can't be removed without breaking API compatibility.

Target *string
}

func NewStringPointerWriter(target *string) (*StringPointerWriter, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similarly here, this shouldn't be exported.

if target == nil {
return nil, errors.New("StringPointerWriter: target *string cannot be nil")
}
return &StringPointerWriter{Target: target}, nil
}

// Write implements the io.Writer interface for *StringPointerWriter.
func (spw *StringPointerWriter) Write(p []byte) (n int, err error) {
// Check if the StringPointerWriter pointer itself is nil, or if its Target is nil.
if spw == nil || spw.Target == nil {
return 0, errors.New("StringPointerWriter: receiver or target *string is nil, cannot write")
}

// *(spw.Target) gives us the actual string value.
// We append the new data (converted from []byte to string) to it.
*(spw.Target) = *(spw.Target) + string(p)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Appending a string to a string causes a copy and and allocation, so when you have a need to append, it's generally better to stick with []byte which can append without copying and allocating whenever there's enough capacity in the slice.


// Return the number of bytes written and no error.
return len(p), nil
}

// GetDiff gets the diff of a file from a certain revision.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-diff
Expand Down Expand Up @@ -445,8 +475,17 @@ func (s *ChangesService) GetPatch(ctx context.Context, changeID, revisionID stri
return nil, nil, err
}

v := new(string)
resp, err := s.client.Do(req, v)
strVal := ""
var v *string = &strVal
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you help clarify what's the motivation to change v := new(string) into the following code?

strVal := ""
var v *string = &strVal


// Create an instance of our writer struct since the /patch endpoint
// returns a base64 encoded string which cannot be marshalled as JSON.
stringWriter, err := NewStringPointerWriter(v)
if err != nil {
return nil, nil, err
}

resp, err := s.client.Do(req, stringWriter)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suspect it's better to go with another way to achieve the outcome you want.

Consider the similar GetCommitContent method which uses getStringResponseWithoutOptions:

// GetCommitContent gets the content of a file from a certain commit.
// The content is returned as base64 encoded string.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html##get-content-from-commit
func (s *ProjectsService) GetCommitContent(ctx context.Context, projectName, commitID, fileID string) (string, *Response, error) {
u := fmt.Sprintf("projects/%s/commits/%s/files/%s/content", url.QueryEscape(projectName), commitID, fileID)
return getStringResponseWithoutOptions(ctx, s.client, u)
}

As another example, see the approach used by another Gerrit client library here:

https://cs.opensource.google/go/x/build/+/1810ea16:gerrit/gerrit.go;l=904-912;drc=2a0314b810a580bd009db088d0d17ba343b2e5ae

if err != nil {
return nil, resp, err
}
Expand Down
39 changes: 39 additions & 0 deletions changes_revision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gerrit_test

import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -82,3 +83,41 @@ func TestChangesService_ListFilesReviewed(t *testing.T) {
t.Errorf("client.Changes.ListFilesReviewed:\ngot: %q\nwant: %q", got, want)
}
}

func TestChangesService_GetPatch(t *testing.T) {
rawPatch := `diff --git a/COMMIT_MSG b/COMMIT_MSG
index 123..456 100644
--- a/COMMIT_MSG
+++ b/COMMIT_MSG
@@ -1,1 +1,1 @@
-Old subject
+New subject for A
diff --git a/fileA.txt b/fileA.txt
new file mode 100644
index 0000000..abc 100644
--- /dev/null
+++ b/fileA.txt
@@ -0,0 +1 @@
+Content for A
`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), "/changes/123/revisions/456/patch"; got != want {
t.Errorf("request URL:\ngot: %q\nwant: %q", got, want)
}
encodedPatchContent := base64.StdEncoding.EncodeToString(
[]byte(rawPatch))
fmt.Fprint(w, encodedPatchContent)
}))
defer ts.Close()

ctx := context.Background()
client := newClient(ctx, t, ts)
got, _, err := client.Changes.GetPatch(ctx, "123", "456", nil)
if err != nil {
t.Fatal(err)
}
want := base64.StdEncoding.EncodeToString([]byte(rawPatch))
if !reflect.DeepEqual(*got, want) {
t.Errorf("client.Changes.GetPatch:\ngot: %q\nwant: %q", *got, want)
}
}