GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package repository
  6
  7import (
  8	"context"
  9	"errors"
 10	"io/fs"
 11	"os"
 12	"time"
 13
 14	"github.com/go-git/go-billy/v6/memfs"
 15	"github.com/go-git/go-git/v6"
 16	"github.com/go-git/go-git/v6/plumbing"
 17	"github.com/go-git/go-git/v6/plumbing/object"
 18	"github.com/go-git/go-git/v6/storage/memory"
 19	"github.com/go-git/go-git/v6/storage/transactional"
 20	"github.com/samber/oops"
 21	grfs "gitroot.dev/server/fs"
 22	"gitroot.dev/server/logger"
 23	"gitroot.dev/server/user"
 24)
 25
 26type GitRootRepositoryWrite struct {
 27	repo         *GitRootRepository
 28	repoWrite    *git.Repository
 29	storer       transactional.Storage
 30	worktree     *git.Worktree
 31	lock         *repoLock
 32	errorHandler oops.OopsErrorBuilder
 33	rejected     bool
 34}
 35
 36func NewGitRootRepositoryWrite(repo *GitRootRepository, branch plumbing.ReferenceName, lock *repoLock) (*GitRootRepositoryWrite, error) {
 37	errorHandler := oops.Code("GitRootRepositoryWrite").With("repoName", repo.name)
 38
 39	unlock := lock.Write()
 40	defer unlock()
 41
 42	storer := transactional.NewStorage(repo.storer, memory.NewStorage())
 43
 44	repoWrite, err := git.Open(storer, memfs.New())
 45	if err != nil {
 46		return nil, errorHandler.Wrapf(err, "can't open repoWrite")
 47	}
 48
 49	ref, err := repoWrite.Reference(branch, true)
 50	if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) {
 51		return nil, errorHandler.Wrapf(err, "can't reference")
 52	}
 53
 54	worktree, err := repoWrite.Worktree()
 55	if err != nil {
 56		return nil, errorHandler.Wrapf(err, "can't worktree repo")
 57	}
 58
 59	if ref != nil && !ref.Hash().IsZero() {
 60		repo.logger.Debug("repo will mount worktree", logger.NewLoggerPair("hash", ref.Hash()))
 61		if err := worktree.Checkout(&git.CheckoutOptions{Branch: ref.Name(), Force: true}); err != nil {
 62			repo.logger.Error("repo worktree", err, logger.NewLoggerPair("hash", ref.Hash()))
 63			return nil, errorHandler.Wrapf(err, "can't worktree reset")
 64		}
 65		repo.logger.Debug("repo has mount worktree", logger.NewLoggerPair("hash", ref.Hash()))
 66	} else {
 67		repo.logger.Warn("repo will no mount worktree", logger.NewLoggerPair("branch", branch), logger.NewLoggerPair("hash", ref))
 68	}
 69
 70	return &GitRootRepositoryWrite{
 71		repo:         repo,
 72		repoWrite:    repoWrite,
 73		storer:       storer,
 74		worktree:     worktree,
 75		lock:         lock,
 76		errorHandler: errorHandler,
 77		rejected:     false,
 78	}, nil
 79}
 80
 81func (repo *GitRootRepositoryWrite) Storer() transactional.Storage {
 82	return repo.storer
 83}
 84
 85func (repo *GitRootRepositoryWrite) ToFs(ctx context.Context) fs.FS {
 86	return grfs.ToFs(ctx, repo.worktree.Filesystem())
 87}
 88
 89func (repo *GitRootRepositoryWrite) GetLastCommit(hash plumbing.Hash) (LastCommit, error) {
 90	com, err := object.GetCommit(repo.storer, hash)
 91	if err != nil {
 92		return LastCommit{}, oops.Wrapf(err, "can't get commit %s", hash.String())
 93	}
 94	return commitChange(com, false, make(map[string]bool, 0))
 95}
 96
 97func (repo *GitRootRepositoryWrite) Write(filepath string, filecontent []byte) error {
 98	f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
 99	if err != nil {
100		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
101	}
102	n, err := f.Write(filecontent)
103	if err != nil {
104		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
105	}
106	if n != len(filecontent) {
107		return repo.errorHandler.With("filepath", filepath).Errorf("has written %d but need %d", n, len(filecontent))
108	}
109	if err := f.Close(); err != nil {
110		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
111	}
112	return nil
113}
114
115func (repo *GitRootRepositoryWrite) Remove(filepath string) error {
116	if err := repo.worktree.Filesystem().Remove(filepath); err != nil {
117		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't remove file")
118	}
119	return nil
120}
121
122func (repo *GitRootRepositoryWrite) WriteExec(filepath string, filecontent []byte) error {
123	f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
124	if err != nil {
125		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
126	}
127	_, err = f.Write(filecontent)
128	if err != nil {
129		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
130	}
131	if err := f.Close(); err != nil {
132		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
133	}
134	return nil
135}
136
137func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) {
138	unlock := repo.lock.Write()
139	defer unlock()
140
141	err := repo.worktree.AddWithOptions(&git.AddOptions{All: true})
142	if err != nil {
143		return plumbing.ZeroHash, oops.Wrapf(err, "Add .")
144	}
145
146	h, err := repo.worktree.Commit(message, &git.CommitOptions{
147		Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()},
148		Signer: commiter.Signer,
149	})
150	if err != nil && !errors.Is(err, git.ErrEmptyCommit) {
151		return plumbing.ZeroHash, oops.Wrapf(err, "Commit")
152	}
153
154	return h, nil
155}
156
157func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error {
158	unlock := repo.lock.Write()
159	defer unlock()
160
161	return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true})
162}
163
164func (repo *GitRootRepositoryWrite) DeleteBranchInStore(branch plumbing.ReferenceName) error {
165	unlock := repo.lock.Write()
166	defer unlock()
167
168	if err := repo.storer.RemoveReference(branch); err != nil {
169		return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", branch)
170	}
171
172	return nil
173}
174
175func (repo *GitRootRepositoryWrite) Accept() {
176	repo.rejected = false
177}
178
179func (repo *GitRootRepositoryWrite) Reject() {
180	repo.rejected = true
181}
182
183func (repo *GitRootRepositoryWrite) close() {
184	unlock := repo.lock.Write()
185	if !repo.rejected {
186		repo.repo.logger.Debug("not rejected, commit to store")
187		if err := repo.storer.Commit(); err != nil {
188			repo.repo.logger.Error("close in write", err)
189		}
190	} else {
191		repo.repo.logger.Debug("rejected")
192	}
193	unlock()
194	repo.lock.Close()
195	repo.repo.logger.Debug("closed in write")
196}