dmitri.shuralyov.com/service/change/maintner

Implement basic functionality with available data.
dmitshur committed 6 years ago commit 345df30e663312345caa0f4588bc6b2093a4d960
Showing partial commit. Full Commit
Collapse all
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