dmitri.shuralyov.com/service/change/...

gerritapi: parse inline comments out of new patchset messages

In Gerrit, it's possible to publish inline comments on patchset push.
Such messages look like this:

	Uploaded patch set 2.

	(3 comments)

Parse and display such comments.
dmitshur committed 5 years ago commit 3bb9b99b3801c03b70ee3c606264f20b2842eafa
Collapse all
gerritapi/gerritapi.go
@@ -5,10 +5,11 @@ import (
	"context"
	"fmt"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"dmitri.shuralyov.com/service/change"
	"github.com/andygrunwald/go-gerrit"
@@ -285,10 +286,41 @@ func (s service) ListTimeline(ctx context.Context, repo string, id uint64, opt *
					Payload: change.MergedEvent{
						CommitID: message.Message[46:86], // TODO: Make safer.
						RefName:  chg.Branch,
					},
				})
			case "gerrit:newPatchSet":
				// Parse a new patchset message, check if it has comments.
				body, err := parsePSMessage(message.Message, message.RevisionNumber)
				if err != nil {
					return nil, err
				}
				if body == "" {
					// No body means no comments.
					break
				}
				var cs []change.InlineComment
				for file, comments := range *comments {
					for _, c := range comments {
						if c.Updated.Equal(message.Date.Time) {
							cs = append(cs, change.InlineComment{
								File: file,
								Line: c.Line,
								Body: c.Message,
							})
						}
					}
				}
				timeline = append(timeline, change.Review{
					ID:        fmt.Sprint(idx), // TODO: message.ID is not uint64; e.g., "bfba753d015916303152305cee7152ea7a112fe0".
					User:      s.gerritUser(message.Author),
					CreatedAt: message.Date.Time,
					State:     change.Commented,
					Body:      body,
					Editable:  false,
					Comments:  cs,
				})
			}
			continue
		}
		labels, body, ok := parseMessage(message.Message)
		if !ok {
@@ -364,10 +396,57 @@ func parseMessage(m string) (labels string, body string, ok bool) {
	}

	return labels, body, true
}

// parsePSMessage parses an autogenerated:gerrit:newPatchSet
// message and returns its body, if any.
func parsePSMessage(m string, revisionNumber int) (body string, _ error) {
	// "Uploaded patch set ".
	if !strings.HasPrefix(m, "Uploaded patch set ") {
		return "", fmt.Errorf("unexpected format")
	}
	m = m[len("Uploaded patch set "):]

	// Revision number, e.g., "123".
	i := matchNumber(m, revisionNumber)
	if i == -1 {
		return "", fmt.Errorf("unexpected format")
	}
	m = m[i:]

	// ".".
	if len(m) < 1 || m[0] != '.' {
		return "", fmt.Errorf("unexpected format")
	}
	m = m[1:]

	if m == "" {
		// No body.
		return "", nil
	}

	// "\n\n".
	if !strings.HasPrefix(m, "\n\n") {
		return "", fmt.Errorf("unexpected format")
	}
	m = m[len("\n\n"):]

	// The remainer is the body.
	return m, nil
}

// matchNumber returns the index after number in s,
// or -1 if number is not immediately present in s.
func matchNumber(s string, number int) int {
	a := strconv.Itoa(number)
	if !strings.HasPrefix(s, a) {
		return -1
	}
	return len(a)
}

func reviewState(labels string) change.ReviewState {
	for _, label := range strings.Split(labels, " ") {
		switch label {
		case "Code-Review+2":
			return change.Approved
gerritapi/gerritapi_test.go
@@ -42,5 +42,43 @@ func TestParseMessage(t *testing.T) {
		if gotBody != tc.wantBody {
			t.Errorf("%d: got body: %q, want: %q", i, gotBody, tc.wantBody)
		}
	}
}

func TestParsePSMessage(t *testing.T) {
	tests := []struct {
		inMessage        string
		inRevisionNumber int
		wantBody         string
		wantError        bool
	}{
		{
			inMessage:        "Uploaded patch set 1.",
			inRevisionNumber: 1,
			wantBody:         "",
		},
		{
			inMessage:        "Uploaded patch set 2.\n\n(3 comments)",
			inRevisionNumber: 2,
			wantBody:         "(3 comments)",
		},
		{
			inMessage:        "something unexpected",
			inRevisionNumber: 3,
			wantError:        true,
		},
	}
	for i, tc := range tests {
		body, err := parsePSMessage(tc.inMessage, tc.inRevisionNumber)
		if got, want := err != nil, tc.wantError; got != want {
			t.Errorf("%d: got error: %v, want: %v", i, got, want)
			continue
		}
		if tc.wantError {
			continue
		}
		if got, want := body, tc.wantBody; got != want {
			t.Errorf("%d: got body: %q, want: %q", i, got, want)
		}
	}
}