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