aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTimofey Kirillov <timofey.kirillov@flant.com>2019-03-28 20:32:01 +0000
committerTimofey Kirillov <timofey.kirillov@flant.com>2020-06-15 17:16:42 +0300
commit63c42e530e9ffdf070f74a2674aa1a1fc24703a8 (patch)
tree41daf4cb42a97aa209f198e0b5299e048957d80f
parent5cafe4097c72c0673255fdf8cac44fc513c6042c (diff)
downloadgo-git-63c42e530e9ffdf070f74a2674aa1a1fc24703a8.tar.gz
Support `.git/commondir` repository layout
Git creates `.git/commondir` when there are custom worktrees (see "git worktree add" related commands). `.git/commondir` in such case contains a link to another dot-git repository tree, which could contain some folders like: - objects; - config; - refs; - etc. In this PR a new dotgit.RepositoryFilesystem struct is defined, which is billy.Filesystem interface compatible object-wrapper, that can handle commondir and dispatch all operations to the correct file path. `git.PlainOpen` remain unchanged, but `git.PlainOpenWithOptions` has a new option: `PlainOpenOptions.EnableDotGitCommonDir=true|false` (which is false by default). When `EnableDotGitCommonDir=true` repository-open procedure will read `.git/commondir` (if it exists) and then create dotgit.RepositoryFilesystem object initialized with 2 filesystems. This object then passed into storage and then into dotgit.DotGit as `billy.Filesystem` interface. This object will catch all filesystem operations and dispatch to the correct repository-filesystem (dot-git or common-dot-git) according to the rules described in the doc: https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt. EnableDotGitCommonDir option will only work with the filesystem-backed storage. Also worktree_test.go has been adopted from an older, already existing existing PR: https://github.com/src-d/go-git/pull/1098. This PR needs new fixtures added in the following PR: https://github.com/go-git/go-git-fixtures/pull/1.
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--options.go3
-rw-r--r--repository.go49
-rw-r--r--storage/filesystem/dotgit/dotgit.go6
-rw-r--r--storage/filesystem/dotgit/repository_filesystem.go111
-rw-r--r--storage/filesystem/dotgit/repository_filesystem_test.go124
-rw-r--r--worktree_test.go75
8 files changed, 370 insertions, 2 deletions
diff --git a/go.mod b/go.mod
index 0c9cfd2..81bbaa8 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
github.com/gliderlabs/ssh v0.2.2
github.com/go-git/gcfg v1.5.0
github.com/go-git/go-billy/v5 v5.0.0
- github.com/go-git/go-git-fixtures/v4 v4.0.1
+ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12
github.com/google/go-cmp v0.3.0
github.com/imdario/mergo v0.3.9
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
diff --git a/go.sum b/go.sum
index e14e29a..9af1b06 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,8 @@ github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agR
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
+github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
+github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
diff --git a/options.go b/options.go
index 8495e97..2f93631 100644
--- a/options.go
+++ b/options.go
@@ -602,6 +602,9 @@ type PlainOpenOptions struct {
// DetectDotGit defines whether parent directories should be
// walked until a .git directory or file is found.
DetectDotGit bool
+ // Enable .git/commondir support (see https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt).
+ // NOTE: This option will only work with the filesystem storage.
+ EnableDotGitCommonDir bool
}
// Validate validates the fields and sets the default values.
diff --git a/repository.go b/repository.go
index 47318d1..1f6de76 100644
--- a/repository.go
+++ b/repository.go
@@ -13,6 +13,8 @@ import (
"strings"
"time"
+ "github.com/go-git/go-git/v5/storage/filesystem/dotgit"
+
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/internal/revision"
"github.com/go-git/go-git/v5/plumbing"
@@ -47,6 +49,7 @@ var (
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
ErrRepositoryNotExists = errors.New("repository does not exist")
+ ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
ErrRepositoryAlreadyExists = errors.New("repository already exists")
ErrRemoteNotFound = errors.New("remote not found")
ErrRemoteExists = errors.New("remote already exists")
@@ -253,7 +256,19 @@ func PlainOpenWithOptions(path string, o *PlainOpenOptions) (*Repository, error)
return nil, err
}
- s := filesystem.NewStorage(dot, cache.NewObjectLRUDefault())
+ var repositoryFs billy.Filesystem
+
+ if o.EnableDotGitCommonDir {
+ dotGitCommon, err := dotGitCommonDirectory(dot)
+ if err != nil {
+ return nil, err
+ }
+ repositoryFs = dotgit.NewRepositoryFilesystem(dot, dotGitCommon)
+ } else {
+ repositoryFs = dot
+ }
+
+ s := filesystem.NewStorage(repositoryFs, cache.NewObjectLRUDefault())
return Open(s, wt)
}
@@ -328,6 +343,38 @@ func dotGitFileToOSFilesystem(path string, fs billy.Filesystem) (bfs billy.Files
return osfs.New(fs.Join(path, gitdir)), nil
}
+func dotGitCommonDirectory(fs billy.Filesystem) (commonDir billy.Filesystem, err error) {
+ f, err := fs.Open("commondir")
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ b, err := stdioutil.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+ if len(b) > 0 {
+ path := strings.TrimSpace(string(b))
+ if filepath.IsAbs(path) {
+ commonDir = osfs.New(path)
+ } else {
+ commonDir = osfs.New(filepath.Join(fs.Root(), path))
+ }
+ if _, err := commonDir.Stat(""); err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrRepositoryIncomplete
+ }
+
+ return nil, err
+ }
+ }
+
+ return commonDir, nil
+}
+
// PlainClone a repository into the path with the given options, isBare defines
// if the new repository will be bare or normal. If the path is not empty
// ErrRepositoryAlreadyExists is returned.
diff --git a/storage/filesystem/dotgit/dotgit.go b/storage/filesystem/dotgit/dotgit.go
index 83c7683..3840ea7 100644
--- a/storage/filesystem/dotgit/dotgit.go
+++ b/storage/filesystem/dotgit/dotgit.go
@@ -30,6 +30,12 @@ const (
objectsPath = "objects"
packPath = "pack"
refsPath = "refs"
+ branchesPath = "branches"
+ hooksPath = "hooks"
+ infoPath = "info"
+ remotesPath = "remotes"
+ logsPath = "logs"
+ worktreesPath = "worktrees"
tmpPackedRefsPrefix = "._packed-refs"
diff --git a/storage/filesystem/dotgit/repository_filesystem.go b/storage/filesystem/dotgit/repository_filesystem.go
new file mode 100644
index 0000000..8d243ef
--- /dev/null
+++ b/storage/filesystem/dotgit/repository_filesystem.go
@@ -0,0 +1,111 @@
+package dotgit
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/go-git/go-billy/v5"
+)
+
+// RepositoryFilesystem is a billy.Filesystem compatible object wrapper
+// which handles dot-git filesystem operations and supports commondir according to git scm layout:
+// https://github.com/git/git/blob/master/Documentation/gitrepository-layout.txt
+type RepositoryFilesystem struct {
+ dotGitFs billy.Filesystem
+ commonDotGitFs billy.Filesystem
+}
+
+func NewRepositoryFilesystem(dotGitFs, commonDotGitFs billy.Filesystem) *RepositoryFilesystem {
+ return &RepositoryFilesystem{
+ dotGitFs: dotGitFs,
+ commonDotGitFs: commonDotGitFs,
+ }
+}
+
+func (fs *RepositoryFilesystem) mapToRepositoryFsByPath(path string) billy.Filesystem {
+ // Nothing to decide if commondir not defined
+ if fs.commonDotGitFs == nil {
+ return fs.dotGitFs
+ }
+
+ cleanPath := filepath.Clean(path)
+
+ // Check exceptions for commondir (https://git-scm.com/docs/gitrepository-layout#Documentation/gitrepository-layout.txt)
+ switch cleanPath {
+ case fs.dotGitFs.Join(logsPath, "HEAD"):
+ return fs.dotGitFs
+ case fs.dotGitFs.Join(refsPath, "bisect"), fs.dotGitFs.Join(refsPath, "rewritten"), fs.dotGitFs.Join(refsPath, "worktree"):
+ return fs.dotGitFs
+ }
+
+ // Determine dot-git root by first path element.
+ // There are some elements which should always use commondir when commondir defined.
+ // Usual dot-git root will be used for the rest of files.
+ switch strings.Split(cleanPath, string(filepath.Separator))[0] {
+ case objectsPath, refsPath, packedRefsPath, configPath, branchesPath, hooksPath, infoPath, remotesPath, logsPath, shallowPath, worktreesPath:
+ return fs.commonDotGitFs
+ default:
+ return fs.dotGitFs
+ }
+}
+
+func (fs *RepositoryFilesystem) Create(filename string) (billy.File, error) {
+ return fs.mapToRepositoryFsByPath(filename).Create(filename)
+}
+
+func (fs *RepositoryFilesystem) Open(filename string) (billy.File, error) {
+ return fs.mapToRepositoryFsByPath(filename).Open(filename)
+}
+
+func (fs *RepositoryFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
+ return fs.mapToRepositoryFsByPath(filename).OpenFile(filename, flag, perm)
+}
+
+func (fs *RepositoryFilesystem) Stat(filename string) (os.FileInfo, error) {
+ return fs.mapToRepositoryFsByPath(filename).Stat(filename)
+}
+
+func (fs *RepositoryFilesystem) Rename(oldpath, newpath string) error {
+ return fs.mapToRepositoryFsByPath(oldpath).Rename(oldpath, newpath)
+}
+
+func (fs *RepositoryFilesystem) Remove(filename string) error {
+ return fs.mapToRepositoryFsByPath(filename).Remove(filename)
+}
+
+func (fs *RepositoryFilesystem) Join(elem ...string) string {
+ return fs.dotGitFs.Join(elem...)
+}
+
+func (fs *RepositoryFilesystem) TempFile(dir, prefix string) (billy.File, error) {
+ return fs.mapToRepositoryFsByPath(dir).TempFile(dir, prefix)
+}
+
+func (fs *RepositoryFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
+ return fs.mapToRepositoryFsByPath(path).ReadDir(path)
+}
+
+func (fs *RepositoryFilesystem) MkdirAll(filename string, perm os.FileMode) error {
+ return fs.mapToRepositoryFsByPath(filename).MkdirAll(filename, perm)
+}
+
+func (fs *RepositoryFilesystem) Lstat(filename string) (os.FileInfo, error) {
+ return fs.mapToRepositoryFsByPath(filename).Lstat(filename)
+}
+
+func (fs *RepositoryFilesystem) Symlink(target, link string) error {
+ return fs.mapToRepositoryFsByPath(target).Symlink(target, link)
+}
+
+func (fs *RepositoryFilesystem) Readlink(link string) (string, error) {
+ return fs.mapToRepositoryFsByPath(link).Readlink(link)
+}
+
+func (fs *RepositoryFilesystem) Chroot(path string) (billy.Filesystem, error) {
+ return fs.mapToRepositoryFsByPath(path).Chroot(path)
+}
+
+func (fs *RepositoryFilesystem) Root() string {
+ return fs.dotGitFs.Root()
+}
diff --git a/storage/filesystem/dotgit/repository_filesystem_test.go b/storage/filesystem/dotgit/repository_filesystem_test.go
new file mode 100644
index 0000000..880ec0d
--- /dev/null
+++ b/storage/filesystem/dotgit/repository_filesystem_test.go
@@ -0,0 +1,124 @@
+package dotgit
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+
+ "github.com/go-git/go-billy/v5/osfs"
+
+ . "gopkg.in/check.v1"
+)
+
+func (s *SuiteDotGit) TestRepositoryFilesystem(c *C) {
+ dir, err := ioutil.TempDir("", "repository_filesystem")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ fs := osfs.New(dir)
+
+ err = fs.MkdirAll("dotGit", 0777)
+ c.Assert(err, IsNil)
+ dotGitFs, err := fs.Chroot("dotGit")
+ c.Assert(err, IsNil)
+
+ err = fs.MkdirAll("commonDotGit", 0777)
+ c.Assert(err, IsNil)
+ commonDotGitFs, err := fs.Chroot("commonDotGit")
+ c.Assert(err, IsNil)
+
+ repositoryFs := NewRepositoryFilesystem(dotGitFs, commonDotGitFs)
+ c.Assert(repositoryFs.Root(), Equals, dotGitFs.Root())
+
+ somedir, err := repositoryFs.Chroot("somedir")
+ c.Assert(err, IsNil)
+ c.Assert(somedir.Root(), Equals, repositoryFs.Join(dotGitFs.Root(), "somedir"))
+
+ _, err = repositoryFs.Create("somefile")
+ c.Assert(err, IsNil)
+
+ _, err = repositoryFs.Stat("somefile")
+ c.Assert(err, IsNil)
+
+ file, err := repositoryFs.Open("somefile")
+ c.Assert(err, IsNil)
+ err = file.Close()
+ c.Assert(err, IsNil)
+
+ file, err = repositoryFs.OpenFile("somefile", os.O_RDONLY, 0666)
+ c.Assert(err, IsNil)
+ err = file.Close()
+ c.Assert(err, IsNil)
+
+ file, err = repositoryFs.Create("somefile2")
+ c.Assert(err, IsNil)
+ err = file.Close()
+ c.Assert(err, IsNil)
+ _, err = repositoryFs.Stat("somefile2")
+ c.Assert(err, IsNil)
+ err = repositoryFs.Rename("somefile2", "newfile")
+ c.Assert(err, IsNil)
+
+ tempDir, err := repositoryFs.TempFile("tmp", "myprefix")
+ c.Assert(err, IsNil)
+ c.Assert(repositoryFs.Join(repositoryFs.Root(), "tmp", tempDir.Name()), Equals, repositoryFs.Join(dotGitFs.Root(), "tmp", tempDir.Name()))
+
+ err = repositoryFs.Symlink("newfile", "somelink")
+ c.Assert(err, IsNil)
+
+ _, err = repositoryFs.Lstat("somelink")
+ c.Assert(err, IsNil)
+
+ link, err := repositoryFs.Readlink("somelink")
+ c.Assert(err, IsNil)
+ c.Assert(link, Equals, "newfile")
+
+ err = repositoryFs.Remove("somelink")
+ c.Assert(err, IsNil)
+
+ _, err = repositoryFs.Stat("somelink")
+ c.Assert(os.IsNotExist(err), Equals, true)
+
+ dirs := []string{objectsPath, refsPath, packedRefsPath, configPath, branchesPath, hooksPath, infoPath, remotesPath, logsPath, shallowPath, worktreesPath}
+ for _, dir := range dirs {
+ err := repositoryFs.MkdirAll(dir, 0777)
+ c.Assert(err, IsNil)
+ _, err = commonDotGitFs.Stat(dir)
+ c.Assert(err, IsNil)
+ _, err = dotGitFs.Stat(dir)
+ c.Assert(os.IsNotExist(err), Equals, true)
+ }
+
+ exceptionsPaths := []string{repositoryFs.Join(logsPath, "HEAD"), repositoryFs.Join(refsPath, "bisect"), repositoryFs.Join(refsPath, "rewritten"), repositoryFs.Join(refsPath, "worktree")}
+ for _, path := range exceptionsPaths {
+ _, err := repositoryFs.Create(path)
+ c.Assert(err, IsNil)
+ _, err = commonDotGitFs.Stat(path)
+ c.Assert(os.IsNotExist(err), Equals, true)
+ _, err = dotGitFs.Stat(path)
+ c.Assert(err, IsNil)
+ }
+
+ err = repositoryFs.MkdirAll("refs/heads", 0777)
+ c.Assert(err, IsNil)
+ _, err = commonDotGitFs.Stat("refs/heads")
+ c.Assert(err, IsNil)
+ _, err = dotGitFs.Stat("refs/heads")
+ c.Assert(os.IsNotExist(err), Equals, true)
+
+ err = repositoryFs.MkdirAll("objects/pack", 0777)
+ c.Assert(err, IsNil)
+ _, err = commonDotGitFs.Stat("objects/pack")
+ c.Assert(err, IsNil)
+ _, err = dotGitFs.Stat("objects/pack")
+ c.Assert(os.IsNotExist(err), Equals, true)
+
+ err = repositoryFs.MkdirAll("a/b/c", 0777)
+ c.Assert(err, IsNil)
+ _, err = commonDotGitFs.Stat("a/b/c")
+ c.Assert(os.IsNotExist(err), Equals, true)
+ _, err = dotGitFs.Stat("a/b/c")
+ c.Assert(err, IsNil)
+}
diff --git a/worktree_test.go b/worktree_test.go
index 72bcbd9..c808ebd 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -2052,3 +2052,78 @@ func (s *WorktreeSuite) TestAddAndCommit(c *C) {
})
c.Assert(err, IsNil)
}
+
+func (s *WorktreeSuite) TestLinkedWorktree(c *C) {
+ fs := fixtures.ByTag("linked-worktree").One().Worktree()
+
+ // Open main repo.
+ {
+ fs, err := fs.Chroot("main")
+ c.Assert(err, IsNil)
+ repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
+ c.Assert(err, IsNil)
+
+ wt, err := repo.Worktree()
+ c.Assert(err, IsNil)
+
+ status, err := wt.Status()
+ c.Assert(err, IsNil)
+ c.Assert(len(status), Equals, 2) // 2 files
+
+ head, err := repo.Head()
+ c.Assert(err, IsNil)
+ c.Assert(string(head.Name()), Equals, "refs/heads/master")
+ }
+
+ // Open linked-worktree #1.
+ {
+ fs, err := fs.Chroot("linked-worktree-1")
+ c.Assert(err, IsNil)
+ repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
+ c.Assert(err, IsNil)
+
+ wt, err := repo.Worktree()
+ c.Assert(err, IsNil)
+
+ status, err := wt.Status()
+ c.Assert(err, IsNil)
+ c.Assert(len(status), Equals, 3) // 3 files
+
+ _, ok := status["linked-worktree-1-unique-file.txt"]
+ c.Assert(ok, Equals, true)
+
+ head, err := repo.Head()
+ c.Assert(err, IsNil)
+ c.Assert(string(head.Name()), Equals, "refs/heads/linked-worktree-1")
+ }
+
+ // Open linked-worktree #2.
+ {
+ fs, err := fs.Chroot("linked-worktree-2")
+ c.Assert(err, IsNil)
+ repo, err := PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
+ c.Assert(err, IsNil)
+
+ wt, err := repo.Worktree()
+ c.Assert(err, IsNil)
+
+ status, err := wt.Status()
+ c.Assert(err, IsNil)
+ c.Assert(len(status), Equals, 3) // 3 files
+
+ _, ok := status["linked-worktree-2-unique-file.txt"]
+ c.Assert(ok, Equals, true)
+
+ head, err := repo.Head()
+ c.Assert(err, IsNil)
+ c.Assert(string(head.Name()), Equals, "refs/heads/branch-with-different-name")
+ }
+
+ // Open linked-worktree #2.
+ {
+ fs, err := fs.Chroot("linked-worktree-invalid-commondir")
+ c.Assert(err, IsNil)
+ _, err = PlainOpenWithOptions(fs.Root(), &PlainOpenOptions{EnableDotGitCommonDir: true})
+ c.Assert(err, Equals, ErrRepositoryIncomplete)
+ }
+}