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