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, commands []pluginLib.Exec) (string, 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				// var cpa commandPluginActivation
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	for _, cmd := range commands {
151		if !cmd.IsFileTouched(cmd.branch, m.conf.PathFilePlugins()) || len(cmd.commits) == 0 {
152			continue
153		}
154
155		oldFilecontent, err := repo.ContentPluginsConfAtHash(cmd.commits[0].parentHash)
156		if err != nil {
157			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't get repo plugin conf at hash")
158		}
159
160		newFilecontent, err := repo.ContentPluginsConfAtRef(cmd.branch)
161		if err != nil {
162			return nil, oops.With("repo", repoName, "ref", cmd.branch.Short()).Wrapf(err, "can't get repo plugin conf at ref")
163		}
164
165		if bytes.Equal(oldFilecontent, newFilecontent) {
166			continue
167		}
168
169		oldConfPlugins, err := ParsePlugins(oldFilecontent, false)
170		if err != nil {
171			return nil, oops.With("repo", repoName, "hash", cmd.commits[0].parentHash.String()).Wrapf(err, "can't parse repo plugin oldconf")
172		}
173		newConfPlugins, err := ParsePlugins(newFilecontent, false)
174		if err != nil {
175			return nil, oops.With("repo", repoName, "Branch", cmd.branch).Wrapf(err, "can't parse repo plugin newconf")
176		}
177
178		plugins := make([]Plugin, 0)
179
180		if len(oldConfPlugins) < len(newConfPlugins) {
181			addedPlugins := []Plugin{}
182			for _, p := range newConfPlugins {
183				found := false
184				for _, op := range oldConfPlugins {
185					if p.Name == op.Name {
186						found = true
187						break
188					}
189				}
190				if !found {
191					addedPlugins = append(addedPlugins, p.SetActive(false))
192				}
193			}
194			oldConfPlugins = append(oldConfPlugins, addedPlugins...)
195		}
196
197		for _, oldConfPlugin := range oldConfPlugins {
198			for _, newConfPlugin := range newConfPlugins {
199				if oldConfPlugin.Name == newConfPlugin.Name {
200					if !oldConfPlugin.Active && newConfPlugin.Active {
201						m.logger.Info("plugin activation", logger.NewLoggerPair("plugin", newConfPlugin.Name))
202						plugins = append(plugins, newConfPlugin)
203						break
204					}
205					if !Equal(oldConfPlugin, newConfPlugin) {
206						m.logger.Info("plugin conf change", logger.NewLoggerPair("plugin", newConfPlugin.Name))
207						plugins = append(plugins, newConfPlugin)
208						break
209					}
210				}
211			}
212		}
213
214		if len(plugins) > 0 {
215			pluginsActivated = append(pluginsActivated, commandPluginActivation{command: cmd, plugins: plugins})
216		}
217	}
218	return pluginsActivated, nil
219}