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