diff --git a/repository.go b/repository.go index e5b12b0c5..7cb05a2ca 100644 --- a/repository.go +++ b/repository.go @@ -46,6 +46,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") @@ -252,7 +253,13 @@ func PlainOpenWithOptions(path string, o *PlainOpenOptions) (*Repository, error) return nil, err } - s := filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) + options := filesystem.Options{} + options.CommonDir, err = dotGitCommonDirectory(dot) + if err != nil { + return nil, err + } + + s := filesystem.NewStorageWithOptions(dot, cache.NewObjectLRUDefault(), options) return Open(s, wt) } @@ -327,6 +334,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 ba9667e65..8f8b5446c 100644 --- a/storage/filesystem/dotgit/dotgit.go +++ b/storage/filesystem/dotgit/dotgit.go @@ -66,6 +66,9 @@ type Options struct { // KeepDescriptors makes the file descriptors to be reused but they will // need to be manually closed calling Close(). KeepDescriptors bool + // CommonDir sets the directory used for accessing non-worktree files that + // would normally be taken from the root directory. + CommonDir billy.Filesystem } // The DotGit type represents a local git repository on disk. This @@ -73,6 +76,7 @@ type Options struct { type DotGit struct { options Options fs billy.Filesystem + localfs billy.Filesystem // incoming object directory information incomingChecked bool @@ -96,9 +100,14 @@ func New(fs billy.Filesystem) *DotGit { // NewWithOptions sets non default configuration options. // See New for complete help. func NewWithOptions(fs billy.Filesystem, o Options) *DotGit { + if o.CommonDir == nil { + o.CommonDir = fs + } + return &DotGit{ options: o, - fs: fs, + fs: o.CommonDir, + localfs: fs, } } @@ -923,7 +932,8 @@ func (d *DotGit) addRefFromHEAD(refs *[]*plumbing.Reference) error { func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference, err error) { path = d.fs.Join(path, d.fs.Join(strings.Split(name, "/")...)) - f, err := d.fs.Open(path) + + f, err := d.fsFromRefPath(path).Open(path) if err != nil { return nil, err } @@ -932,6 +942,15 @@ func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference, return d.readReferenceFrom(f, name) } +func (d *DotGit) fsFromRefPath(path string) billy.Filesystem { + // In general, all pseudo refs are per working tree and all refs starting + // with "refs/" are shared. + if strings.HasPrefix(path, "refs/") { + return d.fs + } + return d.localfs +} + func (d *DotGit) CountLooseRefs() (int, error) { var refs []*plumbing.Reference var seen = make(map[plumbing.ReferenceName]bool) diff --git a/storage/filesystem/dotgit/dotgit_setref.go b/storage/filesystem/dotgit/dotgit_setref.go index 9da2f31e8..5ea98c0e5 100644 --- a/storage/filesystem/dotgit/dotgit_setref.go +++ b/storage/filesystem/dotgit/dotgit_setref.go @@ -25,7 +25,7 @@ func (d *DotGit) setRefRwfs(fileName, content string, old *plumbing.Reference) ( mode |= os.O_TRUNC } - f, err := d.fs.OpenFile(fileName, mode, 0666) + f, err := d.fsFromRefPath(fileName).OpenFile(fileName, mode, 0666) if err != nil { return err } @@ -59,9 +59,11 @@ func (d *DotGit) setRefRwfs(fileName, content string, old *plumbing.Reference) ( // making it compatible with these simple filesystems. This is usually not // a problem as they should be accessed by only one process at a time. func (d *DotGit) setRefNorwfs(fileName, content string, old *plumbing.Reference) error { - _, err := d.fs.Stat(fileName) + fs := d.fsFromRefPath(fileName) + + _, err := fs.Stat(fileName) if err == nil && old != nil { - fRead, err := d.fs.Open(fileName) + fRead, err := fs.Open(fileName) if err != nil { return err } @@ -78,7 +80,7 @@ func (d *DotGit) setRefNorwfs(fileName, content string, old *plumbing.Reference) } } - f, err := d.fs.Create(fileName) + f, err := fs.Create(fileName) if err != nil { return err } diff --git a/storage/filesystem/storage.go b/storage/filesystem/storage.go index 370f7bd34..0712b412f 100644 --- a/storage/filesystem/storage.go +++ b/storage/filesystem/storage.go @@ -12,8 +12,9 @@ import ( // standard git format (this is, the .git directory). Zero values of this type // are not safe to use, see the NewStorage function below. type Storage struct { - fs billy.Filesystem - dir *dotgit.DotGit + fs billy.Filesystem + commonfs billy.Filesystem + dir *dotgit.DotGit ObjectStorage ReferenceStorage @@ -31,6 +32,9 @@ type Options struct { // KeepDescriptors makes the file descriptors to be reused but they will // need to be manually closed calling Close(). KeepDescriptors bool + // CommonDir sets the directory used for accessing non-worktree files that + // would normally be taken from the root directory. + CommonDir billy.Filesystem } // NewStorage returns a new Storage backed by a given `fs.Filesystem` and cache. @@ -44,12 +48,18 @@ func NewStorageWithOptions(fs billy.Filesystem, cache cache.Object, ops Options) dirOps := dotgit.Options{ ExclusiveAccess: ops.ExclusiveAccess, KeepDescriptors: ops.KeepDescriptors, + CommonDir: ops.CommonDir, } + dir := dotgit.NewWithOptions(fs, dirOps) + if ops.CommonDir == nil { + ops.CommonDir = fs + } return &Storage{ - fs: fs, - dir: dir, + fs: fs, + commonfs: ops.CommonDir, + dir: dir, ObjectStorage: *NewObjectStorageWithOptions(dir, cache, ops), ReferenceStorage: ReferenceStorage{dir: dir}, @@ -65,6 +75,12 @@ func (s *Storage) Filesystem() billy.Filesystem { return s.fs } +// MainFilesystem returns the underlying filesystem for the main +// working-tree/common git directory +func (s *Storage) MainFilesystem() billy.Filesystem { + return s.commonfs +} + // Init initializes .git directory func (s *Storage) Init() error { return s.dir.Initialize() diff --git a/worktree_test.go b/worktree_test.go index afedc9195..a47eb5126 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1965,3 +1965,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 := PlainOpen(fs.Root()) + 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 := PlainOpen(fs.Root()) + 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 := PlainOpen(fs.Root()) + 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 = PlainOpen(fs.Root()) + c.Assert(err, Equals, ErrRepositoryIncomplete) + } +}