maintner: add GitCommit.HasAncestor method

maintnerd will have an API to query this in a subsequent CL.

Updates golang/go#20222

Change-Id: I3cb0ce2897eea782f627c2e27c4d02ef0eb6c66b
Reviewed-on: https://go-review.googlesource.com/43554
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Brad Fitzpatrick 2017-05-16 14:10:39 -07:00
Родитель cd5d4b37a9
Коммит b77708e47f
4 изменённых файлов: 154 добавлений и 8 удалений

Просмотреть файл

@ -339,7 +339,7 @@ func (gp *GerritProject) getGerritStatus(currentMeta, oldMeta *GitCommit) string
if len(commit.Parents) == 0 {
return "new"
}
parentHash := commit.Parents[0] // meta tree has no merge commits
parentHash := commit.Parents[0].Hash // meta tree has no merge commits
commit = c.gitCommit[parentHash]
if commit == nil {
gp.logf("getGerritStatus: did not find parent commit %s", parentHash)
@ -402,7 +402,7 @@ func (gp *GerritProject) processMutation(gm *maintpb.GerritMutation) {
delete(gp.need, gc.Hash)
}
for _, p := range gc.Parents {
gp.markNeededCommit(p)
gp.markNeededCommit(p.Hash)
}
}
}

Просмотреть файл

