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