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

maintner: Implement basic functionality with available data.
dmitshur committed 2 years ago commit 345df30e663312345caa0f4588bc6b2093a4d960
maintner/maintner.go
@@ -1,39 +1,43 @@
 // Package maintner implements a read-only change.Service using
-// a x/build/maintner corpus that serves Gerrit changes.
+// a x/build/maintner corpus.
 package maintner
 
 import (
 	"context"
 	"fmt"
 	"log"
+	"os"
 	"sort"
 	"strings"
+	"unicode"
 
 	"dmitri.shuralyov.com/service/change"
 	"github.com/shurcooL/users"
 	"golang.org/x/build/maintner"
+	"sourcegraph.com/sourcegraph/go-diff/diff"
 )
 
-// NewService creates an change.Service backed with the given corpus.
-// However, it serves Gerrit changes, not GitHub issues.
+// NewService creates a change.Service backed with the given corpus.
 func NewService(corpus *maintner.Corpus) change.Service {
 	return service{
 		c: corpus,
 	}
 }
 
 type service struct {
 	c *maintner.Corpus
 }
 
-func (s service) List(ctx context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
-	// TODO: Pagination. Respect opt.Start and opt.Length, if given.
-
-	var is []change.Change
-
+func (s service) List(_ context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
+	s.c.RLock()
+	defer s.c.RUnlock()
 	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return nil, os.ErrNotExist
+	}
+	var is []change.Change
 	err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
 		if cl.Status == "" {
 			log.Printf("empty status for CL %d\n", cl.Number)
 			return nil
 		}
@@ -42,39 +46,39 @@ func (s service) List(ctx context.Context, repo string, opt change.ListOptions)
 		case opt.Filter == change.FilterOpen && state != change.OpenState:
 			return nil
 		case opt.Filter == change.FilterClosedMerged && !(state == change.ClosedState || state == change.MergedState):
 			return nil
 		}
-
 		is = append(is, change.Change{
 			ID:    uint64(cl.Number),
 			State: state,
 			Title: firstParagraph(cl.Commit.Msg),
 			//Labels: labels, // TODO.
 			Author:    gerritUser(cl.Commit.Author),
 			CreatedAt: cl.Created,
 			//Replies: len(cl.Messages),
 		})
-
 		return nil
 	})
 	if err != nil {
 		return nil, err
 	}
-
 	//sort.Sort(sort.Reverse(byID(is))) // For some reason, IDs don't completely line up with created times.
 	sort.Slice(is, func(i, j int) bool {
 		return is[i].CreatedAt.After(is[j].CreatedAt)
 	})
-
 	return is, nil
 }
 
 func (s service) Count(_ context.Context, repo string, opt change.ListOptions) (uint64, error) {
-	var count uint64
-
+	s.c.RLock()
+	defer s.c.RUnlock()
 	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return 0, os.ErrNotExist
+	}
+	var count uint64
 	err := project.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
 		if cl.Status == "" {
 			return nil
 		}
 		state := state(cl.Status)
@@ -82,34 +86,166 @@ func (s service) Count(_ context.Context, repo string, opt change.ListOptions) (
 		case opt.Filter == change.FilterOpen && state != change.OpenState:
 			return nil
 		case opt.Filter == change.FilterClosedMerged && !(state == change.ClosedState || state == change.MergedState):
 			return nil
 		}
-
 		count++
-
 		return nil
 	})
-	if err != nil {
-		return 0, err
+	return count, err
+}
+
+func (s service) Get(_ context.Context, repo string, id uint64) (change.Change, error) {
+	s.c.RLock()
+	defer s.c.RUnlock()
+	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return change.Change{}, os.ErrNotExist
 	}
+	cl := project.CL(int32(id))
+	if cl == nil || cl.Private {
+		return change.Change{}, os.ErrNotExist
+	}
+	return change.Change{
+		ID:    uint64(cl.Number),
+		State: state(cl.Status),
+		Title: firstParagraph(cl.Commit.Msg),
+		//Labels: labels, // TODO.
+		Author:    gerritUser(cl.Commit.Author),
+		CreatedAt: cl.Created,
+		//Replies: len(cl.Messages),
+		Commits: int(cl.Version),
+	}, nil
+}
 
-	return count, nil
+func (s service) ListTimeline(_ context.Context, repo string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
+	s.c.RLock()
+	defer s.c.RUnlock()
+	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return nil, os.ErrNotExist
+	}
+	cl := project.CL(int32(id))
+	if cl == nil || cl.Private {
+		return nil, os.ErrNotExist
+	}
+	var timeline []interface{}
+	for _, m := range cl.Messages {
+		label, body, ok := parseMessage(m.Message)
+		if !ok {
+			continue
+		}
+		var state change.ReviewState
+		switch label {
+		default:
+			state = change.Commented
+		case "Code-Review+2":
+			state = change.Approved
+		case "Code-Review-2":
+			state = change.ChangesRequested
+		}
+		timeline = append(timeline, change.Review{
+			User:      gerritUser(m.Author),
+			CreatedAt: m.Date,
+			State:     state,
+			Body:      body,
+		})
+	}
+	return timeline, nil
 }
 
