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	}
 68
 69	return &GitRootRepositoryWrite{
 70		repo:         repo,
 71		repoWrite:    repoWrite,
 72		storer:       storer,
 73		worktree:     worktree,
 74		lock:         lock,
 75		errorHandler: errorHandler,
 76		rejected:     false,
 77	}, nil
 78}
 79
 80func (repo *GitRootRepositoryWrite) Storer() transactional.Storage {
 81	return repo.storer
 82}
 83
 84func (repo *GitRootRepositoryWrite) ToFs(ctx context.Context) fs.FS {
 85	return grfs.ToFs(ctx, repo.worktree.Filesystem)
 86}
 87
 88func (repo *GitRootRepositoryWrite) GetLastCommit(hash plumbing.Hash) (LastCommit, error) {
 89	com, err := object.GetCommit(repo.storer, hash)
 90	if err != nil {
 91		return LastCommit{}, oops.Wrapf(err, "can't get commit %s", hash.String())
 92	}
 93	return commitChange(com, false, make(map[string]bool, 0))
 94}
 95
 96func (repo *GitRootRepositoryWrite) Write(filepath string, filecontent []byte) error {
 97	f, err := repo.worktree.Filesystem.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
 98	if err != nil {
 99		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
100	}
101	n, err := f.Write(filecontent)
102	if err != nil {
103		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
104	}
105	if n != len(filecontent) {
106		return repo.errorHandler.With("filepath", filepath).Errorf("has written %d but need %d", n, len(filecontent))
107	}
108	if err := f.Close(); err != nil {
109		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
110	}
111	return nil
112}
113
114func (repo *GitRootRepositoryWrite) WriteExec(filepath string, filecontent []byte) error {
115	f, err := repo.worktree.Filesystem.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
116	if err != nil {
117		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
118	}
119	_, err = f.Write(filecontent)
120	if err != nil {
121		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
122	}
123	if err := f.Close(); err != nil {
124		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
125	}
126	return nil
127}
128
129func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) {
130	unlock := repo.lock.Write()
131	defer unlock()
132
133	err := repo.worktree.AddWithOptions(&git.AddOptions{All: true})
134	if err != nil {
135		return plumbing.ZeroHash, oops.Wrapf(err, "Add .")
136	}
137
138	h, err := repo.worktree.Commit(message, &git.CommitOptions{
139		Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()},
140		Signer: commiter.Signer,
141	})
142	if err != nil && !errors.Is(err, git.ErrEmptyCommit) {
143		return plumbing.ZeroHash, oops.Wrapf(err, "Commit")
144	}
145
146	return h, nil
147}
148
149func (repo *GitRootRepositoryWrite) Checkout(branch plumbing.ReferenceName) error {
150	unlock := repo.lock.Write()
151	defer unlock()
152
153	if err := repo.worktree.Checkout(&git.CheckoutOptions{Branch: branch}); err != nil {
154		status, err2 := repo.worktree.Status()
155		if err2 != nil {
156			return err
157		}
158		repo.repo.manager.logger.Warn("worktree", logger.NewLoggerPair("status", status.String()))
159		return err
160	}
161	return nil
162}
163
164func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error {
165	unlock := repo.lock.Write()
166	defer unlock()
167
168	return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true})
169}
170
171func (repo *GitRootRepositoryWrite) Accept() {
172	repo.rejected = false
173}
174
175func (repo *GitRootRepositoryWrite) Reject() {
176	repo.rejected = true
177}
178
179func (repo *GitRootRepositoryWrite) close() {
180	unlock := repo.lock.Write()
181	if !repo.rejected {
182		if err := repo.storer.Commit(); err != nil {
183			repo.repo.logger.Error("close in write", err)
184		}
185	}
186	unlock()
187	repo.lock.Close()
188	repo.repo.logger.Debug("closed in write")
189}