+4
go.mod
+4
go.mod
···
20
20
github.com/pjbgf/sha1cd v0.3.0
21
21
github.com/sergi/go-diff v1.1.0
22
22
github.com/skeema/knownhosts v1.2.1
23
+
github.com/stretchr/testify v1.8.4
23
24
github.com/xanzy/ssh-agent v0.3.3
24
25
golang.org/x/crypto v0.16.0
25
26
golang.org/x/net v0.19.0
···
33
34
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
34
35
github.com/cloudflare/circl v1.3.3 // indirect
35
36
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
37
+
github.com/davecgh/go-spew v1.1.1 // indirect
36
38
github.com/kr/pretty v0.3.1 // indirect
37
39
github.com/kr/text v0.2.0 // indirect
40
+
github.com/pmezard/go-difflib v1.0.0 // indirect
38
41
github.com/rogpeppe/go-internal v1.11.0 // indirect
39
42
golang.org/x/mod v0.12.0 // indirect
40
43
golang.org/x/tools v0.13.0 // indirect
41
44
gopkg.in/warnings.v0 v0.1.2 // indirect
45
+
gopkg.in/yaml.v3 v3.0.1 // indirect
42
46
)
+1
go.sum
+1
go.sum
···
69
69
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
70
70
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
71
71
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
72
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
72
73
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
73
74
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
74
75
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+48
-16
storage/filesystem/dotgit/dotgit.go
+48
-16
storage/filesystem/dotgit/dotgit.go
···
10
10
"os"
11
11
"path"
12
12
"path/filepath"
13
+
"reflect"
13
14
"runtime"
14
15
"sort"
15
16
"strings"
16
17
"time"
17
18
18
-
"github.com/go-git/go-billy/v5/osfs"
19
19
"github.com/go-git/go-git/v5/plumbing"
20
20
"github.com/go-git/go-git/v5/plumbing/hash"
21
21
"github.com/go-git/go-git/v5/storage"
22
22
"github.com/go-git/go-git/v5/utils/ioutil"
23
23
24
24
"github.com/go-git/go-billy/v5"
25
+
"github.com/go-git/go-billy/v5/helper/chroot"
25
26
)
26
27
27
28
const (
···
81
82
// KeepDescriptors makes the file descriptors to be reused but they will
82
83
// need to be manually closed calling Close().
83
84
KeepDescriptors bool
85
+
// AlternatesFS provides the billy filesystem to be used for Git Alternates.
86
+
// If none is provided, it falls back to using the underlying instance used for
87
+
// DotGit.
88
+
AlternatesFS billy.Filesystem
84
89
}
85
90
86
91
// The DotGit type represents a local git repository on disk. This
···
1146
1151
}
1147
1152
defer f.Close()
1148
1153
1154
+
fs := d.options.AlternatesFS
1155
+
if fs == nil {
1156
+
fs = d.fs
1157
+
}
1158
+
1149
1159
var alternates []*DotGit
1160
+
seen := make(map[string]struct{})
1150
1161
1151
1162
// Read alternate paths line-by-line and create DotGit objects.
1152
1163
scanner := bufio.NewScanner(f)
1153
1164
for scanner.Scan() {
1154
1165
path := scanner.Text()
1155
-
if !filepath.IsAbs(path) {
1156
-
// For relative paths, we can perform an internal conversion to
1157
-
// slash so that they work cross-platform.
1158
-
slashPath := filepath.ToSlash(path)
1159
-
// If the path is not absolute, it must be relative to object
1160
-
// database (.git/objects/info).
1161
-
// https://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html
1162
-
// Hence, derive a path relative to DotGit's root.
1163
-
// "../../../reponame/.git/" -> "../../reponame/.git"
1164
-
// Remove the first ../
1165
-
relpath := filepath.Join(strings.Split(slashPath, "/")[1:]...)
1166
-
normalPath := filepath.FromSlash(relpath)
1167
-
path = filepath.Join(d.fs.Root(), normalPath)
1166
+
1167
+
// Avoid creating multiple dotgits for the same alternative path.
1168
+
if _, ok := seen[path]; ok {
1169
+
continue
1170
+
}
1171
+
1172
+
seen[path] = struct{}{}
1173
+
1174
+
if filepath.IsAbs(path) {
1175
+
// Handling absolute paths should be straight-forward. However, the default osfs (Chroot)
1176
+
// tries to concatenate an abs path with the root path in some operations (e.g. Stat),
1177
+
// which leads to unexpected errors. Therefore, make the path relative to the current FS instead.
1178
+
if reflect.TypeOf(fs) == reflect.TypeOf(&chroot.ChrootHelper{}) {
1179
+
path, err = filepath.Rel(fs.Root(), path)
1180
+
if err != nil {
1181
+
return nil, fmt.Errorf("cannot make path %q relative: %w", path, err)
1182
+
}
1183
+
}
1184
+
} else {
1185
+
// By Git conventions, relative paths should be based on the object database (.git/objects/info)
1186
+
// location as per: https://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html
1187
+
// However, due to the nature of go-git and its filesystem handling via Billy, paths cannot
1188
+
// cross its "chroot boundaries". Therefore, ignore any "../" and treat the path from the
1189
+
// fs root. If this is not correct based on the dotgit fs, set a different one via AlternatesFS.
1190
+
abs := filepath.Join(string(filepath.Separator), filepath.ToSlash(path))
1191
+
path = filepath.FromSlash(abs)
1168
1192
}
1169
-
fs := osfs.New(filepath.Dir(path))
1170
-
alternates = append(alternates, New(fs))
1193
+
1194
+
// Aligns with upstream behavior: exit if target path is not a valid directory.
1195
+
if fi, err := fs.Stat(path); err != nil || !fi.IsDir() {
1196
+
return nil, fmt.Errorf("invalid object directory %q: %w", path, err)
1197
+
}
1198
+
afs, err := fs.Chroot(filepath.Dir(path))
1199
+
if err != nil {
1200
+
return nil, fmt.Errorf("cannot chroot %q: %w", path, err)
1201
+
}
1202
+
alternates = append(alternates, New(afs))
1171
1203
}
1172
1204
1173
1205
if err = scanner.Err(); err != nil {
+125
-37
storage/filesystem/dotgit/dotgit_test.go
+125
-37
storage/filesystem/dotgit/dotgit_test.go
···
6
6
"io"
7
7
"os"
8
8
"path/filepath"
9
+
"regexp"
9
10
"runtime"
10
11
"strings"
11
12
"testing"
···
15
16
"github.com/go-git/go-billy/v5/util"
16
17
fixtures "github.com/go-git/go-git-fixtures/v4"
17
18
"github.com/go-git/go-git/v5/plumbing"
19
+
"github.com/stretchr/testify/assert"
18
20
. "gopkg.in/check.v1"
19
21
)
20
22
···
810
812
c.Assert(ref.Hash().String(), Equals, "b8d3ffab552895c19b9fcf7aa264d277cde33881")
811
813
}
812
814
813
-
func (s *SuiteDotGit) TestAlternates(c *C) {
814
-
fs, clean := s.TemporalFilesystem()
815
-
defer clean()
815
+
func TestAlternatesDefault(t *testing.T) {
816
+
// Create a new dotgit object.
817
+
dotFS := osfs.New(t.TempDir())
816
818
817
-
// Create a new dotgit object and initialize.
818
-
dir := New(fs)
819
-
err := dir.Initialize()
820
-
c.Assert(err, IsNil)
819
+
testAlternates(t, dotFS, dotFS)
820
+
}
821
821
822
-
// Create alternates file.
823
-
altpath := fs.Join("objects", "info", "alternates")
824
-
f, err := fs.Create(altpath)
825
-
c.Assert(err, IsNil)
822
+
func TestAlternatesWithFS(t *testing.T) {
823
+
// Create a new dotgit object with a specific FS for alternates.
824
+
altFS := osfs.New(t.TempDir())
825
+
dotFS, _ := altFS.Chroot("repo2")
826
826
827
-
// Multiple alternates.
828
-
var strContent string
829
-
if runtime.GOOS == "windows" {
830
-
strContent = "C:\\Users\\username\\repo1\\.git\\objects\r\n..\\..\\..\\rep2\\.git\\objects"
831
-
} else {
832
-
strContent = "/Users/username/rep1//.git/objects\n../../../rep2//.git/objects"
827
+
testAlternates(t, dotFS, altFS)
828
+
}
829
+
830
+
func TestAlternatesWithBoundOS(t *testing.T) {
831
+
// Create a new dotgit object with a specific FS for alternates.
832
+
altFS := osfs.New(t.TempDir(), osfs.WithBoundOS())
833
+
dotFS, _ := altFS.Chroot("repo2")
834
+
835
+
testAlternates(t, dotFS, altFS)
836
+
}
837
+
838
+
func testAlternates(t *testing.T, dotFS, altFS billy.Filesystem) {
839
+
tests := []struct {
840
+
name string
841
+
in []string
842
+
inWindows []string
843
+
setup func()
844
+
wantErr bool
845
+
wantRoots []string
846
+
}{
847
+
{
848
+
name: "no alternates",
849
+
},
850
+
{
851
+
name: "abs path",
852
+
in: []string{filepath.Join(altFS.Root(), "./repo1/.git/objects")},
853
+
inWindows: []string{filepath.Join(altFS.Root(), ".\\repo1\\.git\\objects")},
854
+
setup: func() {
855
+
err := altFS.MkdirAll(filepath.Join("repo1", ".git", "objects"), 0o700)
856
+
assert.NoError(t, err)
857
+
},
858
+
wantRoots: []string{filepath.Join("repo1", ".git")},
859
+
},
860
+
{
861
+
name: "rel path",
862
+
in: []string{"../../../repo3//.git/objects"},
863
+
inWindows: []string{"..\\..\\..\\repo3\\.git\\objects"},
864
+
setup: func() {
865
+
err := altFS.MkdirAll(filepath.Join("repo3", ".git", "objects"), 0o700)
866
+
assert.NoError(t, err)
867
+
},
868
+
wantRoots: []string{filepath.Join("repo3", ".git")},
869
+
},
870
+
{
871
+
name: "invalid abs path",
872
+
in: []string{"/alt/target2"},
873
+
inWindows: []string{"\\alt\\target2"},
874
+
wantErr: true,
875
+
},
876
+
{
877
+
name: "invalid rel path",
878
+
in: []string{"../../../alt/target3"},
879
+
inWindows: []string{"..\\..\\..\\alt\\target3"},
880
+
wantErr: true,
881
+
},
833
882
}
834
-
content := []byte(strContent)
835
-
f.Write(content)
836
-
f.Close()
837
883
838
-
dotgits, err := dir.Alternates()
839
-
c.Assert(err, IsNil)
840
-
if runtime.GOOS == "windows" {
841
-
c.Assert(dotgits[0].fs.Root(), Equals, "C:\\Users\\username\\repo1\\.git")
842
-
} else {
843
-
c.Assert(dotgits[0].fs.Root(), Equals, "/Users/username/rep1/.git")
884
+
for _, tc := range tests {
885
+
t.Run(tc.name, func(t *testing.T) {
886
+
dir := NewWithOptions(dotFS, Options{AlternatesFS: altFS})
887
+
err := dir.Initialize()
888
+
assert.NoError(t, err)
889
+
890
+
content := strings.Join(tc.in, "\n")
891
+
if runtime.GOOS == "windows" {
892
+
content = strings.Join(tc.inWindows, "\r\n")
893
+
}
894
+
895
+
// Create alternates file.
896
+
altpath := dotFS.Join("objects", "info", "alternates")
897
+
f, err := dotFS.Create(altpath)
898
+
assert.NoError(t, err)
899
+
f.Write([]byte(content))
900
+
f.Close()
901
+
902
+
if tc.setup != nil {
903
+
tc.setup()
904
+
}
905
+
906
+
dotgits, err := dir.Alternates()
907
+
if tc.wantErr {
908
+
assert.Error(t, err)
909
+
} else {
910
+
assert.NoError(t, err)
911
+
}
912
+
913
+
for i, d := range dotgits {
914
+
assert.Regexp(t, "^"+regexp.QuoteMeta(altFS.Root()), d.fs.Root())
915
+
assert.Regexp(t, regexp.QuoteMeta(tc.wantRoots[i])+"$", d.fs.Root())
916
+
}
917
+
})
844
918
}
919
+
}
845
920
846
-
// For relative path:
847
-
// /some/absolute/path/to/dot-git -> /some/absolute/path
848
-
pathx := strings.Split(fs.Root(), string(filepath.Separator))
849
-
pathx = pathx[:len(pathx)-2]
850
-
// Use string.Join() to avoid malformed absolutepath on windows
851
-
// C:Users\\User\\... instead of C:\\Users\\appveyor\\... .
852
-
resolvedPath := strings.Join(pathx, string(filepath.Separator))
853
-
// Append the alternate path to the resolvedPath
854
-
expectedPath := fs.Join(string(filepath.Separator), resolvedPath, "rep2", ".git")
921
+
func TestAlternatesDupes(t *testing.T) {
922
+
dotFS := osfs.New(t.TempDir())
923
+
dir := New(dotFS)
924
+
err := dir.Initialize()
925
+
assert.NoError(t, err)
926
+
927
+
path := filepath.Join(dotFS.Root(), "target3")
928
+
dupes := []string{path, path, path, path, path}
929
+
930
+
content := strings.Join(dupes, "\n")
855
931
if runtime.GOOS == "windows" {
856
-
expectedPath = fs.Join(resolvedPath, "rep2", ".git")
932
+
content = strings.Join(dupes, "\r\n")
857
933
}
858
934
859
-
c.Assert(dotgits[1].fs.Root(), Equals, expectedPath)
935
+
err = dotFS.MkdirAll("target3", 0o700)
936
+
assert.NoError(t, err)
937
+
938
+
// Create alternates file.
939
+
altpath := dotFS.Join("objects", "info", "alternates")
940
+
f, err := dotFS.Create(altpath)
941
+
assert.NoError(t, err)
942
+
f.Write([]byte(content))
943
+
f.Close()
944
+
945
+
dotgits, err := dir.Alternates()
946
+
assert.NoError(t, err)
947
+
assert.Len(t, dotgits, 1)
860
948
}
861
949
862
950
type norwfs struct {
+5
storage/filesystem/storage.go
+5
storage/filesystem/storage.go
···
37
37
// LargeObjectThreshold maximum object size (in bytes) that will be read in to memory.
38
38
// If left unset or set to 0 there is no limit
39
39
LargeObjectThreshold int64
40
+
// AlternatesFS provides the billy filesystem to be used for Git Alternates.
41
+
// If none is provided, it falls back to using the underlying instance used for
42
+
// DotGit.
43
+
AlternatesFS billy.Filesystem
40
44
}
41
45
42
46
// NewStorage returns a new Storage backed by a given `fs.Filesystem` and cache.
···
49
53
func NewStorageWithOptions(fs billy.Filesystem, cache cache.Object, ops Options) *Storage {
50
54
dirOps := dotgit.Options{
51
55
ExclusiveAccess: ops.ExclusiveAccess,
56
+
AlternatesFS: ops.AlternatesFS,
52
57
}
53
58
dir := dotgit.NewWithOptions(fs, dirOps)
54
59
+20
-11
worktree_test.go
+20
-11
worktree_test.go
···
16
16
fixtures "github.com/go-git/go-git-fixtures/v4"
17
17
"github.com/go-git/go-git/v5/config"
18
18
"github.com/go-git/go-git/v5/plumbing"
19
+
"github.com/go-git/go-git/v5/plumbing/cache"
19
20
"github.com/go-git/go-git/v5/plumbing/filemode"
20
21
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
21
22
"github.com/go-git/go-git/v5/plumbing/format/index"
22
23
"github.com/go-git/go-git/v5/plumbing/object"
24
+
"github.com/go-git/go-git/v5/storage/filesystem"
23
25
"github.com/go-git/go-git/v5/storage/memory"
26
+
"github.com/stretchr/testify/assert"
24
27
25
28
"github.com/go-git/go-billy/v5/memfs"
26
29
"github.com/go-git/go-billy/v5/osfs"
···
2198
2201
c.Assert(err, IsNil)
2199
2202
}
2200
2203
2201
-
func (s *WorktreeSuite) TestAlternatesRepo(c *C) {
2204
+
func TestAlternatesRepo(t *testing.T) {
2202
2205
fs := fixtures.ByTag("alternates").One().Worktree()
2203
2206
2204
2207
// Open 1st repo.
2205
2208
rep1fs, err := fs.Chroot("rep1")
2206
-
c.Assert(err, IsNil)
2209
+
assert.NoError(t, err)
2207
2210
rep1, err := PlainOpen(rep1fs.Root())
2208
-
c.Assert(err, IsNil)
2211
+
assert.NoError(t, err)
2209
2212
2210
2213
// Open 2nd repo.
2211
2214
rep2fs, err := fs.Chroot("rep2")
2212
-
c.Assert(err, IsNil)
2213
-
rep2, err := PlainOpen(rep2fs.Root())
2214
-
c.Assert(err, IsNil)
2215
+
assert.NoError(t, err)
2216
+
d, _ := rep2fs.Chroot(GitDirName)
2217
+
storer := filesystem.NewStorageWithOptions(d,
2218
+
cache.NewObjectLRUDefault(), filesystem.Options{
2219
+
AlternatesFS: fs,
2220
+
})
2221
+
rep2, err := Open(storer, rep2fs)
2222
+
2223
+
assert.NoError(t, err)
2215
2224
2216
2225
// Get the HEAD commit from the main repo.
2217
2226
h, err := rep1.Head()
2218
-
c.Assert(err, IsNil)
2227
+
assert.NoError(t, err)
2219
2228
commit1, err := rep1.CommitObject(h.Hash())
2220
-
c.Assert(err, IsNil)
2229
+
assert.NoError(t, err)
2221
2230
2222
2231
// Get the HEAD commit from the shared repo.
2223
2232
h, err = rep2.Head()
2224
-
c.Assert(err, IsNil)
2233
+
assert.NoError(t, err)
2225
2234
commit2, err := rep2.CommitObject(h.Hash())
2226
-
c.Assert(err, IsNil)
2235
+
assert.NoError(t, err)
2227
2236
2228
-
c.Assert(commit1.String(), Equals, commit2.String())
2237
+
assert.Equal(t, commit1.String(), commit2.String())
2229
2238
}
2230
2239
2231
2240
func (s *WorktreeSuite) TestGrep(c *C) {