├── README ├── LICENSE ├── manifest ├── parser_test.go └── parser.go ├── main.go └── fs ├── manifestfs.go ├── multifs.go ├── fs_test.go └── fs.go /README: -------------------------------------------------------------------------------- 1 | 2 | This is a simple git filesystem. 3 | 4 | Usage: 5 | 6 | gitfs $MOUNT & 7 | cd $MOUNT/config 8 | 9 | # Create $MOUNT/repo at commit master 10 | ln -s /home/$USER/myrepo:master repo 11 | 12 | # Create $MOUNT/subdir/repo at commit master^^ 13 | mkdir subdir 14 | ln -s /home/$USER/myrepo:master^^ subdir/repo 15 | 16 | cd $MOUNT/repo 17 | 18 | # Create a transient symlink to store compile outputs. 19 | ln -s /tmp/build-products out 20 | 21 | 22 | DISCLAIMER 23 | 24 | This is not an official Google product. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /manifest/parser_test.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var aospManifest = ` 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ` 24 | 25 | func TestBasic(t *testing.T) { 26 | manifest, err := Parse([]byte(aospManifest)) 27 | if err != nil { 28 | t.Fatalf("Unmarshal: %v", err) 29 | } 30 | 31 | want := &Manifest{ 32 | Remote: Remote{ 33 | Name: "aosp", 34 | Fetch: "..", 35 | Review: "https://android-review.googlesource.com/", 36 | }, 37 | Default: Default{ 38 | Revision: "master", 39 | Remote: "aosp", 40 | SyncJ: "4", 41 | }, 42 | Project: []Project{ 43 | { 44 | Path: "build", 45 | Name: "platform/build", 46 | GroupsString: "pdk,tradefed", 47 | Groups: map[string]bool{ 48 | "pdk": true, 49 | "tradefed": true, 50 | }, 51 | Copyfile: []Copyfile{ 52 | { 53 | Src: "core/root.mk", 54 | Dest: "Makefile", 55 | }, 56 | }, 57 | }, 58 | { 59 | Path: "build/soong", 60 | Name: "platform/build/soong", 61 | GroupsString: "pdk,tradefed", 62 | Groups: map[string]bool{ 63 | "pdk": true, 64 | "tradefed": true, 65 | }, 66 | Linkfile: []Linkfile{ 67 | { 68 | Src: "root.bp", 69 | Dest: "Android.bp", 70 | }, 71 | }, 72 | }, 73 | }, 74 | } 75 | 76 | if !reflect.DeepEqual(manifest, want) { 77 | t.Errorf("got %v, want %v", manifest, want) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/hanwen/gitfs/fs" 12 | "github.com/hanwen/gitfs/manifest" 13 | "github.com/hanwen/go-fuse/fuse/nodefs" 14 | ) 15 | 16 | func main() { 17 | debug := flag.Bool("debug", false, "print FUSE debug data") 18 | lazy := flag.Bool("lazy", true, "only read contents for reads") 19 | disk := flag.Bool("disk", false, "don't use intermediate files") 20 | gitRepo := flag.String("git_repo", "", "if set, mount a single repository.") 21 | repo := flag.String("repo", "", "if set, mount a single manifest from repo repository.") 22 | flag.Parse() 23 | if len(flag.Args()) < 1 { 24 | log.Fatalf("usage: %s MOUNT", os.Args[0]) 25 | } 26 | 27 | tempDir, err := ioutil.TempDir("", "gitfs") 28 | if err != nil { 29 | log.Fatalf("TempDir: %v", err) 30 | } 31 | 32 | mntDir := flag.Args()[0] 33 | opts := fs.GitFSOptions{ 34 | Lazy: *lazy, 35 | Disk: *disk, 36 | TempDir: tempDir, 37 | } 38 | var root nodefs.Node 39 | if *repo != "" { 40 | xml := filepath.Join(*repo, "manifest.xml") 41 | 42 | m, err := manifest.ParseFile(xml) 43 | if err != nil { 44 | log.Fatalf("ParseFile(%q): %v", *repo, err) 45 | } 46 | 47 | root, err = fs.NewManifestFS(m, filepath.Join(*repo, "projects"), &opts) 48 | if err != nil { 49 | log.Fatalf("NewManifestFS: %v", err) 50 | } 51 | } else if *gitRepo != "" { 52 | var err error 53 | root, err = fs.NewGitFSRoot(*gitRepo, &opts) 54 | if err != nil { 55 | log.Fatalf("NewGitFSRoot: %v", err) 56 | } 57 | } else { 58 | root = fs.NewMultiGitFSRoot(&opts) 59 | } 60 | server, _, err := nodefs.MountRoot(mntDir, root, &nodefs.Options{ 61 | EntryTimeout: time.Hour, 62 | NegativeTimeout: time.Hour, 63 | AttrTimeout: time.Hour, 64 | PortableInodes: true, 65 | }) 66 | if err != nil { 67 | log.Fatalf("MountFileSystem: %v", err) 68 | } 69 | if *debug { 70 | server.SetDebug(true) 71 | } 72 | log.Printf("Started git multi fs FUSE on %s", mntDir) 73 | server.Serve() 74 | } 75 | -------------------------------------------------------------------------------- /manifest/parser.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "encoding/xml" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | var _ = xml.Unmarshal 10 | 11 | type Copyfile struct { 12 | Src string `xml:"src,attr"` 13 | Dest string `xml:"dest,attr"` 14 | } 15 | 16 | type Linkfile struct { 17 | Src string `xml:"src,attr"` 18 | Dest string `xml:"dest,attr"` 19 | } 20 | 21 | type Project struct { 22 | Path string `xml:"path,attr"` 23 | Name string `xml:"name,attr"` 24 | Remote string `xml:"remote,attr"` 25 | Copyfile []Copyfile `xml:"copyfile"` 26 | Linkfile []Linkfile `xml:"linkfile"` 27 | GroupsString string `xml:"groups,attr"` 28 | Groups map[string]bool 29 | 30 | Revision string `xml:"revision,attr"` 31 | DestBranch string `xml:"dest-branch,attr"` 32 | SyncJ string `xml:"sync-j,attr"` 33 | SyncC string `xml:"sync-c,attr"` 34 | SyncS string `xml:"sync-s,attr"` 35 | 36 | Upstream string `xml:"upstream,attr"` 37 | CloneDepth string `xml:"clone-depth,attr"` 38 | ForcePath string `xml:"force-path,attr"` 39 | } 40 | 41 | func (p *Project) parse() { 42 | for _, s := range strings.Split(p.GroupsString, ",") { 43 | if s == "" { 44 | continue 45 | } 46 | if p.Groups == nil { 47 | p.Groups = map[string]bool{} 48 | } 49 | p.Groups[s] = true 50 | } 51 | } 52 | 53 | type Remote struct { 54 | Alias string `xml:"alias,attr"` 55 | Name string `xml:"name,attr"` 56 | Fetch string `xml:"fetch,attr"` 57 | Review string `xml:"review,attr"` 58 | Revision string `xml:"revision,attr"` 59 | } 60 | 61 | type Default struct { 62 | Revision string `xml:"revision,attr"` 63 | Remote string `xml:"remote,attr"` 64 | DestBranch string `xml:"dest-branch,attr"` 65 | SyncJ string `xml:"sync-j,attr"` 66 | SyncC string `xml:"sync-c,attr"` 67 | SyncS string `xml:"sync-s,attr"` 68 | } 69 | 70 | type ManifestServer struct { 71 | URL string `xml:"url,attr"` 72 | } 73 | type Manifest struct { 74 | Default Default `xml:"default"` 75 | Remote Remote `xml:"remote"` 76 | Project []Project `xml:"project"` 77 | } 78 | 79 | func Parse(contents []byte) (*Manifest, error) { 80 | var m Manifest 81 | if err := xml.Unmarshal(contents, &m); err != nil { 82 | return nil, err 83 | } 84 | 85 | for i := range m.Project { 86 | m.Project[i].parse() 87 | } 88 | return &m, nil 89 | } 90 | 91 | func ParseFile(name string) (*Manifest, error) { 92 | content, err := ioutil.ReadFile(name) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return Parse(content) 97 | } 98 | -------------------------------------------------------------------------------- /fs/manifestfs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "path/filepath" 7 | 8 | git "github.com/libgit2/git2go" 9 | "github.com/hanwen/gitfs/manifest" 10 | "github.com/hanwen/go-fuse/fuse/nodefs" 11 | ) 12 | 13 | type manifestFSRoot struct { 14 | nodefs.Node 15 | 16 | manifest manifest.Manifest 17 | fsConn *nodefs.FileSystemConnector 18 | // keyed by name (from the manifest) 19 | repoMap map[string]nodefs.Node 20 | } 21 | 22 | func NewManifestFS(m *manifest.Manifest, repoRoot string, gitOpts *GitFSOptions) (nodefs.Node, error) { 23 | filtered := *m 24 | filtered.Project = nil 25 | for _, p := range m.Project { 26 | if p.Groups["notdefault"] { 27 | continue 28 | } 29 | filtered.Project = append(filtered.Project, p) 30 | } 31 | 32 | root := &manifestFSRoot{ 33 | Node: nodefs.NewDefaultNode(), 34 | repoMap: map[string]nodefs.Node{}, 35 | manifest: filtered, 36 | } 37 | 38 | type result struct { 39 | name string 40 | node nodefs.Node 41 | err error 42 | } 43 | 44 | ch := make(chan result, len(root.manifest.Project)) 45 | for _, p := range root.manifest.Project { 46 | go func (p manifest.Project) { 47 | // the spec isn't clear about this, but the git repo 48 | // is placed locally at p.Path rather than p.Name 49 | repo, err := git.OpenRepository(filepath.Join(repoRoot, p.Path) + ".git") 50 | if err != nil { 51 | ch <- result{err: err} 52 | return 53 | } 54 | 55 | remote := p.Remote 56 | revision := p.Revision 57 | if revision == "" { 58 | revision = root.manifest.Default.Revision 59 | } 60 | if remote == "" { 61 | remote = root.manifest.Remote.Name 62 | } 63 | 64 | commit := filepath.Join(remote, revision) 65 | projectRoot, err := NewTreeFSRoot(repo, commit, gitOpts) 66 | ch <- result{p.Name, projectRoot, err} 67 | }(p) 68 | } 69 | 70 | var firstError error 71 | for _ = range root.manifest.Project { 72 | res := <-ch 73 | if firstError != nil { 74 | continue 75 | } 76 | if res.err != nil { 77 | firstError = res.err 78 | } else { 79 | root.repoMap[res.name] = res.node 80 | } 81 | } 82 | if firstError != nil { 83 | return nil, firstError 84 | } 85 | return root, nil 86 | } 87 | 88 | func parents(path string) []string { 89 | var r []string 90 | for { 91 | path = filepath.Dir(path) 92 | if path == "." { 93 | break 94 | } 95 | r = append(r, path) 96 | } 97 | return r 98 | } 99 | 100 | 101 | func (r *manifestFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) { 102 | r.fsConn = fsConn 103 | 104 | todo := map[string]manifest.Project{} 105 | for _, project := range r.manifest.Project { 106 | if project.Groups["notdefault"] { 107 | continue 108 | } 109 | 110 | todo[project.Path] = project 111 | } 112 | 113 | for len(todo) > 0 { 114 | next := map[string]manifest.Project{} 115 | var wg sync.WaitGroup 116 | for _, t := range todo { 117 | foundParent := false 118 | for _, p := range parents(t.Path) { 119 | if _, ok := todo[p] ; ok { 120 | foundParent = true 121 | break 122 | } 123 | } 124 | 125 | if !foundParent { 126 | wg.Add(1) 127 | go func(p manifest.Project) { 128 | r.addRepo(&p) 129 | wg.Done() 130 | }(t) 131 | } else { 132 | next[t.Path] = t 133 | } 134 | } 135 | wg.Wait() 136 | todo = next 137 | } 138 | } 139 | 140 | func (r *manifestFSRoot) addRepo(project *manifest.Project) { 141 | node, components := r.fsConn.Node(r.Inode(), project.Path) 142 | if len(components) == 0 { 143 | log.Fatalf("huh %v", *project) 144 | } 145 | last := len(components) - 1 146 | for _, c := range components[:last] { 147 | node = node.NewChild(c, true, nodefs.NewDefaultNode()) 148 | } 149 | 150 | rootNode := r.repoMap[project.Name] 151 | if rootNode == nil { 152 | panic(project.Name) 153 | } 154 | if code := r.fsConn.Mount(node, components[last], rootNode, nil); !code.Ok() { 155 | // TODO - this cannot happen if the manifest 156 | // is well formed, but should check that in a 157 | // place where we can return error. 158 | log.Printf("Mount: %v - %v", project, code) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /fs/multifs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "syscall" 9 | "time" 10 | 11 | git "github.com/libgit2/git2go" 12 | 13 | "github.com/hanwen/go-fuse/fuse" 14 | "github.com/hanwen/go-fuse/fuse/nodefs" 15 | "github.com/hanwen/go-fuse/fuse/pathfs" 16 | ) 17 | 18 | type multiGitFS struct { 19 | fsConn *nodefs.FileSystemConnector 20 | root nodefs.Node 21 | opts *GitFSOptions 22 | } 23 | 24 | func NewMultiGitFSRoot(opts *GitFSOptions) nodefs.Node { 25 | fs := &multiGitFS{opts: opts} 26 | fs.root = &multiGitRoot{nodefs.NewDefaultNode(), fs} 27 | return fs.root 28 | } 29 | 30 | type multiGitRoot struct { 31 | nodefs.Node 32 | fs *multiGitFS 33 | } 34 | 35 | func (r *multiGitRoot) OnMount(fsConn *nodefs.FileSystemConnector) { 36 | r.fs.fsConn = fsConn 37 | r.Inode().NewChild("config", true, r.fs.newConfigNode(r)) 38 | } 39 | 40 | type configNode struct { 41 | fs *multiGitFS 42 | 43 | nodefs.Node 44 | 45 | // non-config node corresponding to this one. 46 | corresponding nodefs.Node 47 | } 48 | 49 | func (fs *multiGitFS) newConfigNode(corresponding nodefs.Node) *configNode { 50 | return &configNode{ 51 | fs: fs, 52 | Node: nodefs.NewDefaultNode(), 53 | corresponding: corresponding, 54 | } 55 | } 56 | 57 | type gitConfigNode struct { 58 | nodefs.Node 59 | 60 | content string 61 | } 62 | 63 | func newGitConfigNode(content string) *gitConfigNode { 64 | return &gitConfigNode{ 65 | Node: nodefs.NewDefaultNode(), 66 | content: content, 67 | } 68 | } 69 | 70 | func (n *gitConfigNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) { 71 | out.Mode = syscall.S_IFLNK 72 | return fuse.OK 73 | } 74 | 75 | func (n *gitConfigNode) Readlink(c *fuse.Context) ([]byte, fuse.Status) { 76 | return []byte(n.content), fuse.OK 77 | } 78 | 79 | func (n *configNode) Mkdir(name string, mode uint32, context *fuse.Context) (*nodefs.Inode, fuse.Status) { 80 | corr := n.corresponding.Inode().NewChild(name, true, nodefs.NewDefaultNode()) 81 | c := n.fs.newConfigNode(corr.Node()) 82 | return n.Inode().NewChild(name, true, c), fuse.OK 83 | } 84 | 85 | func (n *configNode) Unlink(name string, context *fuse.Context) (code fuse.Status) { 86 | linkInode := n.Inode().GetChild(name) 87 | if linkInode == nil { 88 | return fuse.ENOENT 89 | } 90 | 91 | _, ok := linkInode.Node().(*gitConfigNode) 92 | if !ok { 93 | log.Printf("gitfs: removing %q, child is not a gitConfigNode", name) 94 | return fuse.EINVAL 95 | } 96 | 97 | root := n.corresponding.Inode().GetChild(name) 98 | if root == nil { 99 | return fuse.EINVAL 100 | } 101 | 102 | code = n.fs.fsConn.Unmount(root) 103 | if code.Ok() { 104 | n.Inode().RmChild(name) 105 | } 106 | return code 107 | } 108 | 109 | // Returns a TreeFS for the given repository. The uri must have the format REPO-DIR:TREEISH. 110 | func NewGitFSRoot(uri string, opts *GitFSOptions) (nodefs.Node, error) { 111 | components := strings.Split(uri, ":") 112 | if len(components) != 2 { 113 | return nil, fmt.Errorf("must have 2 components: %q", uri) 114 | } 115 | 116 | if fi, err := os.Lstat(components[0]); err != nil { 117 | return nil, err 118 | } else if !fi.IsDir() { 119 | return nil, syscall.ENOTDIR 120 | } 121 | 122 | repo, err := git.OpenRepository(components[0]) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | root, err := NewTreeFSRoot(repo, components[1], opts) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | return root, nil 133 | } 134 | 135 | func (n *configNode) Symlink(name string, content string, context *fuse.Context) (*nodefs.Inode, fuse.Status) { 136 | dir := content 137 | components := strings.Split(content, ":") 138 | if len(components) > 2 || len(components) == 0 { 139 | return nil, fuse.Status(syscall.EINVAL) 140 | } 141 | 142 | var root nodefs.Node 143 | if len(components) == 2 { 144 | dir = components[0] 145 | } 146 | 147 | if fi, err := os.Lstat(dir); err != nil { 148 | return nil, fuse.ToStatus(err) 149 | } else if !fi.IsDir() { 150 | return nil, fuse.Status(syscall.ENOTDIR) 151 | } 152 | 153 | var opts *nodefs.Options 154 | if len(components) == 1 { 155 | root = pathfs.NewPathNodeFs(pathfs.NewLoopbackFileSystem(content), nil).Root() 156 | } else { 157 | var err error 158 | root, err = NewGitFSRoot(content, n.fs.opts) 159 | if err != nil { 160 | log.Printf("NewGitFSRoot(%q): %v", content, err) 161 | return nil, fuse.ENOENT 162 | } 163 | opts = &nodefs.Options{ 164 | EntryTimeout: time.Hour, 165 | NegativeTimeout: time.Hour, 166 | AttrTimeout: time.Hour, 167 | PortableInodes: true, 168 | } 169 | } 170 | 171 | if code := n.fs.fsConn.Mount(n.corresponding.Inode(), name, root, opts); !code.Ok() { 172 | return nil, code 173 | } 174 | 175 | linkNode := newGitConfigNode(content) 176 | return n.Inode().NewChild(name, false, linkNode), fuse.OK 177 | } 178 | -------------------------------------------------------------------------------- /fs/fs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/hanwen/go-fuse/fuse" 11 | "github.com/hanwen/go-fuse/fuse/nodefs" 12 | 13 | git "github.com/libgit2/git2go" 14 | ) 15 | 16 | func setupRepo(dir string) (*git.Repository, error) { 17 | repo, err := git.InitRepository(dir, false) 18 | if err != nil { 19 | return nil, err 20 | } 21 | odb, err := repo.Odb() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | blobId, err := odb.Write([]byte("hello"), git.ObjectBlob) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | subTree, err := repo.TreeBuilder() 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer subTree.Free() 36 | 37 | if err = subTree.Insert("subfile", blobId, git.FilemodeBlobExecutable); err != nil { 38 | return nil, err 39 | } 40 | treeId, err := subTree.Write() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | rootTree, err := repo.TreeBuilder() 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer rootTree.Free() 50 | 51 | if err := rootTree.Insert("dir", treeId, git.FilemodeTree); err != nil { 52 | return nil, err 53 | } 54 | if err := rootTree.Insert("file", blobId, git.FilemodeBlob); err != nil { 55 | return nil, err 56 | } 57 | if err = rootTree.Insert("link", blobId, git.FilemodeLink); err != nil { 58 | return nil, err 59 | } 60 | 61 | rootId, err := rootTree.Write() 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | root, err := repo.LookupTree(rootId) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | sig := &git.Signature{"user", "user@invalid", time.Now()} 72 | if _, err := repo.CreateCommit("refs/heads/master", sig, sig, 73 | "message", root); err != nil { 74 | return nil, err 75 | } 76 | 77 | return repo, nil 78 | } 79 | 80 | type testCase struct { 81 | repo *git.Repository 82 | server *fuse.Server 83 | mnt string 84 | } 85 | 86 | func (tc *testCase) Cleanup() { 87 | tc.server.Unmount() 88 | tc.repo.Free() 89 | } 90 | 91 | func setupBasic(opts *GitFSOptions) (*testCase, error) { 92 | dir, err := ioutil.TempDir("", "fs_test") 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | repo, err := setupRepo(filepath.Join(dir, "repo")) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | root, err := NewTreeFSRoot(repo, "refs/heads/master", nil) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | mnt := filepath.Join(dir, "mnt") 108 | if err := os.Mkdir(mnt, 0755); err != nil { 109 | return nil, err 110 | } 111 | 112 | server, _, err := nodefs.MountRoot(mnt, root, nil) 113 | server.SetDebug(true) 114 | go server.Serve() 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return &testCase{ 120 | repo, 121 | server, 122 | mnt, 123 | }, nil 124 | } 125 | 126 | func TestBasic(t *testing.T) { 127 | tc, err := setupBasic(nil) 128 | if err != nil { 129 | t.Fatalf("setup: %v", err) 130 | } 131 | defer tc.Cleanup() 132 | 133 | testGitFS(tc.mnt, t) 134 | } 135 | 136 | func TestBasicLazy(t *testing.T) { 137 | tc, err := setupBasic(&GitFSOptions{Disk: true}) 138 | if err != nil { 139 | t.Fatalf("setup: %v", err) 140 | } 141 | defer tc.Cleanup() 142 | 143 | testGitFS(tc.mnt, t) 144 | } 145 | 146 | func TestSymlink(t *testing.T) { 147 | tc, err := setupBasic(nil) 148 | if err != nil { 149 | t.Fatalf("setup: %v", err) 150 | } 151 | defer tc.Cleanup() 152 | 153 | if err := os.Symlink("content", tc.mnt+"/mylink"); err != nil { 154 | t.Fatalf("Symlink: %v", err) 155 | } 156 | if content, err := os.Readlink(tc.mnt + "/mylink"); err != nil { 157 | t.Fatalf("Readlink: %v", err) 158 | } else if content != "content" { 159 | t.Fatalf("got %q, want %q", content, "content") 160 | } 161 | 162 | if err := os.Remove(tc.mnt + "/link"); err == nil { 163 | t.Fatalf("removed r/o file") 164 | } 165 | 166 | if err := os.Remove(tc.mnt + "/mylink"); err != nil { 167 | t.Fatalf("Remove: %v") 168 | } 169 | 170 | if fi, err := os.Lstat(tc.mnt + "/mylink"); err == nil { 171 | t.Fatalf("link still there: %v", fi) 172 | } 173 | } 174 | 175 | func testGitFS(mnt string, t *testing.T) { 176 | fi, err := os.Lstat(mnt + "/file") 177 | if err != nil { 178 | t.Fatalf("Lstat: %v", err) 179 | } else if fi.IsDir() { 180 | t.Fatalf("got mode %v, want file", fi.Mode()) 181 | } else if fi.Size() != 5 { 182 | t.Fatalf("got size %d, want file size 5", fi.Size()) 183 | } 184 | 185 | if fi, err := os.Lstat(mnt + "/dir"); err != nil { 186 | t.Fatalf("Lstat: %v", err) 187 | } else if !fi.IsDir() { 188 | t.Fatalf("got %v, want dir", fi) 189 | } 190 | 191 | if fi, err := os.Lstat(mnt + "/dir/subfile"); err != nil { 192 | t.Fatalf("Lstat: %v", err) 193 | } else if fi.IsDir() || fi.Size() != 5 || fi.Mode()&0x111 == 0 { 194 | t.Fatalf("got %v, want +x file size 5", fi) 195 | } 196 | 197 | if fi, err := os.Lstat(mnt + "/link"); err != nil { 198 | t.Fatalf("Lstat: %v", err) 199 | } else if fi.Mode()&os.ModeSymlink == 0 { 200 | t.Fatalf("got %v, want symlink", fi.Mode()) 201 | } 202 | 203 | if content, err := ioutil.ReadFile(mnt + "/file"); err != nil { 204 | t.Fatalf("ReadFile: %v", err) 205 | } else if string(content) != "hello" { 206 | t.Errorf("got %q, want %q", content, "hello") 207 | } 208 | } 209 | 210 | func setupMulti() (*testCase, error) { 211 | dir, err := ioutil.TempDir("", "fs_test") 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | repo, err := setupRepo(filepath.Join(dir, "repo")) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | root := NewMultiGitFSRoot(nil) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | mnt := filepath.Join(dir, "mnt") 227 | if err := os.Mkdir(mnt, 0755); err != nil { 228 | return nil, err 229 | } 230 | 231 | server, _, err := nodefs.MountRoot(mnt, root, nil) 232 | go server.Serve() 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | return &testCase{ 238 | repo, 239 | server, 240 | mnt, 241 | }, nil 242 | } 243 | 244 | func TestMultiFS(t *testing.T) { 245 | tc, err := setupMulti() 246 | if err != nil { 247 | t.Fatalf("setup: %v", err) 248 | } 249 | defer tc.Cleanup() 250 | 251 | if err := os.Mkdir(tc.mnt+"/config/sub", 0755); err != nil { 252 | t.Fatalf("Mkdir %v", err) 253 | } 254 | 255 | if fi, err := os.Lstat(tc.mnt + "/sub"); err != nil { 256 | t.Fatalf("Lstat: %v", err) 257 | } else if !fi.IsDir() { 258 | t.Fatalf("want dir, got %v", fi.Mode()) 259 | } 260 | 261 | if err := os.Symlink(tc.repo.Path()+":master", tc.mnt+"/config/sub/repo"); err != nil { 262 | t.Fatalf("Symlink: %v", err) 263 | } 264 | entries, err := ioutil.ReadDir(tc.mnt + "/sub") 265 | if err != nil { 266 | t.Fatalf("ReadDir: %v", err) 267 | } 268 | if len(entries) != 1 { 269 | t.Fatalf("got %v, want 2 entries", entries) 270 | } 271 | 272 | testGitFS(tc.mnt+"/sub/repo", t) 273 | 274 | // Ugh. the RELEASE opcode is not synchronized, so it 275 | // may not be completed while we try the unmount. 276 | time.Sleep(time.Millisecond) 277 | if err := os.Remove(tc.mnt + "/config/sub/repo"); err != nil { 278 | t.Fatalf("Remove: %v", err) 279 | } 280 | 281 | if _, err := os.Lstat(tc.mnt + "/sub/repo"); err == nil { 282 | t.Errorf("repo is still there.") 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "sync" 10 | "syscall" 11 | 12 | git "github.com/libgit2/git2go" 13 | 14 | "github.com/hanwen/go-fuse/fuse" 15 | "github.com/hanwen/go-fuse/fuse/nodefs" 16 | ) 17 | 18 | type treeFS struct { 19 | repo *git.Repository 20 | opts GitFSOptions 21 | } 22 | 23 | type GitFSOptions struct { 24 | Lazy bool 25 | Disk bool 26 | TempDir string 27 | } 28 | 29 | // NewTreeFS creates a git Tree FS. The treeish should resolve to tree SHA1. 30 | func NewTreeFSRoot(repo *git.Repository, treeish string, opts *GitFSOptions) (nodefs.Node, error) { 31 | obj, err := repo.RevparseSingle(treeish) 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer obj.Free() 36 | 37 | var treeId *git.Oid 38 | switch obj.Type() { 39 | case git.ObjectCommit: 40 | commit, err := repo.LookupCommit(obj.Id()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | treeId = commit.TreeId() 45 | case git.ObjectTree: 46 | treeId = obj.Id() 47 | default: 48 | return nil, fmt.Errorf("gitfs: unsupported object type %d", obj.Type()) 49 | } 50 | 51 | if opts == nil { 52 | opts = &GitFSOptions{ 53 | Lazy: true, 54 | Disk: false, 55 | } 56 | } 57 | if opts.TempDir == "" { 58 | opts.TempDir, err = ioutil.TempDir("", "gitfs") 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | t := &treeFS{ 65 | repo: repo, 66 | opts: *opts, 67 | } 68 | root := t.newDirNode(treeId) 69 | return root, nil 70 | } 71 | 72 | func (t *treeFS) onMount(root *dirNode) { 73 | tree, err := t.repo.LookupTree(root.id) 74 | if err != nil { 75 | panic(err) 76 | } 77 | defer tree.Free() 78 | if root.Inode() == nil { 79 | panic("nil?") 80 | } 81 | t.recurse(tree, root) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | type mutableLink struct { 88 | nodefs.Node 89 | content []byte 90 | } 91 | 92 | func (n *mutableLink) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) { 93 | out.Mode = fuse.S_IFLNK 94 | return fuse.OK 95 | } 96 | 97 | func (n *mutableLink) Readlink(c *fuse.Context) ([]byte, fuse.Status) { 98 | return n.content, fuse.OK 99 | } 100 | 101 | type gitNode struct { 102 | fs *treeFS 103 | id *git.Oid 104 | nodefs.Node 105 | } 106 | 107 | type dirNode struct { 108 | gitNode 109 | } 110 | 111 | func (n *dirNode) OnMount(conn *nodefs.FileSystemConnector) { 112 | n.fs.onMount(n) 113 | } 114 | 115 | func (n *dirNode) Symlink(name string, content string, context *fuse.Context) (*nodefs.Inode, fuse.Status) { 116 | l := &mutableLink{nodefs.NewDefaultNode(), []byte(content)} 117 | return n.Inode().NewChild(name, false, l), fuse.OK 118 | } 119 | 120 | func (n *dirNode) Unlink(name string, context *fuse.Context) (code fuse.Status) { 121 | ch := n.Inode().GetChild(name) 122 | if ch == nil { 123 | return fuse.ENOENT 124 | } 125 | 126 | if _, ok := ch.Node().(*mutableLink); !ok { 127 | return fuse.EPERM 128 | } 129 | 130 | n.Inode().RmChild(name) 131 | return fuse.OK 132 | } 133 | 134 | type blobNode struct { 135 | gitNode 136 | mode git.Filemode 137 | size uint64 138 | } 139 | 140 | type linkNode struct { 141 | gitNode 142 | target []byte 143 | } 144 | 145 | func (n *linkNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) { 146 | out.Mode = fuse.S_IFLNK 147 | return fuse.OK 148 | } 149 | 150 | func (n *linkNode) Readlink(c *fuse.Context) ([]byte, fuse.Status) { 151 | return n.target, fuse.OK 152 | } 153 | 154 | func (n *blobNode) Open(flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) { 155 | if flags&fuse.O_ANYWRITE != 0 { 156 | return nil, fuse.EPERM 157 | } 158 | 159 | ctor := n.LoadMemory 160 | if n.fs.opts.Disk { 161 | ctor = n.LoadDisk 162 | } 163 | 164 | if !n.fs.opts.Lazy { 165 | f, err := ctor() 166 | if err != nil { 167 | return nil, fuse.ToStatus(err) 168 | } 169 | return f, fuse.OK 170 | } 171 | 172 | return &lazyBlobFile{ 173 | ctor: ctor, 174 | node: n, 175 | }, fuse.OK 176 | } 177 | 178 | func (n *blobNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) { 179 | out.Mode = uint32(n.mode) 180 | out.Size = uint64(n.size) 181 | return fuse.OK 182 | } 183 | 184 | func (t *treeFS) newLinkNode(id *git.Oid) (nodefs.Node, error) { 185 | n := &linkNode{ 186 | gitNode: gitNode{ 187 | fs: t, 188 | id: id.Copy(), 189 | Node: nodefs.NewDefaultNode(), 190 | }, 191 | } 192 | 193 | blob, err := t.repo.LookupBlob(id) 194 | if err != nil { 195 | return nil, err 196 | } 197 | defer blob.Free() 198 | n.target = append([]byte{}, blob.Contents()...) 199 | return n, nil 200 | } 201 | 202 | func (n *blobNode) LoadMemory() (nodefs.File, error) { 203 | blob, err := n.fs.repo.LookupBlob(n.id) 204 | if err != nil { 205 | return nil, err 206 | } 207 | return &memoryFile{ 208 | File: nodefs.NewDefaultFile(), 209 | blob: blob, 210 | }, nil 211 | } 212 | 213 | type lazyBlobFile struct { 214 | mu sync.Mutex 215 | nodefs.File 216 | ctor func() (nodefs.File, error) 217 | node *blobNode 218 | } 219 | 220 | func (f *lazyBlobFile) SetInode(n *nodefs.Inode) { 221 | } 222 | 223 | func (f *lazyBlobFile) Read(dest []byte, off int64) (fuse.ReadResult, fuse.Status) { 224 | f.mu.Lock() 225 | defer f.mu.Unlock() 226 | if f.File == nil { 227 | g, err := f.ctor() 228 | if err != nil { 229 | log.Printf("opening blob for %s: %v", f.node.id.String(), err) 230 | return nil, fuse.EIO 231 | } 232 | f.File = g 233 | } 234 | return f.File.Read(dest, off) 235 | } 236 | 237 | func (f *lazyBlobFile) Flush() fuse.Status { 238 | f.mu.Lock() 239 | defer f.mu.Unlock() 240 | if f.File != nil { 241 | return f.File.Flush() 242 | } 243 | return fuse.OK 244 | } 245 | 246 | func (f *lazyBlobFile) Release() { 247 | f.mu.Lock() 248 | defer f.mu.Unlock() 249 | if f.File != nil { 250 | f.File.Release() 251 | } 252 | } 253 | 254 | type memoryFile struct { 255 | nodefs.File 256 | blob *git.Blob 257 | } 258 | 259 | func (f *memoryFile) Read(dest []byte, off int64) (fuse.ReadResult, fuse.Status) { 260 | b := f.blob.Contents() 261 | end := off + int64(len(dest)) 262 | if end > int64(len(b)) { 263 | end = int64(len(b)) 264 | } 265 | return fuse.ReadResultData(b[off:end]), fuse.OK 266 | } 267 | 268 | func (f *memoryFile) Release() { 269 | f.blob.Free() 270 | } 271 | 272 | func (n *blobNode) LoadDisk() (nodefs.File, error) { 273 | p := filepath.Join(n.fs.opts.TempDir, n.id.String()) 274 | if _, err := os.Lstat(p); os.IsNotExist(err) { 275 | blob, err := n.fs.repo.LookupBlob(n.id) 276 | if err != nil { 277 | return nil, err 278 | } 279 | defer blob.Free() 280 | 281 | // TODO - atomic, use content store to share content. 282 | if err := ioutil.WriteFile(p, blob.Contents(), 0644); err != nil { 283 | return nil, err 284 | } 285 | } 286 | f, err := os.Open(p) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | return nodefs.NewLoopbackFile(f), nil 292 | } 293 | 294 | func (t *treeFS) newBlobNode(id *git.Oid, mode git.Filemode) (nodefs.Node, error) { 295 | n := &blobNode{ 296 | gitNode: gitNode{ 297 | fs: t, 298 | id: id.Copy(), 299 | Node: nodefs.NewDefaultNode(), 300 | }, 301 | } 302 | odb, err := t.repo.Odb() 303 | if err != nil { 304 | return nil, err 305 | } 306 | defer odb.Free() 307 | sz, _, err := odb.ReadHeader(id) 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | n.size = sz 313 | n.mode = mode 314 | return n, nil 315 | } 316 | 317 | func (t *treeFS) newDirNode(id *git.Oid) nodefs.Node { 318 | n := &dirNode{ 319 | gitNode: gitNode{ 320 | fs: t, 321 | id: id.Copy(), 322 | Node: nodefs.NewDefaultNode(), 323 | }, 324 | } 325 | return n 326 | } 327 | 328 | func (t *treeFS) recurse(tree *git.Tree, n nodefs.Node) error { 329 | for i := uint64(0); ; i++ { 330 | e := tree.EntryByIndex(i) 331 | if e == nil { 332 | break 333 | } 334 | isdir := e.Filemode&syscall.S_IFDIR != 0 335 | var chNode nodefs.Node 336 | if isdir { 337 | chNode = t.newDirNode(e.Id) 338 | } else if e.Filemode&^07777 == syscall.S_IFLNK { 339 | l, err := t.newLinkNode(e.Id) 340 | if err != nil { 341 | return err 342 | } 343 | chNode = l 344 | } else if e.Filemode&^07777 == syscall.S_IFREG { 345 | b, err := t.newBlobNode(e.Id, e.Filemode) 346 | if err != nil { 347 | return err 348 | } 349 | chNode = b 350 | } else { 351 | panic(e) 352 | } 353 | n.Inode().NewChild(e.Name, isdir, chNode) 354 | if isdir { 355 | tree, err := t.repo.LookupTree(e.Id) 356 | if err != nil { 357 | return err 358 | } 359 | 360 | if err := t.recurse(tree, chNode); err != nil { 361 | return nil 362 | } 363 | } 364 | } 365 | return nil 366 | } 367 | --------------------------------------------------------------------------------