@@ -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