Skip to content

Commit 5e4f19f

Browse files
authored
Merge pull request #120 from infosiftr/gitfs-submodules
Add support for submodules in gitfs
2 parents d422b8f + fb05da1 commit 5e4f19f

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

pkg/gitfs/fs.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ func (f gitFS) statEntry(name string, entry *goGitPlumbingObject.TreeEntry, foll
158158
fi.entry = entry
159159
fi.name = path.Join(fi.name, name)
160160

161+
if fi.isSubmodule() {
162+
return fi, nil
163+
}
164+
161165
if fi.IsDir() {
162166
fi.tree, err = goGitPlumbingObject.GetTree(f.storer, entry.Hash) // see https://github.com/go-git/go-git/blob/v5.11.0/plumbing/object/tree.go#L103
163167
if err != nil {
@@ -309,6 +313,16 @@ func (f gitFS) ReadDir(n int) ([]fs.DirEntry, error) {
309313
return nil, fmt.Errorf("%q not open (or not a directory)", f.name)
310314
}
311315
ret := []fs.DirEntry{}
316+
if f.isSubmodule() {
317+
// https://pkg.go.dev/io/fs#ReadDirFile
318+
// "If n > 0, ... At the end of a directory, the error is io.EOF."
319+
// "If n <= 0, ... it returns the slice and a nil error."
320+
err := io.EOF
321+
if n <= 0 {
322+
err = nil
323+
}
324+
return ret, err
325+
}
312326
for i := 0; n <= 0 || i < n; i++ {
313327
name, entry, err := f.walker.Next()
314328
if err != nil {
@@ -354,10 +368,18 @@ func (f gitFS) Mode() fs.FileMode {
354368
return 0775
355369
case goGitPlumbingFileMode.Dir:
356370
return 0775 | fs.ModeDir
371+
case goGitPlumbingFileMode.Submodule:
372+
// TODO handle submodules better / more explicitly ("git archive" presents them as empty directories, so we do too, for now)
373+
return 0775 | fs.ModeDir
357374
}
358375
return 0 | fs.ModeIrregular // TODO what to do for files whose types we don't support? 😬
359376
}
360377

378+
func (f gitFS) isSubmodule() bool {
379+
// TODO handle submodules better / more explicitly ("git archive" presents them as empty directories, so we do too, for now)
380+
return f.entry != nil && f.entry.Mode == goGitPlumbingFileMode.Submodule
381+
}
382+
361383
// https://pkg.go.dev/io/fs#FileInfo: modification time
362384
func (f gitFS) ModTime() time.Time {
363385
return f.Mod

pkg/gitfs/fs_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gitfs_test
22

33
import (
44
"io"
5+
"io/fs"
56
"testing"
67
"testing/fstest"
78

@@ -171,3 +172,70 @@ func TestSubdirSymlinkFS(t *testing.T) {
171172
}
172173
})
173174
}
175+
176+
func TestSubmoduleFS(t *testing.T) {
177+
// TODO instead of cloning a remote repository, synthesize a very simple Git repository right in the test here (benefit of the remote repository is that it's much larger, so fstest.TestFS has a lot more data to test against)
178+
// Init + CreateRemoteAnonymous + Fetch because Clone doesn't support fetch-by-commit
179+
repo, err := git.Init(memory.NewStorage(), nil)
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
remote, err := repo.CreateRemoteAnonymous(&goGitConfig.RemoteConfig{
184+
Name: "anonymous",
185+
URLs: []string{"https://github.com/debuerreotype/debuerreotype.git"}, // just a repository with a known submodule (`./validate/`)
186+
})
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
commit := "d12af8e5556e39f82082b44628288e2eb27d4c34"
191+
err = remote.Fetch(&git.FetchOptions{
192+
RefSpecs: []goGitConfig.RefSpec{goGitConfig.RefSpec(commit + ":FETCH_HEAD")},
193+
Tags: git.NoTags,
194+
})
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
f, err := gitfs.CommitHash(repo, commit)
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
203+
t.Run("Stat", func(t *testing.T) {
204+
fi, err := fs.Stat(f, "validate")
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
if fi.Mode().IsRegular() {
209+
t.Fatal("validate should not be a regular file")
210+
}
211+
if !fi.IsDir() {
212+
t.Fatal("validate should be a directory but isn't")
213+
}
214+
})
215+
t.Run("ReadDir", func(t *testing.T) {
216+
entries, err := fs.ReadDir(f, "validate")
217+
if err != nil {
218+
t.Fatal(err)
219+
}
220+
if len(entries) != 0 {
221+
t.Fatalf("validate should have 0 entries, not %d\n\n%#v", len(entries), entries)
222+
}
223+
})
224+
225+
// might as well run fstest again, now that we have a new filesystem tree 😅
226+
t.Run("fstest.TestFS", func(t *testing.T) {
227+
if err := fstest.TestFS(f, "Dockerfile"); err != nil {
228+
t.Fatal(err)
229+
}
230+
})
231+
t.Run("Sub+fstest.TestFS", func(t *testing.T) {
232+
sub, err := fs.Sub(f, "validate")
233+
if err != nil {
234+
t.Fatal(err)
235+
}
236+
// "As a special case, if no expected files are listed, fsys must be empty."
237+
if err := fstest.TestFS(sub); err != nil {
238+
t.Fatal(err)
239+
}
240+
})
241+
}

0 commit comments

Comments
 (0)