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 "context"
9 "encoding/json"
10 "fmt"
11 "io/fs"
12 "slices"
13 "time"
14
15 "github.com/go-git/go-git/v6/plumbing"
16 "github.com/samber/oops"
17 "github.com/tetratelabs/wazero/api"
18 "gitroot.dev/libs/golang/plugin/model"
19 pluginLib "gitroot.dev/libs/golang/plugin/model"
20 grfs "gitroot.dev/server/fs"
21 "gitroot.dev/server/logger"
22 "gitroot.dev/server/repository"
23 "gitroot.dev/server/user"
24)
25
26type callPlugin struct {
27 manager *Manager
28 plugin Plugin
29 repo *repository.GitRootRepository
30 repoWriter *repository.GitRootRepositoryWrite
31 module api.Module
32 logger *logger.Logger
33}
34
35func (r *runtime) start(ctx context.Context, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, plugins []Plugin, command CommandForDiff, mergeHook func(cmd *repository.MergeRes, pusher user.SimpleUser, toDeleteBranchName string)) error {
36 r.repo = repo
37 r.repoWriter = repoWriter
38 r.command = nil
39 r.commit = nil
40
41 toRunLater := []CommandForDiff{}
42
43 r.commitHook = func(hash plumbing.Hash) {
44 lastCom, err := repoWriter.GetLastCommit(hash)
45 if err != nil {
46 r.logger.Error("can't GetLastCommit in start runtime", err)
47 return
48 }
49 newCmd, err := CommandForDiffFromCommitCmd(ctx, r.plugin.commiter.SimpleUser, lastCom, r.command.branch)
50 if err != nil {
51 r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err)
52 return
53 }
54 toRunLater = append(toRunLater, newCmd)
55 }
56
57 r.mergeHook = mergeHook
58 defer func() {
59 r.commitHook = nil
60 r.mergeHook = nil
61 }()
62
63 r.runCmd(ctx, plugins, repo, repoWriter, command)
64
65 for _, cmd := range toRunLater {
66 r.runCmd(ctx, plugins, repo, repoWriter, cmd)
67 }
68
69 return nil
70}
71
72func (r *runtime) runCmd(ctx context.Context, plugins []Plugin, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, command CommandForDiff) {
73 fs := grfs.NewMultiple(ctx, map[string]fs.FS{
74 "worktree": r.repoWriter.ToFs(ctx),
75 "webcontent": r.manager.conf.DataWeb(r.repo.Name()),
76 })
77 for _, plugin := range plugins {
78 fs.UpdateSubFs("cache", r.manager.conf.Cache(r.repo.Name(), plugin.NamespaceAndName()))
79 r.plugin = plugin
80 timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Log()))
81 r.logger.Debug("start plugin", logger.NewLoggerPair("repo", repo.Name()), logger.NewLoggerPair("name", plugin.Log()))
82 m, err := r.loadModule(ctx, plugin, fs)
83 if err != nil {
84 r.logger.Error("loadModule for start error", err, logger.NewLoggerPair("name", plugin.Log()))
85 continue
86 }
87 l := logger.NewLogger(logger.WASM)
88 l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
89 cp := callPlugin{
90 manager: r.manager,
91 plugin: plugin,
92 repo: repo,
93 repoWriter: repoWriter,
94 module: m,
95 logger: r.logger.NewSubLogger(plugin.Log()),
96 }
97 if err := cp.callPluginForDiff(ctx, r, command); err != nil {
98 r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Log()))
99 }
100 l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Log()))
101 r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Log()))
102 timerStop()
103 }
104}
105
106func (c callPlugin) callPluginForDiff(ctx context.Context, r *runtime, cmd CommandForDiff) error {
107 startCommit := c.module.ExportedFunction("startCommit")
108 addFile := c.module.ExportedFunction("addFile")
109 modFile := c.module.ExportedFunction("modFile")
110 delFile := c.module.ExportedFunction("delFile")
111 endCommit := c.module.ExportedFunction("endCommit")
112 malloc := c.module.ExportedFunction("gitrootAlloc")
113 if malloc == nil {
114 malloc = c.module.ExportedFunction("malloc")
115 }
116
117 r.command = &cmd
118
119 for _, pluginRun := range r.plugin.Run {
120 r.pluginRun = pluginRun
121 callOnAdd := addFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenAdd)
122 callOnMod := modFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenMod)
123 callOnDel := delFile != nil && slices.Contains(pluginRun.When, pluginLib.PluginRunWhenDel)
124 atLeastOneCall := callOnAdd || callOnMod || callOnDel
125 if !atLeastOneCall {
126 c.logger.Info("no call given, skip execution", logger.NewLoggerPair("plugin", r.plugin.Log()))
127 continue
128 }
129
130 isAuthorized := checkBranch(pluginRun, cmd.branch)
131 if !isAuthorized {
132 continue
133 }
134
135 if cmd.branchAction == commitForDiffActionDel {
136 c.logger.Info("delete branch", logger.NewLoggerPair("branch", cmd.branch))
137 continue
138 }
139
140 atLeastOneFile := slices.ContainsFunc(cmd.commits, func(com commitForDiffCommit) bool {
141 return slices.ContainsFunc(com.files, func(f pluginLib.File) bool {
142 return pluginRun.glob.Match(f.Path)
143 })
144 })
145 if !atLeastOneFile {
146 c.logger.Info("no file match, skip execution", logger.NewLoggerPair("plugin", r.plugin.Log()))
147 continue
148 }
149
150 if init := c.module.ExportedFunction("init"); init != nil {
151 arg, err := pluginRun.Marshal()
152 if err != nil {
153 c.logger.Error("can't Marshal pluginRun", err, logger.NewLoggerPair("branch", cmd.branch))
154 continue
155 }
156 c.logger.Debug("init plugin", logger.NewLoggerPair("name", c.plugin.Log()), logger.NewLoggerPair("arg", arg))
157 if err := r.writeMemoryAndCall(c.module, init, malloc, c.repo.Name(), string(model.InitKindDiff), string(arg)); err != nil {
158 c.logger.Error("can't init plugin", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
159 continue
160 }
161 } else {
162 c.logger.Info("no init fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
163 }
164
165 for _, com := range cmd.commits {
166 r.commit = &com
167 comMarshalled, err := MarshallOne(cmd.branch.Short(), com)
168 if err != nil {
169 c.logger.Error("can't marshall commit", err, logger.NewLoggerPair("branch", cmd.branch))
170 continue
171 }
172 c.logger.Debug("diff start commit", logger.NewLoggerPair("com", comMarshalled))
173 if startCommit != nil {
174 c.logger.Debug("startCommit", logger.NewLoggerPair("branch", cmd.branch.Short()), logger.NewLoggerPair("hash", com.hash.String()), logger.NewLoggerPair("message", com.message), logger.NewLoggerPair("date", com.date.Format(time.RFC3339)))
175 if err := r.writeMemoryAndCall(c.module, startCommit, malloc, comMarshalled); err != nil {
176 c.logger.Error("startCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
177 continue
178 }
179 }
180
181 for _, f := range com.files {
182 c.logger.Debug("diff start commit file", logger.NewLoggerPair("file", f.Path), logger.NewLoggerPair("action", f.Action))
183 if pluginRun.glob.Match(f.Path) {
184 jsonFile, err := json.Marshal(f)
185 if err != nil {
186 c.logger.Error("can marshal file", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
187 continue
188 }
189 if f.Action == pluginLib.FileActionTypeAdd && callOnAdd {
190 c.logger.Debug("add", logger.NewLoggerPair("confPath", pluginRun.Path), logger.NewLoggerPair("currentPath", f.Path))
191 // creation
192 r.writeMemoryAndCall(c.module, addFile, malloc, string(jsonFile))
193 } else {
194 if f.Action == pluginLib.FileActionTypeDel && callOnDel {
195 // deletion
196 r.writeMemoryAndCall(c.module, delFile, malloc, string(jsonFile))
197 } else if f.Action == pluginLib.FileActionTypeMod && callOnMod {
198 //modification
199 r.writeMemoryAndCall(c.module, modFile, malloc, string(jsonFile))
200 }
201 }
202 }
203 }
204
205 if endCommit != nil {
206 if err := r.writeMemoryAndCall(c.module, endCommit, malloc, comMarshalled); err != nil {
207 c.logger.Error("endCommit error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
208 continue
209 }
210 } else {
211 c.logger.Info("no endCommit fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
212 }
213 }
214
215 if finish := c.module.ExportedFunction("finish"); finish != nil {
216 if _, err := finish.Call(ctx); err != nil {
217 c.logger.Error("finish error", err, logger.NewLoggerPair("branch", cmd.branch), logger.NewLoggerPair("plugin", c.plugin.Log()))
218 continue
219 }
220 } else {
221 c.logger.Info("no finish fn to call", logger.NewLoggerPair("plugin", r.plugin.Log()))
222 }
223 }
224 return nil
225}
226
227func (r *runtime) writeMemoryAndCall(module api.Module, toCall api.Function, malloc api.Function, message ...string) error {
228 _, err := r.writeMemoryAndCallWithRes(module, toCall, malloc, message...)
229 if err != nil {
230 return err
231 }
232 return nil
233}
234
235func (r *runtime) writeMemoryAndCallWithRes(module api.Module, toCall api.Function, malloc api.Function, message ...string) (uint64, error) {
236 params := make([]uint64, 0)
237 for _, m := range message {
238 size := uint64(len(m))
239
240 results, err := malloc.Call(r.ctx, size)
241 if err != nil {
242 return 0, oops.Wrapf(err, "can't malloc memory for %s with %s", toCall.Definition().Name(), m)
243 }
244 ptr := results[0]
245
246 // The pointer is a linear memory offset, which is where we write the name.
247 if !module.Memory().Write(uint32(ptr), []byte(m)) {
248 return 0, oops.Wrapf(err, "can't write memory")
249 }
250
251 params = append(params, ptr, size)
252 }
253
254 defer func() {
255 ptrSizes := make([]ptrSize, 0)
256 for i, d := range params {
257 if i%2 == 0 {
258 ptrSizes = append(ptrSizes, ptrSize{ptr: d, size: params[i+1]})
259 }
260 }
261 r.free(module, ptrSizes)
262 }()
263
264 if res, err := toCall.Call(r.ctx, params...); err != nil {
265 return 0, oops.With("method", toCall.Definition().ExportNames()).Wrapf(err, "can't call")
266 } else if len(res) > 0 {
267 return res[0], nil
268 }
269
270 return 0, nil
271}