feat(backend): cache repos

Ayman Bagabas created

Change summary

server/backend/file/file.go | 89 +++++++++++++++++++++++++++++++-------
1 file changed, 73 insertions(+), 16 deletions(-)

Detailed changes

server/backend/file/file.go 🔗

@@ -66,6 +66,10 @@ type FileBackend struct { // nolint:revive
 	// path is the path to the directory containing the repositories and config
 	// files.
 	path string
+
+	// repos is a map of repositories.
+	repos map[string]*Repo
+
 	// AdditionalAdmins additional admins to the server.
 	AdditionalAdmins []string
 }
@@ -145,6 +149,11 @@ func NewFileBackend(path string) (*FileBackend, error) {
 			_ = f.Close()
 		}
 	}
+
+	if err := fb.initRepos(); err != nil {
+		return nil, err
+	}
+
 	return fb, nil
 }
 
@@ -602,9 +611,10 @@ func (fb *FileBackend) SetPrivate(repo string, priv bool) error {
 // Created repositories are always bare.
 //
 // It implements backend.Backend.
-func (fb *FileBackend) CreateRepository(name string, private bool) (backend.Repository, error) {
-	name = sanatizeRepo(name) + ".git"
-	rp := filepath.Join(fb.reposPath(), name)
+func (fb *FileBackend) CreateRepository(repo string, private bool) (backend.Repository, error) {
+	name := sanatizeRepo(repo)
+	repo = name + ".git"
+	rp := filepath.Join(fb.reposPath(), repo)
 	if _, err := os.Stat(rp); err == nil {
 		return nil, os.ErrExist
 	}
@@ -614,18 +624,23 @@ func (fb *FileBackend) CreateRepository(name string, private bool) (backend.Repo
 		return nil, err
 	}
 
-	fb.SetPrivate(name, private)
-	fb.SetDescription(name, "")
+	fb.SetPrivate(repo, private)
+	fb.SetDescription(repo, "")
 
-	return &Repo{path: rp, root: fb.reposPath()}, nil
+	r := &Repo{path: rp, root: fb.reposPath()}
+	// Add to cache.
+	fb.repos[name] = r
+	return r, nil
 }
 
 // DeleteRepository deletes the given repository.
 //
 // It implements backend.Backend.
-func (fb *FileBackend) DeleteRepository(name string) error {
-	name = sanatizeRepo(name) + ".git"
-	return os.RemoveAll(filepath.Join(fb.reposPath(), name))
+func (fb *FileBackend) DeleteRepository(repo string) error {
+	name := sanatizeRepo(repo)
+	delete(fb.repos, name)
+	repo = name + ".git"
+	return os.RemoveAll(filepath.Join(fb.reposPath(), repo))
 }
 
 // RenameRepository renames the given repository.
@@ -648,7 +663,12 @@ func (fb *FileBackend) RenameRepository(oldName string, newName string) error {
 //
 // It implements backend.Backend.
 func (fb *FileBackend) Repository(repo string) (backend.Repository, error) {
-	repo = sanatizeRepo(repo) + ".git"
+	name := sanatizeRepo(repo)
+	if r, ok := fb.repos[name]; ok {
+		return r, nil
+	}
+
+	repo = name + ".git"
 	rp := filepath.Join(fb.reposPath(), repo)
 	_, err := os.Stat(rp)
 	if err != nil {
@@ -661,10 +681,31 @@ func (fb *FileBackend) Repository(repo string) (backend.Repository, error) {
 	return &Repo{path: rp, root: fb.reposPath()}, nil
 }
 
-// Repositories returns a list of all repositories.
-//
-// It implements backend.Backend.
-func (fb *FileBackend) Repositories() ([]backend.Repository, error) {
+// Returns true if path is a directory containing an `objects` directory and a
+// `HEAD` file.
+func isGitDir(path string) bool {
+	stat, err := os.Stat(filepath.Join(path, "objects"))
+	if err != nil {
+		return false
+	}
+	if !stat.IsDir() {
+		return false
+	}
+
+	stat, err = os.Stat(filepath.Join(path, "HEAD"))
+	if err != nil {
+		return false
+	}
+	if stat.IsDir() {
+		return false
+	}
+
+	return true
+}
+
+// initRepos initializes the repository cache.
+func (fb *FileBackend) initRepos() error {
+	fb.repos = make(map[string]*Repo)
 	repos := make([]backend.Repository, 0)
 	err := filepath.WalkDir(fb.reposPath(), func(path string, d fs.DirEntry, _ error) error {
 		// Skip non-directories.
@@ -677,12 +718,28 @@ func (fb *FileBackend) Repositories() ([]backend.Repository, error) {
 			return nil
 		}
 
-		repos = append(repos, &Repo{path: path, root: fb.reposPath()})
+		if isGitDir(path) {
+			r := &Repo{path: path, root: fb.reposPath()}
+			fb.repos[r.Name()] = r
+			repos = append(repos, r)
+		}
 
 		return nil
 	})
 	if err != nil {
-		return nil, err
+		return err
+	}
+
+	return nil
+}
+
+// Repositories returns a list of all repositories.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) Repositories() ([]backend.Repository, error) {
+	repos := make([]backend.Repository, 0)
+	for _, r := range fb.repos {
+		repos = append(repos, r)
 	}
 
 	return repos, nil