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}