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 plugin
  6
  7import (
  8	"bytes"
  9	"context"
 10	"io/fs"
 11	"sync"
 12
 13	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
 14	"github.com/samber/oops"
 15	"gitroot.dev/libs/golang/plugin/model"
 16	pluginLib "gitroot.dev/libs/golang/plugin/model"
 17	"gitroot.dev/server/logger"
 18	"gitroot.dev/server/repository"
 19	"gitroot.dev/server/user"
 20)
 21
 22type Manager struct {
 23	logger            *logger.Logger
 24	conf              needConf
 25	repoManager       needRepo
 26	userManager       needUser
 27	backgroundManager needBackground
 28	execManager       needManager
 29
 30	plugins     []Plugin
 31	pluginsLock sync.RWMutex
 32	runtimes    *runtimes
 33}
 34
 35type needConf interface {
 36	ForgeConf() model.ForgeConf
 37	GetDirPathDataPlugin(pluginName string) string
 38	PathDataPlugin() string
 39	PathFilePlugins() string
 40	DataWeb(repoName string) fs.FS
 41	PathCache() string
 42	Cache(repoName string, pluginName string) fs.FS
 43}
 44
 45type needRepo interface {
 46	OpenForgeRepo(ctx context.Context) (*repository.GitRootRepository, error)
 47	Open(ctx context.Context, repoName string) (*repository.GitRootRepository, error)
 48}
 49
 50type needUser interface {
 51	NewCommiter(pseudo string) (*user.Commiter, error)
 52}
 53
 54type needBackground interface {
 55	PostPush(pusher user.SimpleUser, repoName string, commands []*packp.Command)
 56	DeleteBranch(repoName string, toDeleteBranchName string)
 57}
 58
 59type needManager interface {
 60	Exec(project *repository.GitRootRepository, branch string, commands pluginLib.Exec) (*pluginLib.ExecStatus, error)
 61}
 62
 63func NewManager(conf needConf, repoManager needRepo, userManager needUser, execManager needManager) *Manager {
 64	log := logger.NewLogger(logger.PLUGIN_MANAGER)
 65	m := &Manager{
 66		logger:      log,
 67		conf:        conf,
 68		repoManager: repoManager,
 69		userManager: userManager,
 70		execManager: execManager,
 71
 72		plugins:     nil,
 73		pluginsLock: sync.RWMutex{},
 74	}
 75	m.runtimes = newRuntimes(m, log)
 76	return m
 77}
 78
 79func (m *Manager) SetBackgroundManager(backgroundManager needBackground) {
 80	m.backgroundManager = backgroundManager
 81}
 82
 83func (m *Manager) Run(ctx context.Context, repoName string, commands []CommandForDiff) error {
 84	if len(commands) > 0 {
 85		plugins, err := m.usableInDefaultBranch(ctx, repoName)
 86		if err != nil {
 87			return err
 88		}
 89
 90		// TODO rework plugin start/activation
 91		// for each command (branch) we have a list of plugins to start/activate
 92		// maybe cut commands into one command previously in code
 93		// and so run plugins/commits for one cmd at a time
 94
 95		pluginsConfChange, err := m.checkPluginConfChange(ctx, repoName, commands)
 96		if err != nil {
 97			return err
 98		}
 99
100		if len(plugins) > 0 {
101			pluginsAlreadyPresent := make([]Plugin, 0)
102			toWorktreePlugin := make([]Plugin, 0)
103			for _, p := range plugins {
104				found := false
105				for _, pp := range pluginsConfChange {
106					for _, ppp := range pp.plugins {
107						if p.Name == ppp.Name {
108							found = true
109							break
110						}
111					}
112					if found {
113						break
114					}
115				}
116				if !found {
117					pluginsAlreadyPresent = append(pluginsAlreadyPresent, p)
118				} else {
119					toWorktreePlugin = append(toWorktreePlugin, p)
120				}
121			}
122			if len(pluginsAlreadyPresent) > 0 {
123				m.runtimes.Start(ctx, repoName, pluginsAlreadyPresent, runtimeInputsKindDiff, commands)
124			}
125			if len(toWorktreePlugin) > 0 {
126				m.runtimes.Start(ctx, repoName, toWorktreePlugin, runtimeInputsKindWorktree, commands)
127			}
128		}
129	}
130	return nil
131}
132
133type commandPluginActivation struct {
134	command CommandForDiff
135	plugins []Plugin
136}
137
138func (m *Manager) checkPluginConfChange(ctx context.Context, repoName string, commands []CommandForDiff) ([]commandPluginActivation, error) {
139	pluginsActivated := make([]commandPluginActivation, 0)
140	if len(commands) == 0 {
141		return pluginsActivated, nil
142	}
143	repo, err := m.repoManager.Open(logger.AddCaller(ctx, "checkPluginActivation"), repoName)
144	if err != nil {
145		return nil, oops.Wrapf(err, "can't open repo")
146	}
147	defer repo.Close()
148
149	repoConf, err := repo.Configuration()
150	if err != nil {
151		return nil, oops.Wrapf(err, "can't get repo conf")
152	}
153
154	for _, cmd := range commands {
155		if len(cmd.commits) == 0 || !cmd.IsFileTouched(repoConf.DefaultBranch, m.conf.PathFilePlugins()) {
156			continue
157		}
158
159		oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
160		if err != nil {
161			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
162		}
163
164		newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
165		if err != nil {
166			return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
167		}
168
169		if bytes.Equal(oldFilecontent, newFilecontent) {
170			continue
171		}
172
173		oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
174		if err != nil {
175			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
176		}
177		newConfPlugins, err := ParsePlugins(newFilecontent, false)
178		if err != nil {
179			return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
180		}
181
182		plugins := make([]Plugin, 0)
183
184		if len(oldConfPlugins) < len(newConfPlugins) {
185			addedPlugins := []Plugin{}
186			for _, p := range newConfPlugins {
187				found := false
188				for _, op := range oldConfPlugins {
189					if p.Name == op.Name {
190						found = true
191						break
192					}
193				}
194				if !found {
195					addedPlugins = append(addedPlugins, p.SetActive(false))
196				}
197			}
198			oldConfPlugins = append(oldConfPlugins, addedPlugins...)
199		}
200
201		for _, oldConfPlugin := range oldConfPlugins {
202			for _, newConfPlugin := range newConfPlugins {
203				if oldConfPlugin.Name == newConfPlugin.Name {
204					if !oldConfPlugin.Active && newConfPlugin.Active {
205						m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Name))
206						plugins = append(plugins, newConfPlugin)
207						break
208					}
209					if !Equal(oldConfPlugin, newConfPlugin) {
210						m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Name))
211						plugins = append(plugins, newConfPlugin)
212						break
213					}
214				}
215			}
216		}
217
218		if len(plugins) > 0 {
219			pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
220		}
221	}
222	return pluginsActivated, nil
223}