// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package plugin import ( "bytes" "context" "io/fs" "sync" "github.com/go-git/go-git/v5/plumbing/protocol/packp" "github.com/samber/oops" "gitroot.dev/libs/golang/plugin/model" pluginLib "gitroot.dev/libs/golang/plugin/model" "gitroot.dev/server/logger" "gitroot.dev/server/repository" "gitroot.dev/server/user" ) type Manager struct { logger *logger.Logger conf needConf repoManager needRepo userManager needUser backgroundManager needBackground execManager needManager plugins []Plugin pluginsLock sync.Mutex runtimes *runtimes } type needConf interface { ForgeConf() model.ForgeConf GetDirPathDataPlugin(pluginName string) string PathDataPlugin() string PathFilePlugins() string DataWeb(repoName string) fs.FS PathCacheProject(repoName string, pluginName string) string PathCache() string Cache(repoName string, pluginName string) fs.FS } type needRepo interface { OpenForgeRepo(ctx context.Context) (*repository.GitRootRepository, error) Open(ctx context.Context, repoName string) (*repository.GitRootRepository, error) } type needUser interface { NewCommiter(pseudo string) (*user.Commiter, error) } type needBackground interface { PostPush(pusher user.SimpleUser, repoName string, commands []*packp.Command) DeleteBranch(repoName string, toDeleteBranchName string) } type needManager interface { Exec(project *repository.GitRootRepository, branch string, pluginName string, commands pluginLib.Exec) (*pluginLib.ExecStatus, error) } func NewManager(conf needConf, repoManager needRepo, userManager needUser, execManager needManager) *Manager { log := logger.NewLogger(logger.PLUGIN_MANAGER) m := &Manager{ logger: log, conf: conf, repoManager: repoManager, userManager: userManager, execManager: execManager, plugins: nil, pluginsLock: sync.Mutex{}, } m.runtimes = newRuntimes(m, log) return m } func (m *Manager) SetBackgroundManager(backgroundManager needBackground) { m.backgroundManager = backgroundManager } func (m *Manager) Run(ctx context.Context, repoName string, commands []CommandForDiff) error { if len(commands) > 0 { com := "" if len(commands[0].commits) > 0 { com = commands[0].commits[0].hash.String() } m.logger.Debug("call usableFromDefaultBranch from manager run", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("branch", commands[0].branch), logger.NewLoggerPair("commit", com)) plugins, err := m.usableFromDefaultBranch(ctx, repoName) if err != nil { return err } // TODO rework plugin start/activation // for each command (branch) we have a list of plugins to start/activate // maybe cut commands into one command previously in code // and so run plugins/commits for one cmd at a time pluginsConfChange, err := m.checkPluginConfChange(ctx, repoName, commands) if err != nil { return err } if len(plugins) > 0 { pluginsAlreadyPresent := make([]Plugin, 0) toWorktreePlugin := make([]Plugin, 0) for _, p := range plugins { found := false for _, pp := range pluginsConfChange { for _, ppp := range pp.plugins { if p.Name == ppp.Name { found = true break } } if found { break } } if !found { pluginsAlreadyPresent = append(pluginsAlreadyPresent, p) } else { toWorktreePlugin = append(toWorktreePlugin, p) } } if len(pluginsAlreadyPresent) > 0 { m.runtimes.Start(ctx, repoName, pluginsAlreadyPresent, runtimeInputsKindDiff, commands) } if len(toWorktreePlugin) > 0 { m.runtimes.Start(ctx, repoName, toWorktreePlugin, runtimeInputsKindWorktree, commands) } } } return nil } type commandPluginActivation struct { command CommandForDiff plugins []Plugin } func (m *Manager) checkPluginConfChange(ctx context.Context, repoName string, commands []CommandForDiff) ([]commandPluginActivation, error) { pluginsActivated := make([]commandPluginActivation, 0) if len(commands) == 0 { return pluginsActivated, nil } repo, err := m.repoManager.Open(logger.AddCaller(ctx, "checkPluginActivation"), repoName) if err != nil { return nil, oops.Wrapf(err, "can't open repo") } defer repo.Close() repoConf, err := repo.Configuration() if err != nil { return nil, oops.Wrapf(err, "can't get repo conf") } for _, cmd := range commands { if len(cmd.commits) == 0 || !cmd.IsFileTouched(repoConf.DefaultBranch, m.conf.PathFilePlugins()) { continue } oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash) if err != nil { return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash") } newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch) if err != nil { return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref") } if bytes.Equal(oldFilecontent, newFilecontent) { continue } oldConfPlugins, err := ParsePlugins(oldFilecontent, false) if err != nil { return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf") } newConfPlugins, err := ParsePlugins(newFilecontent, false) if err != nil { return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf") } plugins := make([]Plugin, 0) if len(oldConfPlugins) < len(newConfPlugins) { addedPlugins := []Plugin{} for _, p := range newConfPlugins { found := false for _, op := range oldConfPlugins { if p.Name == op.Name { found = true break } } if !found { addedPlugins = append(addedPlugins, p.SetActive(false)) } } oldConfPlugins = append(oldConfPlugins, addedPlugins...) } for _, oldConfPlugin := range oldConfPlugins { for _, newConfPlugin := range newConfPlugins { if oldConfPlugin.Name == newConfPlugin.Name { if !oldConfPlugin.Active && newConfPlugin.Active { m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Name)) plugins = append(plugins, newConfPlugin) break } if !Equal(oldConfPlugin, newConfPlugin) { m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Name)) plugins = append(plugins, newConfPlugin) break } } } } if len(plugins) > 0 { pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins}) } } return pluginsActivated, nil }