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