Skip to content

Commit 4c638f2

Browse files
committed
repo: add 'ReadRepositoryStorage()'
Add a function to the 'core.RepositoryProvider' that reads the on-disk repository storage for the bundle server and returns the valid repos in a route -> 'core.Repository' map. 'ReadRepositoryStorage()' identifies repositories by finding the directories at a depth of 2 from the repo storage root, then checking that the repository has a remote 'origin'. The result includes the "active" routes in the bundle server (stored in the 'routes' file) as well as those that have been removed from the routes file with 'git-bundle-server stop'. In later commits, this information will be used to provide more complete information about the state of the bundle server and configure "repairs" to a corrupted server. Signed-off-by: Victoria Dye <[email protected]>
1 parent 063bb1c commit 4c638f2

File tree

5 files changed

+215
-2
lines changed

5 files changed

+215
-2
lines changed

cmd/git-bundle-web-server/bundle-server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import (
1313
"time"
1414

1515
"github.com/github/git-bundle-server/internal/bundles"
16+
"github.com/github/git-bundle-server/internal/cmd"
1617
"github.com/github/git-bundle-server/internal/common"
1718
"github.com/github/git-bundle-server/internal/core"
19+
"github.com/github/git-bundle-server/internal/git"
1820
"github.com/github/git-bundle-server/internal/log"
1921
)
2022

