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}