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) WriteExec(filepath string, filecontent []byte) error {
117	f, err := repo.worktree.Filesystem.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
118	if err != nil {
119		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't open file")
120	}
121	_, err = f.Write(filecontent)
122	if err != nil {
123		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't write file")
124	}
125	if err := f.Close(); err != nil {
126		return repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't close file")
127	}
128	return nil
129}
130
131func (repo *GitRootRepositoryWrite) CommitAll(message string, commiter *user.Commiter) (plumbing.Hash, error) {
132	unlock := repo.lock.Write()
133	defer unlock()
134
135	err := repo.worktree.AddWithOptions(&git.AddOptions{All: true})
136	if err != nil {
137		return plumbing.ZeroHash, oops.Wrapf(err, "Add .")
138	}
139
140	h, err := repo.worktree.Commit(message, &git.CommitOptions{
141		Author: &object.Signature{Name: commiter.Pseudo, Email: commiter.Email, When: time.Now()},
142		Signer: commiter.Signer,
143	})
144	if err != nil && !errors.Is(err, git.ErrEmptyCommit) {
145		return plumbing.ZeroHash, oops.Wrapf(err, "Commit")
146	}
147
148	return h, nil
149}
150
151func (repo *GitRootRepositoryWrite) Checkout(branch plumbing.ReferenceName) error {
152	unlock := repo.lock.Write()
153	defer unlock()
154
155	if err := repo.worktree.Checkout(&git.CheckoutOptions{Branch: branch}); err != nil {
156		status, err2 := repo.worktree.Status()
157		if err2 != nil {
158			return repo.errorHandler.Wrapf(err, "can't checkout and then can't status %s", err2.Error())
159		}
160		repo.repo.manager.logger.Warn("worktree", logger.NewLoggerPair("status", status.String()))
161		return err
162	}
163	return nil
164}
165
166func (repo *GitRootRepositoryWrite) Branch(branch string, fromHash plumbing.Hash) error {
167	unlock := repo.lock.Write()
168	defer unlock()
169
170	return repo.worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Hash: fromHash, Create: true})
171}
172
173func (repo *GitRootRepositoryWrite) DeleteBranchInStore(branch plumbing.ReferenceName) error {
174	unlock := repo.lock.Write()
175	defer unlock()
176
177	if err := repo.storer.RemoveReference(branch); err != nil {
178		return repo.errorHandler.Wrapf(err, "DeleteBranch err %s", branch)
179	}
180
181	return nil
182}
183
184func (repo *GitRootRepositoryWrite) Accept() {
185	repo.rejected = false
186}
187
188func (repo *GitRootRepositoryWrite) Reject() {
189	repo.rejected = true
190}
191
192func (repo *GitRootRepositoryWrite) close() {
193	unlock := repo.lock.Write()
194	if !repo.rejected {
195		if err := repo.storer.Commit(); err != nil {
196			repo.repo.logger.Error("close in write", err)
197		}
198	}
199	unlock()
200	repo.lock.Close()
201	repo.repo.logger.Debug("closed in write")
202}