Skip to content

Commit cec7da6

Browse files
authored
Merge pull request #953 from pjbgf/alternates
storage: filesystem, Add option to set a specific FS for alternates
2 parents 4f61489 + 8b47ceb commit cec7da6

File tree

6 files changed

+203
-64
lines changed

6 files changed

+203
-64
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/pjbgf/sha1cd v0.3.0
2121
github.com/sergi/go-diff v1.1.0
2222
github.com/skeema/knownhosts v1.2.1
23+
github.com/stretchr/testify v1.8.4
2324
github.com/xanzy/ssh-agent v0.3.3
2425
golang.org/x/crypto v0.16.0
2526
golang.org/x/net v0.19.0
@@ -33,10 +34,13 @@ require (
3334
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
3435
github.com/cloudflare/circl v1.3.3 // indirect
3536
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
37+
github.com/davecgh/go-spew v1.1.1 // indirect
3638
github.com/kr/pretty v0.3.1 // indirect
3739
github.com/kr/text v0.2.0 // indirect
40+
github.com/pmezard/go-difflib v1.0.0 // indirect
3841
github.com/rogpeppe/go-internal v1.11.0 // indirect
3942
golang.org/x/mod v0.12.0 // indirect
4043
golang.org/x/tools v0.13.0 // indirect
4144
gopkg.in/warnings.v0 v0.1.2 // indirect
45+
gopkg.in/yaml.v3 v3.0.1 // indirect
4246
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
6969
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
7070
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
7171
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
72+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7273
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
7374
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
7475
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

storage/filesystem/dotgit/dotgit.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ import (
1010
"os"
1111
"path"
1212
"path/filepath"
13+
"reflect"
1314
"runtime"
1415
"sort"
1516
"strings"
1617
"time"
1718

18-
"github.com/go-git/go-billy/v5/osfs"
1919
"github.com/go-git/go-git/v5/plumbing"
2020
"github.com/go-git/go-git/v5/plumbing/hash"
2121
"github.com/go-git/go-git/v5/storage"
2222
"github.com/go-git/go-git/v5/utils/ioutil"
2323

2424
"github.com/go-git/go-billy/v5"
25+
"github.com/go-git/go-billy/v5/helper/chroot"
2526
)
2627

2728
const (
@@ -81,6 +82,10 @@ type Options struct {
8182
// KeepDescriptors makes the file descriptors to be reused but they will
8283
// need to be manually closed calling Close().
8384
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
8489
}
8590

8691
// The DotGit type represents a local git repository on disk. This
@@ -1146,28 +1151,55 @@ func (d *DotGit) Alternates() ([]*DotGit, error) {
11461151
}
11471152
defer f.Close()
11481153

1154+
fs := d.options.AlternatesFS
1155+
if fs == nil {
1156+
fs = d.fs
1157+
}
1158+
11491159
var alternates []*DotGit
1160+
seen := make(map[string]struct{})
11501161

11511162
// Read alternate paths line-by-line and create DotGit objects.
11521163
scanner := bufio.NewScanner(f)
11531164
for scanner.Scan() {
11541165
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)
1192+
}
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)
11681201
}
1169-
fs := osfs.New(filepath.Dir(path))
1170-
alternates = append(alternates, New(fs))
1202+
alternates = append(alternates, New(afs))
11711203
}
11721204

11731205
if err = scanner.Err(); err != nil {

storage/filesystem/dotgit/dotgit_test.go

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"os"
88
"path/filepath"
9+
"regexp"
910
"runtime"
1011
"strings"
1112
"testing"
@@ -15,6 +16,7 @@ import (
1516
"github.com/go-git/go-billy/v5/util"
1617
fixtures "github.com/go-git/go-git-fixtures/v4"
1718
"github.com/go-git/go-git/v5/plumbing"
19+
"github.com/stretchr/testify/assert"
1820
. "gopkg.in/check.v1"
1921
)
2022

@@ -810,53 +812,139 @@ func (s *SuiteDotGit) TestPackRefs(c *C) {
810812
c.Assert(ref.Hash().String(), Equals, "b8d3ffab552895c19b9fcf7aa264d277cde33881")
811813
}
812814

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())
816818

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+
}
821821

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")
826826

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+
},
833882
}
834-
content := []byte(strContent)
835-
f.Write(content)
836-
f.Close()
837883

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+
})
844918
}
919+
}
845920

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")
855931
if runtime.GOOS == "windows" {
856-
expectedPath = fs.Join(resolvedPath, "rep2", ".git")
932+
content = strings.Join(dupes, "\r\n")
857933
}
858934

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)
860948
}
861949

862950
type norwfs struct {

storage/filesystem/storage.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ type Options struct {
3737
// LargeObjectThreshold maximum object size (in bytes) that will be read in to memory.
3838
// If left unset or set to 0 there is no limit
3939
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
4044
}
4145

4246
// NewStorage returns a new Storage backed by a given `fs.Filesystem` and cache.
@@ -49,6 +53,7 @@ func NewStorage(fs billy.Filesystem, cache cache.Object) *Storage {
4953
func NewStorageWithOptions(fs billy.Filesystem, cache cache.Object, ops Options) *Storage {
5054
dirOps := dotgit.Options{
5155
ExclusiveAccess: ops.ExclusiveAccess,
56+
AlternatesFS: ops.AlternatesFS,
5257
}
5358
dir := dotgit.NewWithOptions(fs, dirOps)
5459

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy