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	isClose      bool
 35}
 36
 37func NewGitRootRepositoryWrite(repo *GitRootRepository, branch plumbing.ReferenceName, lock *repoLock) (*GitRootRepositoryWrite, error) {
 38	errorHandler := oops.Code("GitRootRepositoryWrite").With("repoName", repo.name)
 39
 40	unlock := lock.Write()
 41	defer unlock()
 42
 43	storer := transactional.NewStorage(repo.storer, memory.NewStorage())
 44
 45	repoWrite, err := git.Open(storer, memfs.New())
 46	if err != nil {
 47		return nil, errorHandler.Wrapf(err, "can't open repoWrite")
 48	}
 49
 50	ref, err := repoWrite.Reference(branch, true)
 51	if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) {
 52		return nil, errorHandler.Wrapf(err, "can't reference")
 53	}
 54
 55	worktree, err := repoWrite.Worktree()
 56	if err != nil {
 57		return nil, errorHandler.Wrapf(err, "can't worktree repo")
 58	}
 59
 60	if ref != nil && !ref.Hash().IsZero() {
 61		repo.logger.Debug("repo will mount worktree", logger.NewLoggerPair("hash", ref.Hash()))
 62		if err := worktree.Checkout(&git.CheckoutOptions{Branch: ref.Name(), Force: true}); err != nil {
 63			repo.logger.Error("repo worktree", err, logger.NewLoggerPair("hash", ref.Hash()))
 64			return nil, errorHandler.Wrapf(err, "can't worktree reset")
 65		}
 66		repo.logger.Debug("repo has mount worktree", logger.NewLoggerPair("hash", ref.Hash()))
 67	} else {
 68		repo.logger.Warn("repo will no mount worktree", logger.NewLoggerPair("branch", branch), logger.NewLoggerPair("hash", ref))
 69	}
 70
 71	return &GitRootRepositoryWrite{
 72		repo:         repo,
 73		repoWrite:    repoWrite,
 74		storer:       storer,
 75		worktree:     worktree,
 76		lock:         lock,
 77		errorHandler: errorHandler,
 78		rejected:     false,
 79	}, nil
 80}
 81
 82func (repo *GitRootRepositoryWrite) Storer() transactional.Storage {
 83	return repo.storer
 84}
 85
 86func (repo *GitRootRepositoryWrite) ToFs(ctx context.Context) fs.FS {
 87	return grfs.ToFs(ctx, repo.worktree.Filesystem())
 88}
 89
 90func (repo *GitRootRepositoryWrite) GetLastCommit(hash plumbing.Hash) (LastCommit, error) {
 91	com, err := object.GetCommit(repo.storer, hash)
 92	if err != nil {
 93		return LastCommit{}, oops.Wrapf(err, "can't get commit %s", hash.String())
 94	}
 95	return commitChange(com, false, make(map[string]bool, 0))
 96}
 97
 98func (repo *GitRootRepositoryWrite) Write(filepath string, filecontent []byte) error {
 99	if repo.isClose {
100		return repo.errorHandler.Errorf("closed repo")
101	}
102	f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
103	if err != nil {
104		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
105	}
106	n, err := f.Write(filecontent)
107	if err != nil {
108		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
109	}
110	if n != len(filecontent) {
111		return repo.errorHandler.With("filepath", filepath).Errorf("has written %d but need %d", n, len(filecontent))
112	}
113	if err := f.Close(); err != nil {
114		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
115	}
116	return nil
117}
118
119func (repo *GitRootRepositoryWrite) Remove(filepath string) error {
120	if repo.isClose {
121		return repo.errorHandler.Errorf("closed repo")
122	}
123	if err := repo.worktree.Filesystem().Remove(filepath); err != nil {
124		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't remove file")
125	}
126	return nil
127}
128
129func (repo *GitRootRepositoryWrite) WriteExec(filepath string, filecontent []byte) error {
130	if repo.isClose {
131		return repo.errorHandler.Errorf("closed repo")
132	}
133	f, err := repo.worktree.Filesystem().OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
134	if err != nil {
135		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
136	}
137	_, err = f.Write(filecontent)
138	if err != nil {
139		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
140	}
141	if err := f.Close(); err != nil {
142		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
143	}
144	return nil
145}
146
147func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) {
148	if repo.isClose {
149		return plumbing.ZeroHash, repo.errorHandler.Errorf("closed repo")
150	}
151
152	unlock := repo.lock.Write()
153	defer unlock()
154
155	err := repo.worktree.AddWithOptions(&git.AddOptions{All: true})
156	if err != nil {
157		return plumbing.ZeroHash, oops.Wrapf(err, "Add .")
158	}
159
160	h, err := repo.worktree.Commit(message, &git.CommitOptions{
161		Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()},
162		Signer: commiter.Signer,
163	})
164	if err != nil && !errors.Is(err, git.ErrEmptyCommit) {
165		return plumbing.ZeroHash, oops.Wrapf(err, "Commit")
166	}
167
168	return h, nil
169}
170
171func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error {
172	if repo.isClose {
173		return repo.errorHandler.Errorf("closed repo")
174	}
175
176	unlock := repo.lock.Write()
177	defer unlock()
178
179	return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true})
180}
181
182func (repo *GitRootRepositoryWrite) DeleteBranchInStore(branch plumbing.ReferenceName) error {
183	if repo.isClose {
184		return repo.errorHandler.Errorf("closed repo")
185	}
186
187	unlock := repo.lock.Write()
188	defer unlock()
189
190	if err := repo.storer.RemoveReference(branch); err != nil {
191		return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", branch)
192	}
193
194	return nil
195}
196
197func (repo *GitRootRepositoryWrite) Accept() {
198	repo.rejected = false
199}
200
201func (repo *GitRootRepositoryWrite) Reject() {
202	repo.rejected = true
203}
204
205func (repo *GitRootRepositoryWrite) close() {
206	unlock := repo.lock.Write()
207	if !repo.rejected {
208		repo.repo.logger.Debug("not rejected, commit to store")
209		if err := repo.storer.Commit(); err != nil {
210			repo.repo.logger.Error("close in write", err)
211		}
212	} else {
213		repo.repo.logger.Debug("rejected")
214	}
215	unlock()
216	repo.lock.Close()
217	repo.repo.logger.Debug("closed in write")
218	repo.isClose = true
219}