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