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