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