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