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}