MM-21722 - Repository synchronization tool (#86)
This commit is contained in:
parent
4e7a2d3734
commit
0688e8df4c
18 changed files with 1589 additions and 0 deletions
106
build/sync/plan/git/file_history.go
Normal file
106
build/sync/plan/git/file_history.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
git "gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
)
|
||||
|
||||
// ErrNotFound signifies the file was not found.
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
|
||||
// FileHistory will trace all the versions of a file in the git repository
|
||||
// and return a list of sha1 hashes of that file.
|
||||
func FileHistory(path string, repo *git.Repository) ([]string, error) {
|
||||
logOpts := git.LogOptions{
|
||||
FileName: &path,
|
||||
}
|
||||
commits, err := repo.Log(&logOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commits for path %q: %v", path, err)
|
||||
}
|
||||
defer commits.Close()
|
||||
|
||||
hashHistory := []string{}
|
||||
cerr := commits.ForEach(func(c *object.Commit) error {
|
||||
root, err := repo.TreeObject(c.TreeHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get commit tree: %v", err)
|
||||
}
|
||||
f, err := traverseTree(root, path)
|
||||
if err == object.ErrFileNotFound || err == object.ErrDirectoryNotFound {
|
||||
// Ignoring file not found errors.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
sum, err := getReaderHash(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashHistory = append(hashHistory, sum)
|
||||
return nil
|
||||
})
|
||||
if cerr != nil && cerr != io.EOF {
|
||||
return nil, cerr
|
||||
}
|
||||
if len(hashHistory) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return hashHistory, nil
|
||||
}
|
||||
|
||||
func traverseTree(root *object.Tree, path string) (io.ReadCloser, error) {
|
||||
dirName, fileName := filepath.Split(path)
|
||||
var err error
|
||||
t := root
|
||||
if dirName != "" {
|
||||
t, err = root.Tree(filepath.Clean(dirName))
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to traverse tree to %q: %v", dirName, err)
|
||||
}
|
||||
}
|
||||
f, err := t.File(fileName)
|
||||
if err == object.ErrFileNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup file %q: %v", fileName, err)
|
||||
}
|
||||
reader, err := f.Reader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open %q: %v", path, err)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func getReaderHash(r io.Reader) (string, error) {
|
||||
h := sha1.New() // nolint
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// GetFileHash calculates the sha1 hash sum of the file.
|
||||
func GetFileHash(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
sum, err := getReaderHash(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sum, nil
|
||||
}
|
27
build/sync/plan/git/file_history_test.go
Normal file
27
build/sync/plan/git/file_history_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package git_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
git "gopkg.in/src-d/go-git.v4"
|
||||
|
||||
gitutil "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
|
||||
)
|
||||
|
||||
func TestFileHistory(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
repo, err := git.PlainOpenWithOptions("./", &git.PlainOpenOptions{
|
||||
DetectDotGit: true,
|
||||
})
|
||||
assert.Nil(err)
|
||||
sums, err := gitutil.FileHistory("build/sync/plan/git/testdata/testfile.txt", repo)
|
||||
assert.Nil(err)
|
||||
assert.Equal([]string{"ba7192052d7cf77c55d3b7bf40b350b8431b208b"}, sums)
|
||||
|
||||
// Calling with a non-existent file returns error.
|
||||
sums, err = gitutil.FileHistory("build/sync/plan/git/testdata/nosuch_testfile.txt", repo)
|
||||
assert.Equal(gitutil.ErrNotFound, err)
|
||||
assert.Nil(sums)
|
||||
}
|
1
build/sync/plan/git/testdata/testfile.txt
vendored
Normal file
1
build/sync/plan/git/testdata/testfile.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
This file is used to test file history tracking.
|
Reference in a new issue