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