فهرست منبع

Merge pull request #40 from vmarkovtsev/master

Add FileHistory analysis
Vadim Markovtsev 7 سال پیش
والد
کامیت
9e5a71353b
5فایلهای تغییر یافته به همراه312 افزوده شده و 4 حذف شده
  1. 1 0
      .gitignore
  2. 2 4
      burndown.go
  3. 137 0
      file_history.go
  4. 164 0
      file_history_test.go
  5. 8 0
      pb/pb.proto

+ 1 - 0
.gitignore

@@ -1,6 +1,7 @@
 cmd/hercules/plugin_template_source.go
 contrib/_plugin_example/churn_analysis.pb.go
 pb/pb.pb.go
+pb/pb_pb2.py
 
 **/.DS_Store
 .idea

+ 2 - 4
burndown.go

@@ -204,10 +204,8 @@ func (analyser *BurndownAnalysis) Consume(deps map[string]interface{}) (map[stri
 	treeDiffs := deps[DependencyTreeChanges].(object.Changes)
 	fileDiffs := deps[DependencyFileDiff].(map[string]FileDiffData)
 	for _, change := range treeDiffs {
-		action, err := change.Action()
-		if err != nil {
-			return nil, err
-		}
+		action, _ := change.Action()
+		var err error
 		switch action {
 		case merkletrie.Insert:
 			err = analyser.handleInsertion(change, author, cache)

+ 137 - 0
file_history.go

@@ -0,0 +1,137 @@
+package hercules
+
+import (
+  "fmt"
+  "io"
+  "sort"
+  "strings"
+
+  "github.com/gogo/protobuf/proto"
+  "gopkg.in/src-d/go-git.v4"
+  "gopkg.in/src-d/go-git.v4/plumbing"
+  "gopkg.in/src-d/go-git.v4/plumbing/object"
+  "gopkg.in/src-d/go-git.v4/utils/merkletrie"
+  "gopkg.in/src-d/hercules.v3/pb"
+)
+
+// FileHistory contains the intermediate state which is mutated by Consume(). It should implement
+// LeafPipelineItem.
+type FileHistory struct {
+  files map[string][]plumbing.Hash
+}
+
+// FileHistoryResult is returned by Finalize() and represents the analysis result.
+type FileHistoryResult struct {
+  Files map[string][]plumbing.Hash
+}
+
+func (history *FileHistory) Name() string {
+  return "FileHistory"
+}
+
+func (history *FileHistory) Provides() []string {
+  return []string{}
+}
+
+func (history *FileHistory) Requires() []string {
+  arr := [...]string{DependencyTreeChanges}
+  return arr[:]
+}
+
+func (history *FileHistory) ListConfigurationOptions() []ConfigurationOption {
+  return []ConfigurationOption{}
+}
+
+func (history *FileHistory) Flag() string {
+  return "file-history"
+}
+
+func (history *FileHistory) Configure(facts map[string]interface{}) {
+}
+
+// Initialize resets the internal temporary data structures and prepares the object for Consume().
+func (history *FileHistory) Initialize(repository *git.Repository) {
+  history.files = map[string][]plumbing.Hash{}
+}
+
+// Consume is called for every commit in the sequence.
+func (history *FileHistory) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
+  commit := deps["commit"].(*object.Commit).Hash
+  changes := deps[DependencyTreeChanges].(object.Changes)
+  for _, change := range changes {
+    action, _ := change.Action()
+    switch action {
+		case merkletrie.Insert:
+			hashes := make([]plumbing.Hash, 1)
+      hashes[0] = commit
+      history.files[change.To.Name] = hashes
+		case merkletrie.Delete:
+			delete(history.files, change.From.Name)
+		case merkletrie.Modify:
+			hashes := history.files[change.From.Name]
+      if change.From.Name != change.To.Name {
+        delete(history.files, change.From.Name)
+      }
+      hashes = append(hashes, commit)
+      history.files[change.To.Name] = hashes
+		}
+  }
+  return nil, nil
+}
+
+func (history *FileHistory) Finalize() interface{} {
+  return FileHistoryResult{Files: history.files}
+}
+
+// Serialize converts the result from Finalize() to either Protocol Buffers or YAML.
+func (history *FileHistory) Serialize(result interface{}, binary bool, writer io.Writer) error {
+  historyResult := result.(FileHistoryResult)
+  if binary {
+    return history.serializeBinary(&historyResult, writer)
+  }
+  history.serializeText(&historyResult, writer)
+  return nil
+}
+
+func (history *FileHistory) serializeText(result *FileHistoryResult, writer io.Writer) {
+  keys := make([]string, len(result.Files))
+  i := 0
+  for key := range result.Files {
+    keys[i] = key
+    i++
+  }
+  sort.Strings(keys)
+  for _, key := range keys {
+    hashes := result.Files[key]
+    strhashes := make([]string, len(hashes))
+    for i, hash := range hashes {
+      strhashes[i] = "\"" + hash.String() + "\""
+    }
+    fmt.Fprintf(writer, "  - %s: [%s]\n", key, strings.Join(strhashes, ","))
+  }
+}
+
+func (history *FileHistory) serializeBinary(result *FileHistoryResult, writer io.Writer) error {
+  message := pb.FileHistoryResultMessage{
+    Files: map[string]*pb.FileHistory{},
+  }
+  for key, vals := range result.Files {
+    hashes := &pb.FileHistory{
+      Commits: make([]string, len(vals)),
+    }
+    for i, hash := range vals {
+      hashes.Commits[i] = hash.String()
+    }
+    message.Files[key] = hashes
+  }
+  serialized, err := proto.Marshal(&message)
+  if err != nil {
+    return err
+  }
+  writer.Write(serialized)
+  return nil
+}
+
+func init() {
+  Registry.Register(&FileHistory{})
+}

+ 164 - 0
file_history_test.go

@@ -0,0 +1,164 @@
+package hercules
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/gogo/protobuf/proto"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+	"gopkg.in/src-d/hercules.v3/pb"
+)
+
+func fixtureFileHistory() *FileHistory {
+	fh := FileHistory{}
+	fh.Initialize(testRepository)
+	return &fh
+}
+
+func TestFileHistoryMeta(t *testing.T) {
+	fh := fixtureFileHistory()
+	assert.Equal(t, fh.Name(), "FileHistory")
+	assert.Equal(t, len(fh.Provides()), 0)
+	assert.Equal(t, len(fh.Requires()), 1)
+	assert.Equal(t, fh.Requires()[0], DependencyTreeChanges)
+	assert.Len(t, fh.ListConfigurationOptions(), 0)
+	fh.Configure(nil)
+}
+
+func TestFileHistoryRegistration(t *testing.T) {
+	tp, exists := Registry.registered[(&FileHistory{}).Name()]
+	assert.True(t, exists)
+	assert.Equal(t, tp.Elem().Name(), "FileHistory")
+	tp, exists = Registry.flags[(&FileHistory{}).Flag()]
+	assert.True(t, exists)
+	assert.Equal(t, tp.Elem().Name(), "FileHistory")
+}
+
+func TestFileHistoryConsume(t *testing.T) {
+  fh := fixtureFileHistory()
+	deps := map[string]interface{}{}
+	changes := make(object.Changes, 3)
+	treeFrom, _ := testRepository.TreeObject(plumbing.NewHash(
+		"a1eb2ea76eb7f9bfbde9b243861474421000eb96"))
+	treeTo, _ := testRepository.TreeObject(plumbing.NewHash(
+		"994eac1cd07235bb9815e547a75c84265dea00f5"))
+	changes[0] = &object.Change{From: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeFrom,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("dc248ba2b22048cc730c571a748e8ffcf7085ab9"),
+		},
+	}, To: object.ChangeEntry{
+		Name: "analyser.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "analyser.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("baa64828831d174f40140e4b3cfa77d1e917a2c1"),
+		},
+	}}
+	changes[1] = &object.Change{To: object.ChangeEntry{}, From: object.ChangeEntry{
+		Name: "cmd/hercules/main.go",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: "cmd/hercules/main.go",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("c29112dbd697ad9b401333b80c18a63951bc18d9"),
+		},
+	},
+	}
+	changes[2] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: ".travis.yml",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: ".travis.yml",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+		},
+	},
+	}
+	deps[DependencyTreeChanges] = changes
+	commit, _ := testRepository.CommitObject(plumbing.NewHash(
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	deps["commit"] = commit
+	fh.files["cmd/hercules/main.go"] = []plumbing.Hash{plumbing.NewHash(
+		"0000000000000000000000000000000000000000")}
+	fh.files["analyser.go"] = []plumbing.Hash{plumbing.NewHash(
+		"ffffffffffffffffffffffffffffffffffffffff")}
+	fh.Consume(deps)
+	assert.Len(t, fh.files, 2)
+	assert.Nil(t, fh.files["cmd/hercules/main.go"])
+	assert.Len(t, fh.files["analyser.go"], 2)
+	assert.Equal(t, fh.files["analyser.go"][0], plumbing.NewHash(
+		"ffffffffffffffffffffffffffffffffffffffff"))
+	assert.Equal(t, fh.files["analyser.go"][1], plumbing.NewHash(
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	assert.Len(t, fh.files[".travis.yml"], 1)
+	assert.Equal(t, fh.files[".travis.yml"][0], plumbing.NewHash(
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	res := fh.Finalize().(FileHistoryResult)
+	assert.Equal(t, fh.files, res.Files)
+}
+
+func TestFileHistorySerializeText(t *testing.T) {
+  fh := fixtureFileHistory()
+	deps := map[string]interface{}{}
+	changes := make(object.Changes, 1)
+	treeTo, _ := testRepository.TreeObject(plumbing.NewHash(
+		"994eac1cd07235bb9815e547a75c84265dea00f5"))
+	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: ".travis.yml",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: ".travis.yml",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+		},
+	},
+	}
+	deps[DependencyTreeChanges] = changes
+	commit, _ := testRepository.CommitObject(plumbing.NewHash(
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	deps["commit"] = commit
+	fh.Consume(deps)
+	res := fh.Finalize().(FileHistoryResult)
+	buffer := &bytes.Buffer{}
+	fh.Serialize(res, false, buffer)
+	assert.Equal(t, buffer.String(), "  - .travis.yml: [\"2b1ed978194a94edeabbca6de7ff3b5771d4d665\"]\n")
+}
+
+func TestFileHistorySerializeBinary(t *testing.T) {
+  fh := fixtureFileHistory()
+	deps := map[string]interface{}{}
+	changes := make(object.Changes, 1)
+	treeTo, _ := testRepository.TreeObject(plumbing.NewHash(
+		"994eac1cd07235bb9815e547a75c84265dea00f5"))
+	changes[0] = &object.Change{From: object.ChangeEntry{}, To: object.ChangeEntry{
+		Name: ".travis.yml",
+		Tree: treeTo,
+		TreeEntry: object.TreeEntry{
+			Name: ".travis.yml",
+			Mode: 0100644,
+			Hash: plumbing.NewHash("291286b4ac41952cbd1389fda66420ec03c1a9fe"),
+		},
+	},
+	}
+	deps[DependencyTreeChanges] = changes
+	commit, _ := testRepository.CommitObject(plumbing.NewHash(
+		"2b1ed978194a94edeabbca6de7ff3b5771d4d665"))
+	deps["commit"] = commit
+	fh.Consume(deps)
+	res := fh.Finalize().(FileHistoryResult)
+	buffer := &bytes.Buffer{}
+	fh.Serialize(res, true, buffer)
+	msg := pb.FileHistoryResultMessage{}
+	proto.Unmarshal(buffer.Bytes(), &msg)
+	assert.Len(t, msg.Files, 1)
+	assert.Len(t, msg.Files[".travis.yml"].Commits, 1)
+	assert.Equal(t, msg.Files[".travis.yml"].Commits[0], "2b1ed978194a94edeabbca6de7ff3b5771d4d665")
+}
+

+ 8 - 0
pb/pb.proto

@@ -97,6 +97,14 @@ message ShotnessAnalysisResults {
     repeated ShotnessRecord records = 1;
 }
 
+message FileHistory {
+    repeated string commits = 1;
+}
+
+message FileHistoryResultMessage {
+    map<string, FileHistory> files = 1;
+}
+
 message AnalysisResults {
     Metadata header = 1;
     // the mapped values are dynamic messages which require the second parsing pass.