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/v5/memfs"
 15	"github.com/go-git/go-git/v5"
 16	"github.com/go-git/go-git/v5/plumbing"
 17	"github.com/go-git/go-git/v5/plumbing/object"
 18	"github.com/go-git/go-git/v5/storage/memory"
 19	"github.com/go-git/go-git/v5/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 := repo.repo.Reference(branch, true)
 50	if err != nil && 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.Reset(&git.ResetOptions{
 62			Mode:   git.HardReset,
 63			Commit: ref.Hash(),
 64		}); err != nil {
 65			return nil, errorHandler.Wrapf(err, "can't worktree reset")
 66		}
 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	f, err := repo.worktree.Filesystem.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
100	if err != nil {
101		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
102	}
103	n, err := f.Write(filecontent)
104	if err != nil {
105		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
106	}
107	if n != len(filecontent) {
108		return repo.errorHandler.With("filepath", filepath).Errorf("has written %d but need %d", n, len(filecontent))
109	}
110	if err := f.Close(); err != nil {
111		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
112	}
113	return nil
114}
115
116func (repo *GitRootRepositoryWrite) Remove(filepath string) error {
117	if err := repo.worktree.Filesystem.Remove(filepath); err != nil {
118		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't remove file")
119	}
120	return nil
121}
122
123func (repo *GitRootRepositoryWrite) WriteExec(filepath string, filecontent []byte) error {
124	f, err := repo.worktree.Filesystem.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
125	if err != nil {
126		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
127	}
128	_, err = f.Write(filecontent)
129	if err != nil {
130		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
131	}
132	if err := f.Close(); err != nil {
133		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
134	}
135	return nil
136}
137
138func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) {
139	unlock := repo.lock.Write()
140	defer unlock()
141
142	err := repo.worktree.AddWithOptions(&git.AddOptions{All: true})
143	if err != nil {
144		return plumbing.ZeroHash, oops.Wrapf(err, "Add .")
145	}
146
147	h, err := repo.worktree.Commit(message, &git.CommitOptions{
148		Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()},
149		Signer: commiter.Signer,
150	})
151	if err != nil && !errors.Is(err, git.ErrEmptyCommit) {
152		return plumbing.ZeroHash, oops.Wrapf(err, "Commit")
153	}
154
155	return h, nil
156}
157
158func (repo *GitRootRepositoryWrite) Checkout(branch plumbing.ReferenceName) error {
159	unlock := repo.lock.Write()
160	defer unlock()
161
162	if err := repo.worktree.Checkout(&git.CheckoutOptions{Branch: branch}); err != nil {
163		status, err2 := repo.worktree.Status()
164		if err2 != nil {
165			return repo.errorHandler.Wrapf(err, "can't checkout and then can't status %s", err2.Error())
166		}
167		repo.repo.manager.logger.Warn("worktree", logger.NewLoggerPair("status", status.String()))
168		return err
169	}
170	return nil
171}
172
173func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error {
174	unlock := repo.lock.Write()
175	defer unlock()
176
177	return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true})
178}
179
180func (repo *GitRootRepositoryWrite) DeleteBranchInStore(branch plumbing.ReferenceName) error {
181	unlock := repo.lock.Write()
182	defer unlock()
183
184	if err := repo.storer.RemoveReference(branch); err != nil {
185		return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", branch)
186	}
187
188	return nil
189}
190
191func (repo *GitRootRepositoryWrite) Accept() {
192	repo.rejected = false
193}
194
195func (repo *GitRootRepositoryWrite) Reject() {
196	repo.rejected = true
197}
198
199func (repo *GitRootRepositoryWrite) close() {
200	unlock := repo.lock.Write()
201	if !repo.rejected {
202		if err := repo.storer.Commit(); err != nil {
203			repo.repo.logger.Error("close in write", err)
204		}
205	}
206	unlock()
207	repo.lock.Close()
208	repo.repo.logger.Debug("closed in write")
209}