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