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			pluginsAlreadyPresent := make([]Plugin, 0)
110			toWorktreePlugin := make([]Plugin, 0)
111			for _, p := range plugins {
112				found := false
113				for _, pp := range pluginsConfChange {
114					for _, ppp := range pp.plugins {
115						if p.Equal(ppp) {
116							found = true
117							break
118						}
119					}
120					if found {
121						break
122					}
123				}
124				if !found {
125					pluginsAlreadyPresent = append(pluginsAlreadyPresent, p)
126				} else {
127					toWorktreePlugin = append(toWorktreePlugin, p)
128				}
129			}
130			if len(pluginsAlreadyPresent) > 0 {
131				m.runtimes.Start(ctx, repoName, pluginsAlreadyPresent, runtimeInputsKindDiff, commands)
132			}
133			if len(toWorktreePlugin) > 0 {
134				m.runtimes.Start(ctx, repoName, toWorktreePlugin, runtimeInputsKindWorktree, commands)
135			}
136		}
137	}
138	return nil
139}
140
141func (m *Manager) Schedule(ctx context.Context, repoName string, pluginPurl string, pluginRun *PluginRun) error {
142	return m.runtimes.Schedule(ctx, repoName, pluginPurl, pluginRun)
143}
144
145type commandPluginActivation struct {
146	command CommandForDiff
147	plugins []Plugin
148}
149
150func (m *Manager) checkPluginConfChange(ctx context.Context, repoName string, commands []CommandForDiff) ([]commandPluginActivation, error) {
151	pluginsActivated := make([]commandPluginActivation, 0)
152	if len(commands) == 0 {
153		return pluginsActivated, nil
154	}
155	repo, err := m.repoManager.Open(logger.AddCaller(ctx, "checkPluginActivation"), repoName)
156	if err != nil {
157		return nil, oops.Wrapf(err, "can't open repo")
158	}
159	defer repo.Close()
160
161	repoConf, err := repo.Configuration()
162	if err != nil {
163		return nil, oops.Wrapf(err, "can't get repo conf")
164	}
165
166	for _, cmd := range commands {
167		if len(cmd.commits) == 0 || !cmd.IsFileTouched(repoConf.DefaultBranch, m.conf.PathFilePlugins()) {
168			continue
169		}
170
171		oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
172		if err != nil {
173			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
174		}
175
176		newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
177		if err != nil {
178			return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
179		}
180
181		if bytes.Equal(oldFilecontent, newFilecontent) {
182			continue
183		}
184
185		oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
186		if err != nil {
187			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
188		}
189		newConfPlugins, err := ParsePlugins(newFilecontent, false)
190		if err != nil {
191			return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
192		}
193
194		plugins := make([]Plugin, 0)
195
196		if len(oldConfPlugins) < len(newConfPlugins) {
197			addedPlugins := []Plugin{}
198			for _, p := range newConfPlugins {
199				found := false
200				for _, op := range oldConfPlugins {
201					if p.Equal(op) {
202						found = true
203						break
204					}
205				}
206				if !found {
207					addedPlugins = append(addedPlugins, p.SetActive(false))
208				}
209			}
210			oldConfPlugins = append(oldConfPlugins, addedPlugins...)
211		}
212
213		for _, oldConfPlugin := range oldConfPlugins {
214			for _, newConfPlugin := range newConfPlugins {
215				if oldConfPlugin.Equal(newConfPlugin) {
216					if !oldConfPlugin.Active && newConfPlugin.Active {
217						m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
218						plugins = append(plugins, newConfPlugin)
219						break
220					}
221					if !Equal(oldConfPlugin, newConfPlugin) {
222						m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Log()))
223						plugins = append(plugins, newConfPlugin)
224						break
225					}
226				}
227			}
228		}
229
230		if len(plugins) > 0 {
231			pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
232		}
233	}
234	return pluginsActivated, nil
235}