@ -54,11 +54,17 @@ func (c *Corpus) gitHashFromHex(s []byte) GitHash {
return GitHash(c.strb(buf[:20]))
}
// placeholderCommitter is a sentinel value for GitCommit.Committer to
// mean that the GitCommit is a placeholder. It's used for commits we
// know should exist (because they're referenced as parents) but we
// haven't yet seen in the log.
var placeholderCommitter = new(GitPerson)
// GitCommit represents a single commit in a git repository.
type GitCommit struct {
Hash GitHash
Tree GitHash
Parents []GitHash
Parents []*GitCommit
Author *GitPerson
AuthorTime time.Time
Committer *GitPerson
@ -67,6 +73,32 @@ type GitCommit struct {
Files []*maintpb.GitDiffTreeFile
}
// HasAncestor reports whether gc contains the provided ancestor
// commit in gc's history.
func (gc *GitCommit) HasAncestor(ancestor *GitCommit) bool {
return gc.hasAncestor(ancestor, make(map[*GitCommit]bool))
}
func (gc *GitCommit) hasAncestor(ancestor *GitCommit, checked map[*GitCommit]bool) bool {
if v, ok := checked[ancestor]; ok {
return v
}
checked[gc] = false
for _, pc := range gc.Parents {
if pc == nil {
panic("nil parent")
}
if pc.Committer == placeholderCommitter {
panic("found placeholder")
}
if pc.Hash == ancestor.Hash || pc.hasAncestor(ancestor, checked) {
checked[gc] = true
return true
}
}
return false
}
// GitPerson is a person in a git commit.
type GitPerson struct {
Str string // "Foo Bar <foo@bar.com>"
@ -266,6 +298,9 @@ func (c *Corpus) processGitMutation(m *maintpb.GitMutation) {
// c.mu is held for writing.
func (c *Corpus) processGitCommit(commit *maintpb.GitCommit) (*GitCommit, error) {
if c.gitCommit == nil {
c.gitCommit = map[GitHash]*GitCommit{}
}
if len(commit.Sha1) != 40 {
return nil, fmt.Errorf("bogus git sha1 %q", commit.Sha1)
}
@ -279,7 +314,7 @@ func (c *Corpus) processGitCommit(commit *maintpb.GitCommit) (*GitCommit, error)
hdr, msg := catFile[:i], catFile[i+2:]
gc := &GitCommit{
Hash: hash,
Parents: make([]GitHash, 0, bytes.Count(hdr, parentSpace)),
Parents: make([]*GitCommit, 0, bytes.Count(hdr, parentSpace)),
Msg: c.strb(msg),
}
if commit.DiffTree != nil {
@ -293,7 +328,16 @@ func (c *Corpus) processGitCommit(commit *maintpb.GitCommit) (*GitCommit, error)
if bytes.HasPrefix(ln, parentSpace) {
parents++
parentHash := c.gitHashFromHex(ln[len(parentSpace):])
gc.Parents = append(gc.Parents, parentHash)
parent := c.gitCommit[parentHash]
if parent == nil {
// Install a placeholder to be filled in later.
parent = &GitCommit{
Hash: parentHash,
Committer: placeholderCommitter,
}
c.gitCommit[parentHash] = parent
}
gc.Parents = append(gc.Parents, parent)
c.enqueueCommitLocked(parentHash)
return nil
}
@ -344,10 +388,12 @@ func (c *Corpus) processGitCommit(commit *maintpb.GitCommit) (*GitCommit, error)
log.Printf("Unparseable commit %q: %v", hash, err)
return nil, fmt.Errorf("Unparseable commit %q: %v", hash, err)
}
if c.gitCommit == nil {
c.gitCommit = map[GitHash]*GitCommit{}
if ph, ok := c.gitCommit[hash]; ok {
// Update placeholder.
*ph = *gc
} else {
c.gitCommit[hash] = gc
}
c.gitCommit[hash] = gc
if c.gitCommitTodo != nil {
delete(c.gitCommitTodo, hash)
}

Просмотреть файл

@ -6,7 +6,10 @@ package godata
import (
"context"
"sync"
"testing"
"golang.org/x/build/maintner"
)
func BenchmarkGet(b *testing.B) {
@ -18,3 +21,76 @@ func BenchmarkGet(b *testing.B) {
}
}
}
var (
corpusMu sync.Mutex
corpusCache *maintner.Corpus
)
func getGoData(tb testing.TB) *maintner.Corpus {
corpusMu.Lock()
defer corpusMu.Unlock()
if corpusCache != nil {
return corpusCache
}
var err error
corpusCache, err = Get(context.Background())
if err != nil {
tb.Fatalf("getting corpus: %v", err)
}
return corpusCache
}
func TestCorpusCheck(t *testing.T) {
c := getGoData(t)
if err := c.Check(); err != nil {
t.Fatal(err)
}
}
func TestGitAncestor(t *testing.T) {
c := getGoData(t)
tests := []struct {
subject, ancestor string
want bool
}{
{"3b5637ff2bd5c03479780995e7a35c48222157c1", "0bb0b61d6a85b2a1a33dcbc418089656f2754d32", true},
{"0bb0b61d6a85b2a1a33dcbc418089656f2754d32", "3b5637ff2bd5c03479780995e7a35c48222157c1", false},
// Same on both sides:
{"0bb0b61d6a85b2a1a33dcbc418089656f2754d32", "0bb0b61d6a85b2a1a33dcbc418089656f2754d32", false},
{"3b5637ff2bd5c03479780995e7a35c48222157c1", "3b5637ff2bd5c03479780995e7a35c48222157c1", false},
}
for i, tt := range tests {
subject := c.GitCommit(tt.subject)
if subject == nil {
t.Errorf("%d. missing subject commit %q", i, tt.subject)
continue
}
anc := c.GitCommit(tt.ancestor)
if anc == nil {
t.Errorf("%d. missing ancestor commit %q", i, tt.ancestor)
continue
}
got := subject.HasAncestor(anc)
if got != tt.want {
t.Errorf("HasAncestor(%q, %q) = %v; want %v", tt.subject, tt.ancestor, got, tt.want)
}
}
}
func BenchmarkGitAncestor(b *testing.B) {
c := getGoData(b)
subject := c.GitCommit("3b5637ff2bd5c03479780995e7a35c48222157c1")
anc := c.GitCommit("0bb0b61d6a85b2a1a33dcbc418089656f2754d32")
if subject == nil || anc == nil {
b.Fatal("missing commit(s)")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !subject.HasAncestor(anc) {
b.Fatal("wrong answer")
}
}
}

Просмотреть файл

@ -60,6 +60,15 @@ type Corpus struct {
zoneCache map[string]*time.Location // "+0530" => location
}
// RLock grabs the corpus's read lock. Grabbing the read lock prevents
// any concurrent writes from mutation the corpus. This is only
// necessary if the application is querying the corpus and calling its
// Update method concurrently.
func (c *Corpus) RLock() { c.mu.RLock() }
// RUnlock unlocks the corpus's read lock.
func (c *Corpus) RUnlock() { c.mu.RUnlock() }
type polledGitCommits struct {
repo *maintpb.GitRepo
dir string
@ -74,6 +83,7 @@ func (c *Corpus) EnableLeaderMode(logger MutationLogger, scratchDir string) {
c.dataDir = scratchDir
}
// SetVerbose enables or disables verbose logging.
func (c *Corpus) SetVerbose(v bool) { c.verbose = v }
func (c *Corpus) getDataDir() string {
@ -99,6 +109,20 @@ func (c *Corpus) Gerrit() *Gerrit {
return new(Gerrit)
}
// Check verifies the internal structure of the Corpus data structures.
// It is intended for tests and debugging.
func (c *Corpus) Check() error {
for hash, gc := range c.gitCommit {
if gc.Committer == placeholderCommitter {
return fmt.Errorf("git commit for key %q was placeholder", hash)
}
if gc.Hash != hash {
return fmt.Errorf("git commit for key %q had GitCommit.Hash %q", hash, gc.Hash)
}
}
return nil
}
// GitCommit returns the provided git commit, or nil if it's unknown.
func (c *Corpus) GitCommit(hash string) *GitCommit {
if len(hash) != 40 {