@@ -84,7 +86,9 @@ func (b *bundleWebServer) serve(w http.ResponseWriter, r *http.Request) {
8486

8587
userProvider := common.NewUserProvider()
8688
fileSystem := common.NewFileSystem()
87-
repoProvider := core.NewRepositoryProvider(b.logger, userProvider, fileSystem)
89+
commandExecutor := cmd.NewCommandExecutor(b.logger)
90+
gitHelper := git.NewGitHelper(b.logger, commandExecutor)
91+
repoProvider := core.NewRepositoryProvider(b.logger, userProvider, fileSystem, gitHelper)
8892

8993
repos, err := repoProvider.GetRepositories(ctx)
9094
if err != nil {

cmd/utils/container-helpers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func BuildGitBundleServerContainer(logger log.TraceLogger) *DependencyContainer
2828
logger,
2929
GetDependency[common.UserProvider](ctx, container),
3030
GetDependency[common.FileSystem](ctx, container),
31+
GetDependency[git.GitHelper](ctx, container),
3132
)
3233
})
3334
registerDependency(container, func(ctx context.Context) bundles.BundleProvider {

internal/core/repo.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"strings"
89

910
"github.com/github/git-bundle-server/internal/common"
11+
"github.com/github/git-bundle-server/internal/git"
1012
"github.com/github/git-bundle-server/internal/log"
1113
)
1214

@@ -19,23 +21,27 @@ type Repository struct {
1921
type RepositoryProvider interface {
2022
CreateRepository(ctx context.Context, route string) (*Repository, error)
2123
GetRepositories(ctx context.Context) (map[string]Repository, error)
24+
ReadRepositoryStorage(ctx context.Context) (map[string]Repository, error)
2225
RemoveRoute(ctx context.Context, route string) error
2326
}
2427

2528
type repoProvider struct {
2629
logger log.TraceLogger
2730
user common.UserProvider
2831
fileSystem common.FileSystem
32+
gitHelper git.GitHelper
2933
}
3034

3135
func NewRepositoryProvider(logger log.TraceLogger,
3236
u common.UserProvider,
3337
fs common.FileSystem,
38+
g git.GitHelper,
3439
) RepositoryProvider {
3540
return &repoProvider{
3641
logger: logger,
3742
user: u,
3843
fileSystem: fs,
44+
gitHelper: g,
3945
}
4046
}
4147

@@ -149,3 +155,43 @@ func (r *repoProvider) GetRepositories(ctx context.Context) (map[string]Reposito
149155

150156
return repos, nil
151157
}
158+
159+
func (r *repoProvider) ReadRepositoryStorage(ctx context.Context) (map[string]Repository, error) {
160+
ctx, exitRegion := r.logger.Region(ctx, "repo", "get_on_disk_repos")
161+
defer exitRegion()
162+
163+
user, err := r.user.CurrentUser()
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
entries, err := r.fileSystem.ReadDirRecursive(reporoot(user), 2, true)
169+
if err != nil {
170+
return nil, err
171+
}
172+
173+
repos := make(map[string]Repository)
174+
for _, entry := range entries {
175+
if !entry.IsDir() {
176+
continue
177+
}
178+
179+
_, err = r.gitHelper.GetRemoteUrl(ctx, entry.Path())
180+
if err != nil {
181+
continue
182+
}
183+
184+
pathElems := strings.Split(entry.Path(), string(os.PathSeparator))
185+
if len(pathElems) < 2 {
186+
return nil, r.logger.Errorf(ctx, "invalid repo path '%s'", entry.Path())
187+
}
188+
route := strings.Join(pathElems[len(pathElems)-2:], "/")
189+
repos[route] = Repository{
190+
Route: route,
191+
RepoDir: filepath.Join(reporoot(user), route),
192+
WebDir: filepath.Join(webroot(user), route),
193+
}
194+
}
195+
196+
return repos, nil
197+
}

internal/core/repo_test.go

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import (
77
"path/filepath"
88
"testing"
99

10+
"github.com/github/git-bundle-server/internal/common"
1011
"github.com/github/git-bundle-server/internal/core"
1112
. "github.com/github/git-bundle-server/internal/testhelpers"
13+
"github.com/github/git-bundle-server/internal/utils"
1214
"github.com/stretchr/testify/assert"
1315
"github.com/stretchr/testify/mock"
1416
)
@@ -94,7 +96,7 @@ func TestRepos_GetRepositories(t *testing.T) {
9496
}
9597
testUserProvider := &MockUserProvider{}
9698
testUserProvider.On("CurrentUser").Return(testUser, nil)
97-
repoProvider := core.NewRepositoryProvider(testLogger, testUserProvider, testFileSystem)
99+
repoProvider := core.NewRepositoryProvider(testLogger, testUserProvider, testFileSystem, nil)
98100

99101
for _, tt := range getRepositoriesTests {
100102
t.Run(tt.title, func(t *testing.T) {
@@ -123,3 +125,134 @@ func TestRepos_GetRepositories(t *testing.T) {
123125
})
124126
}
125127
}
128+
129+
var readRepositoryStorageTests = []struct {
130+
title string
131+
132+
foundPaths Pair[[]Pair[string, bool], error] // list of (path, isDir), error
133+
foundRouteIsValidRepo map[string]bool // map of route -> whether GetRemoteUrl succeeds
134+
135+
expectedRoutes []string
136+
expectedErr bool
137+
}{
138+
{
139+
"no dirs found",
140+
NewPair([]Pair[string, bool]{}, error(nil)),
141+
map[string]bool{},
142+
[]string{},
143+
false,
144+
},
145+
{
146+
"multiple valid repos found",
147+
NewPair(
148+
[]Pair[string, bool]{
149+
NewPair("my/repo", true),
150+
NewPair("another/route", true),
151+
},
152+
error(nil),
153+
),
154+
map[string]bool{
155+
"my/repo": true,
156+
"another/route": true,
157+
},
158+
[]string{"my/repo", "another/route"},
159+
false,
160+
},
161+
{
162+
"ignores non-directories",
163+
NewPair(
164+
[]Pair[string, bool]{
165+
NewPair("this-is-a/file", false),
166+
NewPair("my/repo", true),
167+
},
168+
error(nil),
169+
),
170+
map[string]bool{
171+
"my/repo": true,
172+
},
173+
[]string{"my/repo"},
174+
false,
175+
},
176+
{
177+
"ignores invalid Git repos",
178+
NewPair(
179+
[]Pair[string, bool]{
180+
NewPair("is/a-repo", true),
181+
NewPair("not/a-repo", true),
182+
},
183+
error(nil),
184+
),
185+
map[string]bool{
186+
"is/a-repo": true,
187+
"not/a-repo": false,
188+
},
189+
[]string{"is/a-repo"},
190+
false,
191+
},
192+
}
193+
194+
func TestRepos_ReadRepositoryStorage(t *testing.T) {
195+
testLogger := &MockTraceLogger{}
196+
testFileSystem := &MockFileSystem{}
197+
testGitHelper := &MockGitHelper{}
198+
testUser := &user.User{
199+
Uid: "123",
200+
Username: "testuser",
201+
HomeDir: "/my/test/dir",
202+
}
203+
testUserProvider := &MockUserProvider{}
204+
testUserProvider.On("CurrentUser").Return(testUser, nil)
205+
repoProvider := core.NewRepositoryProvider(testLogger, testUserProvider, testFileSystem, testGitHelper)
206+
207+
for _, tt := range readRepositoryStorageTests {
208+
t.Run(tt.title, func(t *testing.T) {
209+
testFileSystem.On("ReadDirRecursive",
210+
filepath.Clean("/my/test/dir/git-bundle-server/git"),
211+
2,
212+
true,
213+
).Return(utils.Map(tt.foundPaths.First, func(path Pair[string, bool]) common.ReadDirEntry {
214+
return TestReadDirEntry{
215+
PathVal: filepath.Join("/my/test/dir/git-bundle-server/git", path.First),
216+
IsDirVal: path.Second,
217+
}
218+
}), tt.foundPaths.Second).Once()
219+
220+
for route, isValid := range tt.foundRouteIsValidRepo {
221+
call := testGitHelper.On("GetRemoteUrl",
222+
mock.Anything,
223+
filepath.Join("/my/test/dir/git-bundle-server/git", route),
224+
).Once()
225+
226+
if isValid {
227+
call.Return("https://localhost/example-remote", nil)
228+
} else {
229+
call.Return("", errors.New("could not get remote URL"))
230+
}
231+
}
232+
233+
actual, err := repoProvider.ReadRepositoryStorage(context.Background())
234+
mock.AssertExpectationsForObjects(t, testUserProvider, testFileSystem, testGitHelper)
235+
236+
if tt.expectedErr {
237+
assert.NotNil(t, err, "Expected error")
238+
assert.Nil(t, actual, "Expected nil map")
239+
} else {
240+
assert.Nil(t, err, "Expected success")
241+
assert.NotNil(t, actual, "Expected non-nil map")
242+
assert.NotNil(t, actual, "Expected non-nil list")
243+
assert.Equal(t, len(tt.expectedRoutes), len(actual), "Length mismatch")
244+
for _, route := range tt.expectedRoutes {
245+
a := actual[route]
246+
247+
assert.Equal(t, route, a.Route)
248+
assert.Equal(t, filepath.Join("/my/test/dir/git-bundle-server/git", route), a.RepoDir)
249+
assert.Equal(t, filepath.Join("/my/test/dir/git-bundle-server/www", route), a.WebDir)
250+
}
251+
}
252+
253+
// Reset mocks
254+
testFileSystem.Mock = mock.Mock{}
255+
testGitHelper.Mock = mock.Mock{}
256+
})
257+
}
258+
}

internal/testhelpers/mocks.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"io/fs"
78
"os/exec"
89
"os/user"
910
"runtime"
@@ -13,6 +14,34 @@ import (
1314
"github.com/stretchr/testify/mock"
1415
)
1516

17+
type TestReadDirEntry struct {
18+
PathVal string
19+
NameVal string
20+
IsDirVal bool
21+
TypeVal fs.FileMode
22+
InfoVal fs.FileInfo
23+
}
24+
25+
func (e TestReadDirEntry) Path() string {
26+
return e.PathVal
27+
}
28+
29+
func (e TestReadDirEntry) Name() string {
30+
return e.NameVal
31+
}
32+
33+
func (e TestReadDirEntry) IsDir() bool {
34+
return e.IsDirVal
35+
}
36+
37+
func (e TestReadDirEntry) Type() fs.FileMode {
38+
return e.TypeVal
39+
}
40+
41+
func (e TestReadDirEntry) Info() (fs.FileInfo, error) {
42+
return e.InfoVal, nil
43+
}
44+
1645
func methodIsMocked(m *mock.Mock) bool {
1746
// Get the calling method name
1847
pc := make([]uintptr, 1)

0 commit comments

Comments
 (0)