// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package repository import ( "context" "errors" "io/fs" "os" "time" "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/object" "github.com/go-git/go-git/v6/storage/memory" "github.com/go-git/go-git/v6/storage/transactional" "github.com/samber/oops" grfs "gitroot.dev/server/fs" "gitroot.dev/server/logger" "gitroot.dev/server/user" ) type GitRootRepositoryWrite struct { repo *GitRootRepository repoWrite *git.Repository storer transactional.Storage worktree *git.Worktree lock *repoLock errorHandler oops.OopsErrorBuilder rejected bool } func NewGitRootRepositoryWrite(repo *GitRootRepository, branch plumbing.ReferenceName, lock *repoLock) (*GitRootRepositoryWrite, error) { errorHandler := oops.Code("GitRootRepositoryWrite").With("repoName", repo.name) unlock := lock.Write() defer unlock() storer := transactional.NewStorage(repo.storer, memory.NewStorage()) repoWrite, err := git.Open(storer, memfs.New()) if err != nil { return nil, errorHandler.Wrapf(err, "can't open repoWrite") } ref, err := repo.repo.Reference(branch, true) if err != nil && err != plumbing.ErrReferenceNotFound { return nil, errorHandler.Wrapf(err, "can't reference") } worktree, err := repoWrite.Worktree() if err != nil { return nil, errorHandler.Wrapf(err, "can't worktree repo") } if ref != nil && !ref.Hash().IsZero() { repo.logger.Debug("repo will mount worktree", logger.NewLoggerPair("hash", ref.Hash())) if err := worktree.Reset(&git.ResetOptions{ Mode: git.HardReset, Commit: ref.Hash(), }); err != nil { return nil, errorHandler.Wrapf(err, "can't worktree reset") } } else { repo.logger.Warn("repo will no mount worktree", logger.NewLoggerPair("branch", branch), logger.NewLoggerPair("hash", ref)) } return &GitRootRepositoryWrite{ repo: repo, repoWrite: repoWrite, storer: storer, worktree: worktree, lock: lock, errorHandler: errorHandler, rejected: false, }, nil } func (repo *GitRootRepositoryWrite) Storer() transactional.Storage { return repo.storer } func (repo *GitRootRepositoryWrite) ToFs(ctx context.Context) fs.FS { return grfs.ToFs(ctx, repo.worktree.Filesystem()) } func (repo *GitRootRepositoryWrite) GetLastCommit(hash plumbing.Hash) (LastCommit, error) { com, err := object.GetCommit(repo.storer, hash) if err != nil { return LastCommit{}, oops.Wrapf(err, "can't get commit %s", hash.String()) } return commitChange(com, false, make(map[string]bool, 0)) } func (repo *GitRootRepositoryWrite) Write(filepath string, filecontent []byte) error { f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666) if err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file") } n, err := f.Write(filecontent) if err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file") } if n != len(filecontent) { return repo.errorHandler.With("filepath", filepath).Errorf("has written %d but need %d", n, len(filecontent)) } if err := f.Close(); err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file") } return nil } func (repo *GitRootRepositoryWrite) Remove(filepath string) error { if err := repo.worktree.Filesystem().Remove(filepath); err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't remove file") } return nil } func (repo *GitRootRepositoryWrite) WriteExec(filepath string, filecontent []byte) error { f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755) if err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file") } _, err = f.Write(filecontent) if err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file") } if err := f.Close(); err != nil { return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file") } return nil } func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) { unlock := repo.lock.Write() defer unlock() err := repo.worktree.AddWithOptions(&git.AddOptions{All: true}) if err != nil { return plumbing.ZeroHash, oops.Wrapf(err, "Add .") } h, err := repo.worktree.Commit(message, &git.CommitOptions{ Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()}, Signer: commiter.Signer, }) if err != nil && !errors.Is(err, git.ErrEmptyCommit) { return plumbing.ZeroHash, oops.Wrapf(err, "Commit") } return h, nil } func (repo *GitRootRepositoryWrite) Checkout(branch plumbing.ReferenceName) error { unlock := repo.lock.Write() defer unlock() if err := repo.worktree.Checkout(&git.CheckoutOptions{Branch: branch}); err != nil { status, err2 := repo.worktree.Status() if err2 != nil { return repo.errorHandler.Wrapf(err, "can't checkout and then can't status %s", err2.Error()) } repo.repo.manager.logger.Warn("worktree", logger.NewLoggerPair("status", status.String())) return err } return nil } func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error { unlock := repo.lock.Write() defer unlock() return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true}) } func (repo *GitRootRepositoryWrite) DeleteBranchInStore(branch plumbing.ReferenceName) error { unlock := repo.lock.Write() defer unlock() if err := repo.storer.RemoveReference(branch); err != nil { return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", branch) } return nil } func (repo *GitRootRepositoryWrite) Accept() { repo.rejected = false } func (repo *GitRootRepositoryWrite) Reject() { repo.rejected = true } func (repo *GitRootRepositoryWrite) close() { unlock := repo.lock.Write() if !repo.rejected { repo.repo.logger.Debug("not rejected, commit to store") if err := repo.storer.Commit(); err != nil { repo.repo.logger.Error("close in write", err) } } else { repo.repo.logger.Debug("rejected") } unlock() repo.lock.Close() repo.repo.logger.Debug("closed in write") }