// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package repository import ( "os" "path/filepath" "github.com/go-git/go-billy/v6/osfs" git "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/cache" "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/storage" "github.com/go-git/go-git/v6/storage/filesystem" "github.com/samber/oops" "gitroot.dev/server/logger" ) type GitRootRepository struct { name string logger *logger.Logger errorHandler oops.OopsErrorBuilder repo *git.Repository storer storage.Storer storerCache cache.Object manager *Manager hasBecomeWrite *GitRootRepositoryWrite configLoaded bool config RepoConfiguration } func (m *Manager) NewGitRootRepository(name string, withCache *GitRootRepository) (*GitRootRepository, error) { errorHandler := oops.Code("NewGitRootRepository").With("name", name) unlock := m.repoLocks.Read(name) defer unlock() if withCache == nil { storerCache := cache.NewObjectLRUDefault() bfs := filesystem.NewStorage(osfs.New(m.conf.GetDirPathForRepo(name), osfs.WithBoundOS()), storerCache) repo, err := git.Open(bfs, nil) if err != nil { return nil, errorHandler.Wrapf(err, "can't open") } return &GitRootRepository{ name: name, logger: m.logger.NewSubLogger(name), errorHandler: errorHandler, repo: repo, storer: bfs, storerCache: storerCache, manager: m, hasBecomeWrite: nil, }, nil } else { return &GitRootRepository{ name: name, logger: m.logger.NewSubLogger(name), errorHandler: errorHandler, repo: withCache.repo, storer: withCache.storer, storerCache: withCache.storerCache, manager: m, hasBecomeWrite: nil, }, nil } } func (repo *GitRootRepository) Name() string { return repo.name } func (repo *GitRootRepository) WillWrite(branch plumbing.ReferenceName) (*GitRootRepositoryWrite, error) { lock := repo.manager.repoLocks.WillWrite(repo.name) if repo.hasBecomeWrite != nil { return nil, repo.errorHandler.Errorf("WillWrite called multiple times before close") } hasBecomeWrite, err := NewGitRootRepositoryWrite(repo, branch, lock) repo.hasBecomeWrite = hasBecomeWrite return repo.hasBecomeWrite, err } func (repo *GitRootRepository) StopWrite() { if repo.hasBecomeWrite != nil { repo.hasBecomeWrite.close() } repo.hasBecomeWrite = nil } func (repo *GitRootRepository) Commit(hash plumbing.Hash) (*object.Commit, error) { return repo.repo.CommitObject(hash) } func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) { if !repo.configLoaded { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't get default hash") } content, err := repo.ContentAtHash(repo.manager.conf.PathFileRepoConfigurationName(), hash) if err != nil { return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file") } conf, err := readRepoConfiguration(content) if err != nil { return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read repo conf") } repo.configLoaded = true repo.config = conf } return repo.config, nil } func (repo *GitRootRepository) DefaultBranch() (plumbing.ReferenceName, error) { unlock := repo.manager.repoLocks.Read(repo.name) defer unlock() conf, err := repo.repo.Config() if err != nil { return plumbing.NewBranchReferenceName(""), repo.errorHandler.Wrapf(err, "can't read conf git") } return plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), nil } func (repo *GitRootRepository) DefaultBranchCurrentHash() (plumbing.Hash, error) { unlock := repo.manager.repoLocks.Read(repo.name) defer unlock() conf, err := repo.repo.Config() if err != nil { return plumbing.ZeroHash, repo.errorHandler.Wrapf(err, "can't read conf git") } ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), true) if err != nil { return plumbing.ZeroHash, repo.errorHandler.Wrapf(err, "can't read conf git") } return ref.Hash(), nil } func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf") } return repo.ContentAtHash(repo.manager.conf.PathFileRepositories(), hash) } func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf") } return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash) } func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) { return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash) } func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) { return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name) } func (repo *GitRootRepository) ContentUserAtDefaultBranch() ([]byte, error) { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf") } return repo.ContentAtHash(repo.manager.conf.PathFileUsers(), hash) } func (repo *GitRootRepository) PathDataWeb(path string) string { return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path) } func (repo *GitRootRepository) Content(filepath string) ([]byte, error) { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return nil, oops.Wrapf(err, "Head") } repo.logger.Debug("head", logger.NewLoggerPair("hash", hash.String())) return repo.ContentAtHash(filepath, hash) } func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) { unlock := repo.manager.repoLocks.Read(repo.name) ref, err := repo.repo.Reference(name, false) if err != nil { unlock() return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef") } unlock() return repo.ContentAtHash(filepath, ref.Hash()) } func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) { unlock := repo.manager.repoLocks.Read(repo.name) defer unlock() c, err := repo.repo.CommitObject(hash) if err != nil { return nil, err } tree, err := c.Tree() if err != nil { return nil, err } file, err := tree.File(filepath) if err != nil { return nil, err } fileContent, err := file.Contents() if err != nil { return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file") } return []byte(fileContent), nil } func (repo *GitRootRepository) Exists(filepath string) bool { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return false } return repo.ExistsAtHash(filepath, hash) } func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool { unlock := repo.manager.repoLocks.Read(repo.name) defer unlock() c, err := repo.repo.CommitObject(hash) if err != nil { return false } tree, err := c.Tree() if err != nil { return false } f, _ := tree.FindEntry(filepath) return f != nil } func (repo *GitRootRepository) Worktree(path string, branch string) error { hash, err := repo.DefaultBranchCurrentHash() if err != nil { return repo.errorHandler.Wrapf(err, "can't get repoConf for worktree on osfs") } unlock := repo.manager.repoLocks.Read(repo.name) defer unlock() ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(branch), false) if err != nil { return oops.With("ref", branch).Wrapf(err, "Worktree") } commit, err := repo.repo.CommitObject(ref.Hash()) if err != nil { return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get CommitObject for worktree on osfs") } tree, err := repo.repo.TreeObject(commit.TreeHash) if err != nil { return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get TreeObject for worktree on osfs") } files := tree.Files() files.ForEach(func(file *object.File) error { content, err := file.Contents() if err != nil { repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name)) return nil } dir, _ := filepath.Split(file.Name) err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm) if err != nil { repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir)) } if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil { repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name)) } return nil }) return nil } func (repo *GitRootRepository) Close() error { repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil)) if repo.hasBecomeWrite != nil { repo.hasBecomeWrite.close() repo.hasBecomeWrite = nil } if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 { return repo.manager.cacheRepos.Delete(repo.name, func() error { repoLock := repo.manager.repoLocks.WillWrite(repo.name) defer repoLock.Close() unwrite := repoLock.Write() defer unwrite() // if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) { // repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer))) // } else { // repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer))) // } // if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) { // repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects")) // } else { // repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer))) // } if sto, ok := repo.storer.(*filesystem.Storage); ok { sto.Close() } repo.storerCache.Clear() return nil }) } return nil }