From 63c42e530e9ffdf070f74a2674aa1a1fc24703a8 Mon Sep 17 00:00:00 2001 From: Timofey Kirillov Date: Thu, 28 Mar 2019 20:32:01 +0000 Subject: 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. --- storage/filesystem/dotgit/dotgit.go | 6 + storage/filesystem/dotgit/repository_filesystem.go | 111 ++++++++++++++++++ .../dotgit/repository_filesystem_test.go | 124 +++++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 storage/filesystem/dotgit/repository_filesystem.go create mode 100644 storage/filesystem/dotgit/repository_filesystem_test.go (limited to 'storage/filesystem') 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) +} -- cgit