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

Add support for querying the events-log plugin #10

Merged
merged 13 commits into from
Aug 2, 2016
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ go-gerrit is a [Go(lang)](https://golang.org/) client library for accessing the
* [/groups/](https://godoc.org/github.com/andygrunwald/go-gerrit#GroupsService)
* [/plugins/](https://godoc.org/github.com/andygrunwald/go-gerrit#PluginsService)
* [/projects/](https://godoc.org/github.com/andygrunwald/go-gerrit#ProjectsService)
* Supports optional plugin APIs such as
* events-log - [About](https://gerrit.googlesource.com/plugins/events-log/+/master/src/main/resources/Documentation/about.md), [REST API](https://gerrit.googlesource.com/plugins/events-log/+/master/src/main/resources/Documentation/rest-api-events.md)


## Installation

Expand Down Expand Up @@ -243,6 +246,14 @@ This library might be working with older versions as well.
If you notice an incompatibility [open a new issue](https://github.com/andygrunwald/go-gerrit/issues/new) or try to fix it.
We welcome contribution!


### What about adding code to support the REST API of an optional plugin?

It will depend on the plugin, you are welcome to [open a new issue](https://github.com/andygrunwald/go-gerrit/issues/new) first to propose the idea if you wish.
As an example the addition of support for events-log plugin was supported because the plugin itself is fairly
popular and the structures that the REST API uses could also be used by `gerrit stream-events`.


## License

This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
Expand Down
147 changes: 147 additions & 0 deletions events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package gerrit

import (
"bytes"
"encoding/json"
"io/ioutil"
"net/url"
"time"
)

// PatchSet contains detailed information about a specific patch set.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#patchSet
type PatchSet struct {
Number string `json:"number"`
Revision string `json:"revision"`
Parents []string `json:"parents"`
Ref string `json:"ref"`
Uploader AccountInfo `json:"uploader"`
Author AccountInfo `json:"author"`
CreatedOn int `json:"createdOn"`
IsDraft bool `json:"isDraft"`
Kind string `json:"kind"`
}

// RefUpdate contains data about a reference update.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#refUpdate
type RefUpdate struct {
OldRev string `json:"oldRev"`
NewRev string `json:"newRev"`
RefName string `json:"refName"`
Project string `json:"project"`
}

// EventInfo contains information about an event emitted by Gerrit. This
// structure can be used either when parsing streamed events or when reading
// the output of the events-log plugin.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events
type EventInfo struct {
Type string `json:"type"`
Change ChangeInfo `json:"change,omitempty"`
PatchSet PatchSet `json:"patchSet,omitempty"`
EventCreatedOn int `json:"eventCreatedOn,omitempty"`
Reason string `json:"reason,omitempty"`
Abandoner AccountInfo `json:"abandoner,omitempty"`
Restorer AccountInfo `json:"restorer,omitempty"`
Submitter AccountInfo `json:"submitter,omitempty"`
Author AccountInfo `json:"author,omitempty"`
Uploader AccountInfo `json:"uploader,omitempty"`
Approvals []AccountInfo `json:"approvals,omitempty"`
Comment string `json:"comment,omitempty"`
Editor AccountInfo `json:"editor,omitempty"`
Added []string `json:"added,omitempty"`
Removed []string `json:"removed,omitempty"`
Hashtags []string `json:"hashtags,omitempty"`
RefUpdate RefUpdate `json:"refUpdate,omitempty"`
Project string `json:"project,omitempty"`
Reviewer AccountInfo `json:"reviewer,omitempty"`
OldTopic string `json:"oldTopic,omitempty"`
Changer AccountInfo `json:"changer,omitempty"`
}

// EventsLogService contains functions for querying the API provided
// by the optional events-log plugin.
type EventsLogService struct {
client *Client
}

// EventsLogOptions contains options for querying events from the events-logs
// plugin.
type EventsLogOptions struct {
From time.Time
To time.Time
}

// getURL returns the url that should be used in the request. This will vary
// depending on the options provided to GetEvents.
func (events *EventsLogService) getURL(options *EventsLogOptions) (string, error) {
parsed, err := url.Parse("/plugins/events-log/events/")
if err != nil {
return "", err
}

query := parsed.Query()

if !options.From.IsZero() {
query.Set("t1", options.From.Format("2006-01-02 15:04:05"))
}

if !options.To.IsZero() {
query.Set("t2", options.To.Format("2006-01-02 15:04:05"))
}

encoded := query.Encode()
if len(encoded) > 0 {
parsed.RawQuery = encoded
}

return parsed.String(), nil
}

// GetEvents returns a list of events for the given input options. Use of this
// function an authenticated user.
//
// Gerrit API docs: https://<yourserver>/plugins/events-log/Documentation/rest-api-events.html
func (events *EventsLogService) GetEvents(options *EventsLogOptions) (*[]EventInfo, *Response, error) {
requestURL, err := events.getURL(options)
if err != nil {
return nil, nil, err
}

request, err := events.client.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, nil, err
}

// Perform the request but do not pass in a structure to unpack
// the response into. The format of the response is one EventInfo
// object per line so we need to manually handle the response here.
response, err := events.client.Do(request, nil)
if err != nil {
return nil, nil, err
}

body, err := ioutil.ReadAll(response.Body)

defer response.Body.Close()
if err != nil {
return nil, nil, err
}

eventInfo := new([]EventInfo)
for _, line := range bytes.Split(body, []byte("\n")) {
if len(line) > 0 {
event := EventInfo{}
err := json.Unmarshal(line, &event)
if err != nil {
return nil, nil, err
}
*eventInfo = append(*eventInfo, event)
}
}

return eventInfo, response, err
}
135 changes: 135 additions & 0 deletions events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package gerrit_test

import (
"net/http"
"testing"
"time"

"github.com/andygrunwald/go-gerrit"
)

var (
fakeEvents = []byte(`
{"submitter":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"newRev":"0000000000000000000000000000000000000000","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"TRIVIAL_REBASE","sizeInsertions":10,"sizeDeletions":0},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nline2\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"MERGED"},"type":"change-merged","eventCreatedOn":1470000000}
{"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"comment":"Patch Set 1:\n\n(2 comments)\n\nSome comment","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"REWORK","sizeInsertions":4,"sizeDeletions":-2},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"NEW"},"type":"comment-added","eventCreatedOn":1470000000}`)
)

func TestEventsLogService_GetEvents_NoDateRange(t *testing.T) {
setup()
defer teardown()

testMux.HandleFunc("/plugins/events-log/events/", func(writer http.ResponseWriter, request *http.Request) {
writer.Write(fakeEvents)
})

options := &gerrit.EventsLogOptions{}
events, _, err := testClient.EventsLog.GetEvents(options)
if err != nil {
t.Error(err)
}

if len(*events) != 2 {
t.Error("Expected 2 events")
}

// Basic test
for i, event := range *events {
switch i {
case 0:
if event.Type != "change-merged" {
t.Error("Expected event type to be `change-merged`")
}
case 1:
if event.Type != "comment-added" {
t.Error("Expected event type to be `comment-added`")
}
}
}
}

func TestEventsLogService_GetEvents_DateRangeFromAndTo(t *testing.T) {
setup()
defer teardown()

to := time.Now()
from := to.AddDate(0, 0, -7)

testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()

fromFormat := from.Format("2006-01-02 15:04:05")
if query.Get("t1") != fromFormat {
t.Errorf("%s != %s", query.Get("t1"), fromFormat)
}

toFormat := to.Format("2006-01-02 15:04:05")
if query.Get("t2") != toFormat {
t.Errorf("%s != %s", query.Get("t2"), toFormat)
}

writer.Write(fakeEvents)
})

options := &gerrit.EventsLogOptions{From: from, To: to}
_, _, err := testClient.EventsLog.GetEvents(options)
if err != nil {
t.Error(err)
}
}

func TestEventsLogService_GetEvents_DateRangeFromOnly(t *testing.T) {
setup()
defer teardown()

to := time.Now()
from := to.AddDate(0, 0, -7)

testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()

fromFormat := from.Format("2006-01-02 15:04:05")
if query.Get("t1") != fromFormat {
t.Errorf("%s != %s", query.Get("t1"), fromFormat)
}

if query.Get("t2") != "" {
t.Error("Did not expect t2 to be set")
}

writer.Write(fakeEvents)
})

options := &gerrit.EventsLogOptions{From: from}
_, _, err := testClient.EventsLog.GetEvents(options)
if err != nil {
t.Error(err)
}
}

func TestEventsLogService_GetEvents_DateRangeToOnly(t *testing.T) {
setup()
defer teardown()

to := time.Now()

testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()

toFormat := to.Format("2006-01-02 15:04:05")
if query.Get("t2") != toFormat {
t.Errorf("%s != %s", query.Get("t2"), toFormat)
}

if query.Get("t1") != "" {
t.Error("Did not expect t1 to be set")
}

writer.Write(fakeEvents)
})

options := &gerrit.EventsLogOptions{To: to}
_, _, err := testClient.EventsLog.GetEvents(options)
if err != nil {
t.Error(err)
}
}
28 changes: 19 additions & 9 deletions gerrit.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@ type Client struct {
// Gerrit service for authentication
Authentication *AuthenticationService

// Services used for talking to different parts of the Gerrit API.
// Services used for talking to different parts of the standard
// Gerrit API.
Access *AccessService
Accounts *AccountsService
Changes *ChangesService
Config *ConfigService
Groups *GroupsService
Plugins *PluginsService
Projects *ProjectsService

// Additional services used for talking to non-standard Gerrit
// APIs.
EventsLog *EventsLogService
}

