cmd/maintner: start of in-memory data structure & processing mutation log

Change-Id: Id48171287b23dd88ab14912bafb3a71e942a866e
Reviewed-on: https://go-review.googlesource.com/36896
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/maintner/maintner.go b/maintner/maintner.go
index abeea8a..f342af7 100644
--- a/maintner/maintner.go
+++ b/maintner/maintner.go
@@ -10,11 +10,86 @@
 // data to other tools is "maintnerd".
 package maintner
 
-import "context"
+import (
+	"context"
+	"sync"
+	"time"
+
+	"golang.org/x/build/maintner/maintpb"
+)
 
 // Corpus holds all of a project's metadata.
 type Corpus struct {
 	// ... TODO
+
+	mu           sync.RWMutex
+	githubIssues map[githubRepo]map[int]*githubIssue // repo -> num -> issue
+	githubUsers  map[int64]*githubUser
+}
+
+// githubRepo is a github org & repo, lowercase, joined by a '/',
+// such as "golang/go".
+type githubRepo string
+
+// githubUser represents a github user.
+// It is a subset of https://developer.github.com/v3/users/#get-a-single-user
+type githubUser struct {
+	ID       int64
+	Username string // what Github calls "Login"
+}
+
+// githubIssue represents a github issue.
+// See https://developer.github.com/v3/issues/#get-a-single-issue
+type githubIssue struct {
+	ID      int64
+	Number  int32
+	Closed  bool
+	User    *githubUser
+	Created time.Time
+	Updated time.Time
+	Body    string
+	// TODO Comments ...
+}
+
+// A MutationSource yields a log of mutations that will catch a corpus
+// back up to the present.
+type MutationSource interface {
+	// GetMutations returns a channel of mutations.
+	// The channel should be closed at the end.
+	// All sends on the returned channel should select
+	// on the provided context.
+	GetMutations(context.Context) <-chan *maintpb.Mutation
+}
+
+func (c *Corpus) processMutations(ctx context.Context, src MutationSource) error {
+	ch := src.GetMutations(ctx)
+	done := ctx.Done()
+
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	for {
+		select {
+		case <-done:
+			return ctx.Err()
+		case m, ok := <-ch:
+			if !ok {
+				return nil
+			}
+			c.processMutationLocked(m)
+		}
+	}
+}
+
+// c.mu must be held.
+func (c *Corpus) processMutationLocked(m *maintpb.Mutation) {
+	if im := m.GithubIssue; im != nil {
+		c.processGithubIssueMutation(im)
+	}
+	// TODO: more...
+}
+
+func (c *Corpus) processGithubIssueMutation(m *maintpb.GithubIssueMutation) {
+	// TODO: ...
 }
 
 // PopulateFromServer populates the corpus from a maintnerd server.