aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--COMPATIBILITY.md2
-rw-r--r--plumbing/format/diff/unified_encoder.go5
-rw-r--r--plumbing/format/diff/unified_encoder_test.go2
-rw-r--r--plumbing/object/patch.go5
-rw-r--r--plumbing/object/patch_test.go44
-rw-r--r--storage/filesystem/internal/dotgit/dotgit.go45
-rw-r--r--storage/filesystem/internal/dotgit/dotgit_test.go53
-rw-r--r--storage/filesystem/object.go21
-rw-r--r--worktree_test.go30
9 files changed, 200 insertions, 7 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 4464ed8..5a7f0f0 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -56,7 +56,7 @@ is supported by go-git.
| svn | ✖ |
| fast-import | ✖ |
| **administration** |
-| clean | ✖ |
+| clean | ✔ |
| gc | ✖ |
| fsck | ✖ |
| reflog | ✖ |
diff --git a/plumbing/format/diff/unified_encoder.go b/plumbing/format/diff/unified_encoder.go
index a4ff7ab..cf2a34b 100644
--- a/plumbing/format/diff/unified_encoder.go
+++ b/plumbing/format/diff/unified_encoder.go
@@ -2,7 +2,6 @@ package diff
import (
"bytes"
- "errors"
"fmt"
"io"
"strings"
@@ -45,8 +44,6 @@ const (
DefaultContextLines = 3
)
-var ErrBothFilesEmpty = errors.New("both files are empty")
-
// UnifiedEncoder encodes an unified diff into the provided Writer.
// There are some unsupported features:
// - Similarity index for renames
@@ -106,7 +103,7 @@ func (e *UnifiedEncoder) printMessage(message string) {
func (e *UnifiedEncoder) header(from, to File, isBinary bool) error {
switch {
case from == nil && to == nil:
- return ErrBothFilesEmpty
+ return nil
case from != nil && to != nil:
hashEquals := from.Hash() == to.Hash()
diff --git a/plumbing/format/diff/unified_encoder_test.go b/plumbing/format/diff/unified_encoder_test.go
index b832920..6e12070 100644
--- a/plumbing/format/diff/unified_encoder_test.go
+++ b/plumbing/format/diff/unified_encoder_test.go
@@ -20,7 +20,7 @@ func (s *UnifiedEncoderTestSuite) TestBothFilesEmpty(c *C) {
buffer := bytes.NewBuffer(nil)
e := NewUnifiedEncoder(buffer, 1)
err := e.Encode(testPatch{filePatches: []testFilePatch{{}}})
- c.Assert(err, Equals, ErrBothFilesEmpty)
+ c.Assert(err, IsNil)
}
func (s *UnifiedEncoderTestSuite) TestBinaryFile(c *C) {
diff --git a/plumbing/object/patch.go b/plumbing/object/patch.go
index a920631..aa96a96 100644
--- a/plumbing/object/patch.go
+++ b/plumbing/object/patch.go
@@ -271,6 +271,11 @@ func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
var fileStats FileStats
for _, fp := range filePatches {
+ // ignore empty patches (binary files, submodule refs updates)
+ if len(fp.Chunks()) == 0 {
+ continue
+ }
+
cs := FileStat{}
from, to := fp.Files()
if from == nil {
diff --git a/plumbing/object/patch_test.go b/plumbing/object/patch_test.go
new file mode 100644
index 0000000..8eb65ec
--- /dev/null
+++ b/plumbing/object/patch_test.go
@@ -0,0 +1,44 @@
+package object
+
+import (
+ . "gopkg.in/check.v1"
+ fixtures "gopkg.in/src-d/go-git-fixtures.v3"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
+)
+
+type PatchSuite struct {
+ BaseObjectsSuite
+}
+
+var _ = Suite(&PatchSuite{})
+
+func (s *PatchSuite) TestStatsWithSubmodules(c *C) {
+ storer, err := filesystem.NewStorage(
+ fixtures.ByURL("https://github.com/git-fixtures/submodule.git").One().DotGit())
+
+ commit, err := GetCommit(storer, plumbing.NewHash("b685400c1f9316f350965a5993d350bc746b0bf4"))
+
+ tree, err := commit.Tree()
+ c.Assert(err, IsNil)
+
+ e, err := tree.entry("basic")
+ c.Assert(err, IsNil)
+
+ ch := &Change{
+ From: ChangeEntry{
+ Name: "basic",
+ Tree: tree,
+ TreeEntry: *e,
+ },
+ To: ChangeEntry{
+ Name: "basic",
+ Tree: tree,
+ TreeEntry: *e,
+ },
+ }
+
+ p, err := getPatch("", ch)
+ c.Assert(err, IsNil)
+ c.Assert(p, NotNil)
+}
diff --git a/storage/filesystem/internal/dotgit/dotgit.go b/storage/filesystem/internal/dotgit/dotgit.go
index 11b2512..ebc6bad 100644
--- a/storage/filesystem/internal/dotgit/dotgit.go
+++ b/storage/filesystem/internal/dotgit/dotgit.go
@@ -8,9 +8,11 @@ import (
"io"
stdioutil "io/ioutil"
"os"
+ "path/filepath"
"strings"
"time"
+ "gopkg.in/src-d/go-billy.v4/osfs"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/utils/ioutil"
@@ -756,11 +758,52 @@ func (d *DotGit) PackRefs() (err error) {
return nil
}
-// Module return a billy.Filesystem poiting to the module folder
+// Module return a billy.Filesystem pointing to the module folder
func (d *DotGit) Module(name string) (billy.Filesystem, error) {
return d.fs.Chroot(d.fs.Join(modulePath, name))
}
+// Alternates returns DotGit(s) based off paths in objects/info/alternates if
+// available. This can be used to checks if it's a shared repository.
+func (d *DotGit) Alternates() ([]*DotGit, error) {
+ altpath := d.fs.Join("objects", "info", "alternates")
+ f, err := d.fs.Open(altpath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var alternates []*DotGit
+
+ // Read alternate paths line-by-line and create DotGit objects.
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ path := scanner.Text()
+ if !filepath.IsAbs(path) {
+ // For relative paths, we can perform an internal conversion to
+ // slash so that they work cross-platform.
+ slashPath := filepath.ToSlash(path)
+ // If the path is not absolute, it must be relative to object
+ // database (.git/objects/info).
+ // https://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html
+ // Hence, derive a path relative to DotGit's root.
+ // "../../../reponame/.git/" -> "../../reponame/.git"
+ // Remove the first ../
+ relpath := filepath.Join(strings.Split(slashPath, "/")[1:]...)
+ normalPath := filepath.FromSlash(relpath)
+ path = filepath.Join(d.fs.Root(), normalPath)
+ }
+ fs := osfs.New(filepath.Dir(path))
+ alternates = append(alternates, New(fs))
+ }
+
+ if err = scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return alternates, nil
+}
+
func isHex(s string) bool {
for _, b := range []byte(s) {
if isNum(b) {
diff --git a/storage/filesystem/internal/dotgit/dotgit_test.go b/storage/filesystem/internal/dotgit/dotgit_test.go
index 0ed9ec5..2c43295 100644
--- a/storage/filesystem/internal/dotgit/dotgit_test.go
+++ b/storage/filesystem/internal/dotgit/dotgit_test.go
@@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
+ "runtime"
"strings"
"testing"
@@ -615,3 +616,55 @@ func (s *SuiteDotGit) TestPackRefs(c *C) {
c.Assert(ref, NotNil)
c.Assert(ref.Hash().String(), Equals, "b8d3ffab552895c19b9fcf7aa264d277cde33881")
}
+
+func (s *SuiteDotGit) TestAlternates(c *C) {
+ tmp, err := ioutil.TempDir("", "dot-git")
+ c.Assert(err, IsNil)
+ defer os.RemoveAll(tmp)
+
+ // Create a new billy fs.
+ fs := osfs.New(tmp)
+
+ // Create a new dotgit object and initialize.
+ dir := New(fs)
+ err = dir.Initialize()
+ c.Assert(err, IsNil)
+
+ // Create alternates file.
+ altpath := filepath.Join("objects", "info", "alternates")
+ f, err := fs.Create(altpath)
+ c.Assert(err, IsNil)
+
+ // Multiple alternates.
+ var strContent string
+ if runtime.GOOS == "windows" {
+ strContent = "C:\\Users\\username\\repo1\\.git\\objects\r\n..\\..\\..\\rep2\\.git\\objects"
+ } else {
+ strContent = "/Users/username/rep1//.git/objects\n../../../rep2//.git/objects"
+ }
+ content := []byte(strContent)
+ f.Write(content)
+ f.Close()
+
+ dotgits, err := dir.Alternates()
+ c.Assert(err, IsNil)
+ if runtime.GOOS == "windows" {
+ c.Assert(dotgits[0].fs.Root(), Equals, "C:\\Users\\username\\repo1\\.git")
+ } else {
+ c.Assert(dotgits[0].fs.Root(), Equals, "/Users/username/rep1/.git")
+ }
+
+ // For relative path:
+ // /some/absolute/path/to/dot-git -> /some/absolute/path
+ pathx := strings.Split(tmp, string(filepath.Separator))
+ pathx = pathx[:len(pathx)-2]
+ // Use string.Join() to avoid malformed absolutepath on windows
+ // C:Users\\User\\... instead of C:\\Users\\appveyor\\... .
+ resolvedPath := strings.Join(pathx, string(filepath.Separator))
+ // Append the alternate path to the resolvedPath
+ expectedPath := filepath.Join(string(filepath.Separator), resolvedPath, "rep2", ".git")
+ if runtime.GOOS == "windows" {
+ expectedPath = filepath.Join(resolvedPath, "rep2", ".git")
+ }
+ c.Assert(dotgits[1].fs.Root(), Equals, expectedPath)
+}
diff --git a/storage/filesystem/object.go b/storage/filesystem/object.go
index 6ca67cc..fd52ed5 100644
--- a/storage/filesystem/object.go
+++ b/storage/filesystem/object.go
@@ -160,6 +160,27 @@ func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (p
obj, err = s.getFromPackfile(h, false)
}
+ // If the error is still object not found, check if it's a shared object
+ // repository.
+ if err == plumbing.ErrObjectNotFound {
+ dotgits, e := s.dir.Alternates()
+ if e == nil {
+ // Create a new object storage with the DotGit(s) and check for the
+ // required hash object. Skip when not found.
+ for _, dg := range dotgits {
+ o, oe := newObjectStorage(dg)
+ if oe != nil {
+ continue
+ }
+ enobj, enerr := o.EncodedObject(t, h)
+ if enerr != nil {
+ continue
+ }
+ return enobj, nil
+ }
+ }
+ }
+
if err != nil {
return nil, err
}
diff --git a/worktree_test.go b/worktree_test.go
index 46c1faa..4f24cff 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -1287,3 +1287,33 @@ func (s *WorktreeSuite) TestClean(c *C) {
c.Assert(len(status), Equals, 0)
}
+
+func (s *WorktreeSuite) TestAlternatesRepo(c *C) {
+ fs := fixtures.ByTag("alternates").One().Worktree()
+
+ // Open 1st repo.
+ rep1fs, err := fs.Chroot("rep1")
+ c.Assert(err, IsNil)
+ rep1, err := PlainOpen(rep1fs.Root())
+ c.Assert(err, IsNil)
+
+ // Open 2nd repo.
+ rep2fs, err := fs.Chroot("rep2")
+ c.Assert(err, IsNil)
+ rep2, err := PlainOpen(rep2fs.Root())
+ c.Assert(err, IsNil)
+
+ // Get the HEAD commit from the main repo.
+ h, err := rep1.Head()
+ c.Assert(err, IsNil)
+ commit1, err := rep1.CommitObject(h.Hash())
+ c.Assert(err, IsNil)
+
+ // Get the HEAD commit from the shared repo.
+ h, err = rep2.Head()
+ c.Assert(err, IsNil)
+ commit2, err := rep2.CommitObject(h.Hash())
+ c.Assert(err, IsNil)
+
+ c.Assert(commit1.String(), Equals, commit2.String())
+}