// Response is a Gerrit API response.
Expand Down Expand Up @@ -74,6 +79,7 @@ func NewClient(gerritURL string, httpClient *http.Client) (*Client, error) {
c.Groups = &GroupsService{client: c}
c.Plugins = &PluginsService{client: c}
c.Projects = &ProjectsService{client: c}
c.EventsLog = &EventsLogService{client: c}

return c, nil
}
Expand Down Expand Up @@ -265,18 +271,22 @@ func (c *Client) DeleteRequest(urlStr string, body interface{}) (*Response, erro
return c.Do(req, nil)
}

// RemoveMagicPrefixLine removed the "magic prefix line" of Gerris JSON response.
// the JSON response body starts with a magic prefix line that must be stripped before feeding the rest of the response body to a JSON parser.
// The reason for this is to prevent against Cross Site Script Inclusion (XSSI) attacks.
// RemoveMagicPrefixLine removes the "magic prefix line" of Gerris JSON
// response if present. The JSON response body starts with a magic prefix line
// that must be stripped before feeding the rest of the response body to a JSON
// parser. The reason for this is to prevent against Cross Site Script
// Inclusion (XSSI) attacks. By default all standard Gerrit APIs include this
// prefix line though some plugins may not.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
func RemoveMagicPrefixLine(body []byte) []byte {
index := bytes.IndexByte(body, '\n')
if index > -1 {
// +1 to catch the \n as well
body = body[(index + 1):]
if bytes.HasPrefix(body, []byte(")]}'\n")) {
index := bytes.IndexByte(body, '\n')
if index > -1 {
// +1 to catch the \n as well
body = body[(index + 1):]
}
}
Copy link
Collaborator

@dmitshur dmitshur May 16, 2017

Choose a reason for hiding this comment

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

This code can be significantly simplified without changing behavior.

If body has ")]}'\n" prefix, there's no need to use bytes.IndexByte to find the first '\n' character, it's known to be at index 4 because bytes.HasPrefix(body, []byte(")]}'\n")) was true.

Also, since this is called often, it's probably a good idea to factor out []byte(")]}'\n") into a single package scope variable, instead of potentially allocating once per RemoveMagicPrefixLine call.

func RemoveMagicPrefixLine(body []byte) []byte {
	if bytes.HasPrefix(body, magicPrefix) {
		return body[5:]
	}
	return body
}

var magicPrefix = []byte(")]}'\n")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shurcooL, no issues here if you want to go ahead and make the changes. I'd do it myself but I don't currently have my local host setup to make the change and test it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@opalmer I already made a PR that applies this suggestion, see #36.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah my bad sorry about that. Reviewed, thanks!

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks both of you @opalmer and @shurcooL


return body
}

Expand Down
15 changes: 15 additions & 0 deletions gerrit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,18 @@ func TestRemoveMagicPrefixLine(t *testing.T) {
}
}
}

func TestRemoveMagicPrefixLineDoesNothingWithoutPrefix(t *testing.T) {
mockData := []struct {
Current, Expected []byte
}{
{[]byte(`{"A":"a"}`), []byte(`{"A":"a"}`)},
{[]byte(`{"A":"a"}`), []byte(`{"A":"a"}`)},
}
for _, mock := range mockData {
body := gerrit.RemoveMagicPrefixLine(mock.Current)
if !reflect.DeepEqual(body, mock.Expected) {
t.Errorf("Response body = %v, want %v", body, mock.Expected)
}
}
}