Skip to content

Commit d23b943

Browse files
authored
Merge pull request #93 from infosiftr/gitfs-symlinks
Fix gitfs symlink handling
2 parents ca06476 + 722f7d6 commit d23b943

File tree

2 files changed

+93
-8
lines changed

2 files changed

+93
-8
lines changed

pkg/gitfs/fs.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ func CommitHash(repo *goGit.Repository, commit string) (fs.FS, error) {
3434
if err != nil {
3535
return nil, err
3636
}
37+
f := &gitFS{
38+
storer: repo.Storer,
39+
tree: tree,
40+
name: ".",
41+
Mod: CommitTime(gitCommit),
42+
}
43+
f.root = f
3744
return gitFSFS{
38-
gitFS: &gitFS{
39-
storer: repo.Storer,
40-
tree: tree,
41-
name: ".",
42-
Mod: CommitTime(gitCommit),
43-
},
45+
gitFS: f,
4446
}, nil
4547
}
4648

@@ -54,6 +56,8 @@ type gitFSFS struct {
5456
// https://pkg.go.dev/io/fs#FileInfo
5557
// https://pkg.go.dev/io/fs#DirEntry
5658
type gitFS struct {
59+
root *gitFS // used so we can rewind back to the root if we need to (see symlink handling code; should *only* be set in CommitHash / constructors)
60+
5761
storer goGitPlumbingStorer.EncodedObjectStorer
5862
tree *goGitPlumbingObject.Tree
5963
entry *goGitPlumbingObject.TreeEntry // might be nil ("." at the top-level of the repo)
@@ -172,7 +176,10 @@ func (f gitFS) statEntry(name string, entry *goGitPlumbingObject.TreeEntry, foll
172176
if target, err := fi.resolveLink(); err != nil {
173177
return nil, err
174178
} else if target != "" {
175-
return f.stat(target, followSymlinks)
179+
// the value from resolveLink is relative to the root
180+
return f.root.stat(target, followSymlinks)
181+
// ideally this would "just" use "path.Rel" to make "target" relative to "f.name" instead, but "path.Rel" does not exist and only "filepath.Rel" does which would break this code on Windows, so instead we added a "root" pointer that we pass around forever that links us back to the root of our "Tree"
182+
// we could technically solve this by judicious use of "../" (with enough "../" to catch all the "/" in "f.name"), but it seems simpler and more obvious (and less error prone) to just pass around a pointer to the root
176183
}
177184
}
178185

pkg/gitfs/fs_test.go

+79-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/docker-library/bashbrew/pkg/gitfs"
99

1010
"github.com/go-git/go-git/v5"
11+
goGitConfig "github.com/go-git/go-git/v5/config"
1112
"github.com/go-git/go-git/v5/storage/memory"
1213
)
1314

@@ -52,7 +53,7 @@ func TestCommitFS(t *testing.T) {
5253
})
5354
}
5455

55-
func TestSymlinkFS(t *testing.T) {
56+
func TestRootSymlinkFS(t *testing.T) {
5657
// 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)
5758
repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
5859
URL: "https://github.com/tianon/gosu.git", // just a repository with a known symlink (`.dockerignore` -> `.gitignore`)
@@ -93,3 +94,80 @@ func TestSymlinkFS(t *testing.T) {
9394
}
9495
})
9596
}
97+
98+
func TestSubdirSymlinkFS(t *testing.T) {
99+
// 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)
100+
// Init + CreateRemoteAnonymous + Fetch because Clone doesn't support fetch-by-commit
101+
repo, err := git.Init(memory.NewStorage(), nil)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
remote, err := repo.CreateRemoteAnonymous(&goGitConfig.RemoteConfig{
106+
Name: "anonymous",
107+
URLs: []string{"https://github.com/docker-library/busybox.git"}, // just a repository with a known symlink at a non-root level (`latest/musl/amd64/blobs/sha256/6e5e0f90c009d12db9478afe5656920e7bdd548e9fd8f50eab2be694102ae318` -> `../../image-config.json`)
108+
})
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
commit := "668d52e6f0596e0fd0b1be1d8267c4b9240dc2b3"
113+
err = remote.Fetch(&git.FetchOptions{
114+
RefSpecs: []goGitConfig.RefSpec{goGitConfig.RefSpec(commit + ":FETCH_HEAD")},
115+
Tags: git.NoTags,
116+
})
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
f, err := gitfs.CommitHash(repo, commit)
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
125+
t.Run("Open+ReadAll", func(t *testing.T) {
126+
r, err := f.Open("latest/musl/amd64/blobs/sha256/6e5e0f90c009d12db9478afe5656920e7bdd548e9fd8f50eab2be694102ae318")
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
defer func() {
131+
if err := r.Close(); err != nil {
132+
t.Fatal(err)
133+
}
134+
}()
135+
b, err := io.ReadAll(r)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
expected := `{
140+
"config": {
141+
"Cmd": [
142+
"sh"
143+
]
144+
},
145+
"created": "2023-05-18T22:34:17Z",
146+
"history": [
147+
{
148+
"created": "2023-05-18T22:34:17Z",
149+
"created_by": "BusyBox 1.36.1 (musl), Alpine 3.19.1"
150+
}
151+
],
152+
"rootfs": {
153+
"type": "layers",
154+
"diff_ids": [
155+
"sha256:994bf8f4adc78c5c1e4a6b5e3b59ad57902b301e0e79255a3e95ea4b213a76bd"
156+
]
157+
},
158+
"architecture": "amd64",
159+
"os": "linux"
160+
}
161+
`
162+
if string(b) != expected {
163+
t.Fatalf("expected %q, got %q", expected, string(b))
164+
}
165+
})
166+
167+
// might as well run fstest again, now that we have a new filesystem tree 😅
168+
t.Run("fstest.TestFS", func(t *testing.T) {
169+
if err := fstest.TestFS(f, "latest/musl/amd64/blobs/sha256/6e5e0f90c009d12db9478afe5656920e7bdd548e9fd8f50eab2be694102ae318", "latest/musl/amd64/index.json"); err != nil {
170+
t.Fatal(err)
171+
}
172+
})
173+
}

0 commit comments

Comments
 (0)