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