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}