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}