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