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 "bytes"
9 "context"
10 _ "embed"
11 "fmt"
12 "time"
13
14 git "github.com/go-git/go-git/v5"
15 branch "github.com/go-git/go-git/v5/config"
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/format/config"
18 "github.com/samber/oops"
19 "gitroot.dev/server/logger"
20 "gitroot.dev/server/user"
21)
22
23//go:embed resources/standard_README.md
24var standard_README []byte
25
26//go:embed resources/forgerepo_README.md
27var forgerepo_README []byte
28
29//go:embed resources/init.sh
30var forgerepo_initSh []byte
31
32func (m *Manager) CreateRootRepoIfNeeded(ctx context.Context, rootRemote string) error {
33 if !m.Exists(m.conf.ForgeConfigName()) {
34 return m.createRootRepo(ctx, rootRemote)
35 }
36 return nil
37}
38
39type extraFiles struct {
40 path string
41 content []byte
42}
43
44func (m *Manager) createInitializedRepo(ctx context.Context, repoConf RepoConf, extraFiles []extraFiles, users []user.SimpleUser, availablePlugins []byte) error {
45 finish := m.logger.Time(fmt.Sprintf("create repo %s", repoConf.Name))
46 defer finish()
47
48 writeLock := m.repoLocks.WillWrite(repoConf.Name)
49 unlock := writeLock.Write()
50
51 if repoConf.Kind == "" || repoConf.Kind == RepoKindNormal {
52 bareRepo, err := git.PlainInitWithOptions(m.conf.GetDirPathForRepo(repoConf.Name), &git.PlainInitOptions{
53 InitOptions: git.InitOptions{DefaultBranch: plumbing.NewBranchReferenceName(repoConf.DefaultBranch)},
54 Bare: true,
55 ObjectFormat: config.SHA1, // TODO debug sha256
56 })
57 if err != nil {
58 return oops.Wrapf(err, "PlainInitWithOptions")
59 }
60 if err := bareRepo.CreateBranch(&branch.Branch{
61 Name: repoConf.DefaultBranch,
62 Merge: plumbing.NewBranchReferenceName(repoConf.DefaultBranch),
63 }); err != nil {
64 return oops.Wrapf(err, "CreateBranch")
65 }
66 } else {
67 _, err := git.PlainClone(m.conf.GetDirPathForRepo(repoConf.Name), true, &git.CloneOptions{
68 URL: repoConf.ForkUrl,
69 ReferenceName: plumbing.HEAD,
70 SingleBranch: true,
71 Tags: plumbing.NoTags,
72 NoCheckout: true,
73 Mirror: true,
74 //Progress: os.Stdout,
75 })
76 if err != nil {
77 return oops.Wrapf(err, "PlainClone")
78 }
79 }
80
81 go func() {
82 time.Sleep(1 * time.Millisecond) //TODO not very smart
83 unlock()
84 writeLock.Close()
85 }()
86
87 repo, err := m.NewGitRootRepository(repoConf.Name, nil)
88 if err != nil {
89 return oops.Wrapf(err, "Open repo")
90 }
91 defer repo.Close()
92
93 repoWriter, err := repo.WillWrite(plumbing.HEAD)
94 if err != nil {
95 return oops.Wrapf(err, "will write")
96 }
97
98 // TODO que faire pour les fichiers en mode proxy? Override? Branch?
99
100 for _, extraFile := range extraFiles {
101 if !repo.Exists(extraFile.path) {
102 if err := repoWriter.Write(extraFile.path, extraFile.content); err != nil {
103 return err
104 }
105 }
106 }
107
108 if err := repoWriter.WriteExec(m.conf.ForPathConfig("init.sh"), forgerepo_initSh); err != nil {
109 return err
110 }
111
112 sshString := bytes.NewBuffer([]byte(""))
113 sshString.WriteString(m.userManager.RootCommiter().Email)
114 sshString.WriteString(" ")
115 sshString.WriteString(m.userManager.RootCommiter().Signer.PublicKey())
116 sshString.WriteString("\n")
117 for _, u := range users {
118 sshString.WriteString(u.Email)
119 sshString.WriteString(" ")
120 sshString.WriteString(u.Ssh)
121 sshString.WriteString("\n")
122 }
123
124 if err := repoWriter.Write(m.conf.ForPathConfig("allowed_signers"), sshString.Bytes()); err != nil {
125 return err
126 }
127
128 if err := repoWriter.Write(m.conf.PathFilePlugins(), availablePlugins); err != nil {
129 return err
130 }
131
132 if err := repoWriter.Write(m.conf.PathFileRepoConfigurationName(), newRepoConfiguration(repoConf.DefaultBranch).toFileContent()); err != nil {
133 return err
134 }
135
136 fileUserContent, err := user.CreateFileUser(
137 repoConf.DefaultBranch,
138 append([]user.SimpleUser{m.userManager.RootCommiter().SimpleUser}, users...)...,
139 )
140 if err != nil {
141 return oops.Wrapf(err, "CreateFileUser")
142 }
143 if err := repoWriter.Write(m.conf.PathFileUsers(), fileUserContent); err != nil {
144 return err
145 }
146
147 if _, err := repoWriter.CommitAll("init", m.userManager.RootCommiter()); err != nil {
148 return oops.Wrapf(err, "CommitAll")
149 }
150
151 // if err := repo.repo.Prune(git.PruneOptions{}); err != nil {
152 // return oops.Wrapf(err, "Prune after PlainClone")
153 // }
154 // if err := repo.repo.RepackObjects(&git.RepackConfig{}); err != nil {
155 // return oops.Wrapf(err, "Repack after PlainClone")
156 // }
157
158 return nil
159}
160
161func (m *Manager) createRootRepo(ctx context.Context, rootRemote string) error {
162 repoConf := RepoConf{Name: m.conf.ForgeConfigName(), DefaultBranch: m.conf.DefaultBranchName(), Kind: RepoKindNormal}
163 if rootRemote != "" {
164 repoConf = RepoConf{Name: m.conf.ForgeConfigName(), DefaultBranch: m.conf.DefaultBranchName(), Kind: RepoKindFork, ForkUrl: rootRemote}
165 }
166
167 repositoriesContent, err := toFileContent([]RepoConf{repoConf})
168 if err != nil {
169 return oops.Wrapf(err, "toFileContent")
170 }
171 defaultConf, err := m.conf.Marshall()
172 if err != nil {
173 return oops.Wrapf(err, "marshall conf")
174 }
175 extraFiles := []extraFiles{
176 {path: m.conf.PathFileForgeConfig(), content: defaultConf},
177 {path: "README.md", content: forgerepo_README},
178 {path: m.conf.PathFileRepositories(), content: repositoriesContent},
179 {path: "first_pull", content: []byte("")},
180 }
181
182 return m.createInitializedRepo(ctx, repoConf, extraFiles, []user.SimpleUser{}, []byte(""))
183}
184
185func (m *Manager) CreateUserRepo(ctx context.Context, repo RepoConf, users []user.SimpleUser, availablePlugins []byte) error {
186 m.logger.Warn("plugins version?", logger.NewLoggerPair("p", string(availablePlugins)))
187 owners := make([]user.SimpleUser, len(repo.Owners))
188 for i, u := range repo.Owners {
189 owners[i] = user.SimpleUser{
190 Pseudo: "",
191 Email: "",
192 Ssh: u,
193 }
194 }
195 return m.createInitializedRepo(ctx, repo, []extraFiles{{path: "README.md", content: standard_README}}, append(users, owners...), availablePlugins)
196}