diff options
-rw-r--r-- | COMPATIBILITY.md | 2 | ||||
-rw-r--r-- | plumbing/format/diff/unified_encoder.go | 5 | ||||
-rw-r--r-- | plumbing/format/diff/unified_encoder_test.go | 2 | ||||
-rw-r--r-- | plumbing/object/patch.go | 5 | ||||
-rw-r--r-- | plumbing/object/patch_test.go | 44 | ||||
-rw-r--r-- | storage/filesystem/internal/dotgit/dotgit.go | 45 | ||||
-rw-r--r-- | storage/filesystem/internal/dotgit/dotgit_test.go | 53 | ||||
-rw-r--r-- | storage/filesystem/object.go | 21 | ||||
-rw-r--r-- | worktree_test.go | 30 |
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()) +} |