-func (s service) Get(ctx context.Context, _ string, id uint64) (change.Change, error) {
-	// TODO.
-	return change.Change{}, fmt.Errorf("Get: not implemented")
+func parseMessage(m string) (label string, body string, ok bool) {
+	// "Patch Set ".
+	if !strings.HasPrefix(m, "Patch Set ") {
+		return "", "", false
+	}
+	m = m[len("Patch Set "):]
+
+	// "123".
+	i := strings.IndexFunc(m, func(c rune) bool { return !unicode.IsNumber(c) })
+	if i == -1 {
+		return "", "", false
+	}
+	m = m[i:]
+
+	// ":".
+	if len(m) < 1 || m[0] != ':' {
+		return "", "", false
+	}
+	m = m[1:]
+
+	switch i = strings.IndexByte(m, '\n'); i {
+	case -1:
+		label = m
+	default:
+		label = m[:i]
+		body = m[i+1:]
+	}
+
+	if label != "" {
+		// " ".
+		if len(label) < 1 || label[0] != ' ' {
+			return "", "", false
+		}
+		label = label[1:]
+	}
+
+	if body != "" {
+		// "\n".
+		if len(body) < 1 || body[0] != '\n' {
+			return "", "", false
+		}
+		body = body[1:]
+	}
+
+	return label, body, true
 }
 
-func (s service) ListCommits(ctx context.Context, _ string, id uint64) ([]change.Commit, error) {
-	return nil, fmt.Errorf("ListCommits: not implemented")
+func (s service) ListCommits(_ context.Context, repo string, id uint64) ([]change.Commit, error) {
+	s.c.RLock()
+	defer s.c.RUnlock()
+	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return nil, os.ErrNotExist
+	}
+	cl := project.CL(int32(id))
+	if cl == nil || cl.Private {
+		return nil, os.ErrNotExist
+	}
+	commits := make([]change.Commit, int(cl.Version))
+	for n := int32(1); n <= cl.Version; n++ {
+		c := cl.CommitAtVersion(n)
+		commits[n-1] = change.Commit{
+			SHA:        c.Hash.String(),
+			Message:    fmt.Sprintf("Patch Set %d", n),
+			Author:     gerritUser(c.Author),
+			AuthorTime: c.AuthorTime,
+		}
+	}
+	return commits, nil
 }
 
-func (s service) GetDiff(ctx context.Context, _ string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
-	// TODO.
-	return nil, fmt.Errorf("GetDiff: not implemented")
+func (s service) GetDiff(_ context.Context, repo string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
+	s.c.RLock()
+	defer s.c.RUnlock()
+	project := s.c.Gerrit().Project(serverProject(repo))
+	if project == nil {
+		return nil, os.ErrNotExist
+	}
+	cl := project.CL(int32(id))
+	if cl == nil || cl.Private {
+		return nil, os.ErrNotExist
+	}
+	var fds []*diff.FileDiff
+	for _, f := range cl.Commit.Files {
+		fds = append(fds, &diff.FileDiff{
+			OrigName: f.File,
+			NewName:  f.File,
+			Hunks:    []*diff.Hunk{}, // Hunk data isn't present in maintner.Corpus.
+		})
+	}
+	return diff.PrintMultiFileDiff(fds)
 }
 
 func state(status string) change.State {
 	switch status {
 	case "new":
@@ -123,15 +259,10 @@ func state(status string) change.State {
 	default:
 		panic(fmt.Errorf("unrecognized status %q", status))
 	}
 }
 
-func (s service) ListTimeline(ctx context.Context, _ string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
-	// TODO.
-	return nil, fmt.Errorf("ListTimeline: not implemented")
-}
-
 func gerritUser(user *maintner.GitPerson) users.User {
 	return users.User{
 		UserSpec: users.UserSpec{
 			ID:     0,  // TODO.
 			Domain: "", // TODO.
@@ -149,26 +280,10 @@ func serverProject(repo string) (server, project string) {
 		return "", ""
 	}
 	return repo[:i], repo[i+1:]
 }
 
-func server(repo string) string {
-	i := strings.IndexByte(repo, '/')
-	if i == -1 {
-		return ""
-	}
-	return repo[:i]
-}
-
-func project(repo string) string {
-	i := strings.IndexByte(repo, '/')
-	if i == -1 {
-		return ""
-	}
-	return repo[i+1:]
-}
-
 // firstParagraph returns the first paragraph of text s.
 func firstParagraph(s string) string {
 	i := strings.Index(s, "\n\n")
 	if i == -1 {
 		return s