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}