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}