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}