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	"crypto/rand"
 11	"encoding/json"
 12	"errors"
 13	"fmt"
 14	"io/fs"
 15	"os"
 16	"path/filepath"
 17	"unicode/utf16"
 18
 19	"github.com/go-git/go-git/v5/plumbing"
 20	"github.com/go-git/go-git/v5/plumbing/object"
 21	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
 22	"github.com/samber/oops"
 23	"github.com/tetratelabs/wazero"
 24	"github.com/tetratelabs/wazero/api"
 25	"github.com/tetratelabs/wazero/imports/assemblyscript"
 26	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
 27	"github.com/tetratelabs/wazero/sys"
 28	pluginLib "gitroot.dev/libs/golang/plugin/model"
 29	"gitroot.dev/server/logger"
 30	"gitroot.dev/server/repository"
 31	"gitroot.dev/server/user"
 32)
 33
 34type runtimeInputsKind int
 35
 36const (
 37	runtimeInputsKindDiff     runtimeInputsKind = iota
 38	runtimeInputsKindWorktree runtimeInputsKind = iota
 39)
 40
 41type runtimeInputs struct {
 42	ctx      context.Context
 43	repoName string
 44	kind     runtimeInputsKind
 45	plugins  []Plugin
 46	commands []CommandForDiff
 47}
 48
 49type runtime struct {
 50	ctx              context.Context
 51	manager          *Manager
 52	logger           *logger.Logger
 53	repo             *repository.GitRootRepository
 54	repoWriter       *repository.GitRootRepositoryWrite
 55	plugin           Plugin
 56	pluginRun        PluginRun
 57	command          *CommandForDiff
 58	commit           *commitForDiffCommit
 59	wazRun           wazero.Runtime
 60	commitHook       func(h plumbing.Hash)
 61	mergeHook        func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string)
 62	reports          []pluginLib.Report
 63	memoryToGarbage  []ptrSize
 64	modulesToGarbage []api.Module
 65}
 66
 67func newRuntime(ctx context.Context, manager *Manager, logger *logger.Logger, runtimeConfig wazero.RuntimeConfig) (*runtime, error) {
 68	r := &runtime{
 69		ctx:              ctx,
 70		manager:          manager,
 71		logger:           logger,
 72		wazRun:           wazero.NewRuntimeWithConfig(ctx, runtimeConfig),
 73		commitHook:       nil,
 74		mergeHook:        nil,
 75		memoryToGarbage:  []ptrSize{},
 76		modulesToGarbage: []api.Module{},
 77	}
 78
 79	_, err := assemblyscript.Instantiate(ctx, r.wazRun)
 80	if err != nil {
 81		return nil, err
 82	}
 83
 84	_, err = r.wazRun.
 85		NewHostModuleBuilder("gitroot").
 86		NewFunctionBuilder().WithFunc(r.ForgeConf).Export("forgeConf").
 87		NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(false)).Export("modifyContent").
 88		NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(true)).Export("modifyContentAS").
 89		NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(false)).Export("modifyWebContent").
 90		NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(true)).Export("modifyWebContentAS").
 91		NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(false)).Export("replaceWebContent").
 92		NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(true)).Export("replaceWebContentAS").
 93		NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(false)).Export("modifyCacheContent").
 94		NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(true)).Export("modifyCacheContentAS").
 95		NewFunctionBuilder().WithFunc(r.CommitAllBuilder(false)).Export("commitAll").
 96		NewFunctionBuilder().WithFunc(r.CommitAllBuilder(true)).Export("commitAllAS").
 97		NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(false)).Export("diffWithParent").
 98		NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(true)).Export("diffWithParentAS").
 99		NewFunctionBuilder().WithFunc(r.LogBuilder(false)).Export("log").
100		NewFunctionBuilder().WithFunc(r.LogBuilder(true)).Export("logAS").
101		NewFunctionBuilder().WithFunc(r.LogErrorBuilder(false)).Export("logError").
102		NewFunctionBuilder().WithFunc(r.LogErrorBuilder(true)).Export("logErrorAS").
103		NewFunctionBuilder().WithFunc(r.MergeBuilder(false)).Export("merge").
104		NewFunctionBuilder().WithFunc(r.MergeBuilder(true)).Export("mergeAS").
105		NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(false)).Export("commits").
106		NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(true)).Export("commitsAS").
107		NewFunctionBuilder().WithFunc(r.ExecBuilder(false)).Export("exec").
108		NewFunctionBuilder().WithFunc(r.ExecBuilder(true)).Export("execAS").
109		NewFunctionBuilder().WithFunc(r.ReportBuilder(false)).Export("report").
110		NewFunctionBuilder().WithFunc(r.ReportBuilder(true)).Export("reportAS").
111		Instantiate(ctx)
112	if err != nil {
113		return nil, err
114	}
115	_, err = wasi_snapshot_preview1.Instantiate(ctx, r.wazRun)
116	if err != nil {
117		return nil, err
118	}
119	return r, nil
120}
121
122type hasBeenMerged struct {
123	cmd                *packp.Command
124	pusher             user.SimpleUser
125	toDeleteBranchName string
126}
127
128func (r *runtime) listen(c chan runtimeInputs) {
129	postMergeActions := []hasBeenMerged{}
130	for i := range c {
131		r.logger.Debug("listen call", logger.NewLoggerPair("repo", i.repoName), logger.NewLoggerPair("isDiff", i.kind == runtimeInputsKindDiff), logger.NewLoggerPair("isWorktree", i.kind == runtimeInputsKindWorktree))
132		repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.listen"), i.repoName)
133		if err != nil {
134			r.logger.Error("open error in listen", err)
135			repo.Close()
136			continue
137		}
138		repoWriter, err := repo.WillWrite(plumbing.HEAD) //TODO should mount good branch directly inside i.commands
139		if err != nil {
140			r.logger.Error("will write error in listen", err, logger.NewLoggerPair("repo", repo.Name()))
141			repo.Close()
142			continue
143		}
144
145		switch i.kind {
146		case runtimeInputsKindDiff:
147			timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins", repo.Name()))
148			err := r.start(i.ctx, repo, repoWriter, i.plugins, i.commands, func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string) {
149				if cmd != nil {
150					postMergeActions = append(postMergeActions, hasBeenMerged{
151						cmd:                cmd,
152						pusher:             pusher,
153						toDeleteBranchName: toDeleteBranchName,
154					})
155				}
156			})
157			if err != nil {
158				r.logger.Error("start error", err)
159			}
160			timerStop()
161		case runtimeInputsKindWorktree:
162			r.logger.Info("start worktree", logger.NewLoggerPair("repo", repo.Name()))
163			timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins worktree", repo.Name()))
164			if err := r.worktree(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil {
165				r.logger.Error("start error", err)
166			}
167			timerStop()
168		}
169		r.sendReports(i.ctx, repo, repoWriter, i.plugins)
170		r.garbage()
171		repo.Close()
172		for _, postMerge := range postMergeActions {
173			r.manager.backgroundManager.DeleteBranch(i.repoName, postMerge.toDeleteBranchName)
174			r.manager.backgroundManager.PostPush(postMerge.pusher, i.repoName, []*packp.Command{postMerge.cmd})
175		}
176		postMergeActions = nil
177	}
178}
179
180func (r *runtime) loadModule(ctx context.Context, plugin Plugin, withFs fs.FS) (api.Module, error) {
181	m := r.wazRun.Module(plugin.uuid())
182	if m == nil {
183		r.logger.Debug("instantiate plugin conf", logger.NewLoggerPair("name", plugin.Name), logger.NewLoggerPair("with external fs", withFs != nil))
184		config := wazero.NewModuleConfig().
185			WithStdout(os.Stdout).WithStderr(os.Stderr).
186			WithSysWalltime().
187			WithSysNanotime().
188			WithRandSource(rand.Reader).
189			WithName(plugin.uuid()).
190			WithStartFunctions("_initialize", "install")
191
192		if withFs != nil {
193			config = config.WithFS(withFs)
194		} else {
195			r.logger.Warn("no fs mounted", logger.NewLoggerPair("plugin", plugin.Name), logger.NewLoggerPair("repo is nil", r.repo == nil), logger.NewLoggerPair("repoWrite is nil", r.repoWriter == nil))
196		}
197
198		mod, err := r.wazRun.InstantiateModule(ctx, plugin.compiledModule, config)
199		if err != nil {
200			if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
201				fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
202				return nil, err
203			} else if !ok {
204				return nil, err
205			}
206		}
207		m = mod
208	} else {
209		r.logger.Debug("already exist conf", logger.NewLoggerPair("name", plugin.Name))
210	}
211	r.modulesToGarbage = append(r.modulesToGarbage, m)
212	return m, nil
213}
214
215func (r *runtime) garbageMemory(m api.Module) {
216	r.free(m, r.memoryToGarbage)
217	r.memoryToGarbage = []ptrSize{}
218}
219
220func (r *runtime) garbage() {
221	r.reports = []pluginLib.Report{}
222	for _, m := range r.modulesToGarbage {
223		if err := m.Close(r.ctx); err != nil {
224			r.logger.Error("close module fail", err, logger.NewLoggerPair("name", m.Name()))
225		}
226	}
227	r.modulesToGarbage = []api.Module{}
228}
229
230func (r *runtime) ForgeConf(_ context.Context, m api.Module) uint64 {
231	forgeConf, err := json.Marshal(r.manager.conf.ForgeConf())
232	if err != nil {
233		r.logger.Error("ForgeConf can't be serialized", err)
234		return 0
235	}
236	ptrSize, err := r.sendData(m, string(forgeConf))
237	if err != nil {
238		r.logger.Error("ForgeConf can't send data", err)
239		return 0
240	}
241	return ptrSize
242}
243
244func (r *runtime) ModifyContentBuilder(forAS bool) interface{} {
245	f := func(filename string, content string) {
246		if ok, err := checkWrite(r.pluginRun.write.git, r.repoWriter.ToFs(r.ctx), filename); !ok {
247			r.logger.Error("plugin can't write in git", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
248			return
249		}
250		r.logger.Debug("modifyContent", logger.NewLoggerPair("file", filename))
251		if err := r.repoWriter.Write(filename, []byte(content)); err != nil {
252			r.logger.Error("modifyContent can't open file", err, logger.NewLoggerPair("filepath", filename))
253			return
254		}
255	}
256
257	if !forAS {
258		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
259			filename, err := r.readString(m, "ModifyContent filename", filenamePtr, filenameSize)
260			if err != nil {
261				return
262			}
263			content, err := r.readString(m, "ModifyContent content", contentPtr, contentSize)
264			if err != nil {
265				return
266			}
267			f(filename, content)
268		}
269	} else {
270		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
271			filename, err := r.readASString(m, "ModifyContent filename", filenamePtr)
272			if err != nil {
273				return
274			}
275			content, err := r.readASString(m, "ModifyContent content", contentPtr)
276			if err != nil {
277				return
278			}
279			f(filename, content)
280		}
281	}
282}
283
284func (r *runtime) ModifyWebContentBuilder(forAS bool) interface{} {
285	f := func(filename string, content string) {
286		if r.repo == nil { //in forconf scenario their is no repo
287			r.logger.Error("ModifyWebContentBuilder can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
288			return
289		}
290		if ok, err := checkWrite(r.pluginRun.write.web, r.manager.conf.DataWeb(r.repo.Name()), filename); !ok {
291			r.logger.Warn("plugin can't write in web", logger.NewLoggerPair("err", err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
292			return
293		}
294		fullPath := r.repo.PathDataWeb(filename)
295		dir, _ := filepath.Split(fullPath)
296		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
297			r.logger.Error("modifyWebContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
298			return
299		}
300		if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
301			r.logger.Error("modifyWebContent can't open file", err, logger.NewLoggerPair("filepath", filename))
302			return
303		}
304		r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
305	}
306
307	if !forAS {
308		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
309			filename, err := r.readString(m, "ModifyWebContent filename", filenamePtr, filenameSize)
310			if err != nil {
311				return
312			}
313			content, err := r.readString(m, "ModifyWebContent content", contentPtr, contentSize)
314			if err != nil {
315				return
316			}
317			f(filename, content)
318		}
319	} else {
320		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
321			filename, err := r.readASString(m, "ModifyWebContent filename", filenamePtr)
322			if err != nil {
323				return
324			}
325			content, err := r.readASString(m, "ModifyWebContent content", contentPtr)
326			if err != nil {
327				return
328			}
329			f(filename, content)
330		}
331	}
332}
333
334func (r *runtime) ReplaceWebContentBuilder(forAS bool) interface{} {
335	f := func(filename string, oldContent string, content string) {
336		if r.repo == nil { //in forconf scenario their is no repo
337			r.logger.Error("ReplaceWebContentBuilder can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
338			return
339		}
340		if ok, err := checkWrite(r.pluginRun.write.web, r.manager.conf.DataWeb(r.repo.Name()), filename); !ok {
341			r.logger.Warn("plugin can't write in web for replace", logger.NewLoggerPair("err", err), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
342			return
343		}
344		fullPath := r.repo.PathDataWeb(filename)
345		fileContent, err := os.ReadFile(fullPath)
346		if err != nil {
347			r.logger.Error("ReplaceWebContentBuilder can't ReadFile", err, logger.NewLoggerPair("filepath", filename))
348			return
349		}
350		if err := os.WriteFile(fullPath, bytes.Replace(fileContent, []byte(oldContent), []byte(content), 1), 0666); err != nil {
351			r.logger.Error("ReplaceWebContentBuilder can't open file", err, logger.NewLoggerPair("filepath", filename))
352			return
353		}
354		r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
355	}
356
357	if !forAS {
358		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, oldContentPtr, oldContentSize, contentPtr, contentSize uint32) {
359			filename, err := r.readString(m, "ReplaceWebContentBuilder filename", filenamePtr, filenameSize)
360			if err != nil {
361				return
362			}
363			oldContent, err := r.readString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr, oldContentSize)
364			if err != nil {
365				return
366			}
367			content, err := r.readString(m, "ReplaceWebContentBuilder content", contentPtr, contentSize)
368			if err != nil {
369				return
370			}
371			f(filename, oldContent, content)
372		}
373	} else {
374		return func(_ context.Context, m api.Module, filenamePtr, oldContentPtr, contentPtr uint32) {
375			filename, err := r.readASString(m, "ReplaceWebContentBuilder filename", filenamePtr)
376			if err != nil {
377				return
378			}
379			oldContent, err := r.readASString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr)
380			if err != nil {
381				return
382			}
383			content, err := r.readASString(m, "ReplaceWebContentBuilder content", contentPtr)
384			if err != nil {
385				return
386			}
387			f(filename, oldContent, content)
388		}
389	}
390}
391
392func (r *runtime) ModifyCacheContentBuilder(forAS bool) interface{} {
393	f := func(filename string, content string) {
394		r.logger.Debug("ModifyCacheContent", logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("file", filename))
395		if r.repo == nil { //in forconf scenario their is no repo
396			r.logger.Error("ModifyCacheContent can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
397			return
398		}
399		fullPath := filepath.Join(r.manager.conf.PathCache(), r.repo.Name(), r.plugin.Name, filename)
400		dir, _ := filepath.Split(fullPath)
401		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
402			r.logger.Error("ModifyCacheContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
403			return
404		}
405		if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
406			r.logger.Error("ModifyCacheContent can't open file", err, logger.NewLoggerPair("filepath", filename))
407			return
408		}
409		r.logger.Debug("Write in cache", logger.NewLoggerPair("fullPath", fullPath))
410	}
411	if !forAS {
412		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
413			filename, err := r.readString(m, "ModifyCacheContent filename", filenamePtr, filenameSize)
414			if err != nil {
415				return
416			}
417			content, err := r.readString(m, "ModifyCacheContent content", contentPtr, contentSize)
418			if err != nil {
419				return
420			}
421			f(filename, content)
422		}
423	} else {
424		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
425			filename, err := r.readASString(m, "ModifyCacheContent filename", filenamePtr)
426			if err != nil {
427				return
428			}
429			content, err := r.readASString(m, "ModifyCacheContent content", contentPtr)
430			if err != nil {
431				return
432			}
433			f(filename, content)
434		}
435	}
436}
437
438func (r *runtime) CommitAllBuilder(forAS bool) interface{} {
439	f := func(msg string, err error) {
440		if err != nil {
441			return
442		}
443		if h, err := r.repoWriter.CommitAll(msg, r.plugin.commiter); err != nil {
444			r.logger.Error("commitAll can't commit", err)
445			return
446		} else {
447			r.logger.Debug("Commit", logger.NewLoggerPair("message", msg), logger.NewLoggerPair("hash", h.String()))
448			if r.commitHook != nil && !h.IsZero() {
449				r.commitHook(h)
450			}
451		}
452	}
453	if !forAS {
454		return func(_ context.Context, m api.Module, offset, byteCount uint32) {
455			f(r.readString(m, "commitAll", offset, byteCount))
456		}
457	} else {
458		return func(_ context.Context, m api.Module, offset uint32) {
459			f(r.readASString(m, "commitAll", offset))
460		}
461	}
462}
463
464func (r *runtime) DiffWithParentBuilder(forAS bool) interface{} {
465	f := func(m api.Module, hash string, oldfile string, newfile string) uint64 {
466		r.logger.Info("In diffWithParent before found ancestor")
467		diffStr, err := r.repoWriter.GetDiff(plumbing.NewHash(string(hash)), string(oldfile), string(newfile))
468		if err != nil {
469			r.logger.Error("GetDiff", err)
470			return 0
471		}
472		r.logger.Debug("In diffWithParent send diff", logger.NewLoggerPair("diff", diffStr))
473		ptrSize, err := r.sendData(m, diffStr)
474		if err != nil {
475			r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", diffStr))
476			return 0
477		}
478		return ptrSize
479	}
480	if !forAS {
481		return func(_ context.Context, m api.Module, hashPtr, hashSize, oldFilenamePtr, oldFilenameSize, newFilenamePtr, newFilenameSize uint32) uint64 {
482			hash, err := r.readString(m, "DiffWithParent hash", hashPtr, hashSize)
483			if err != nil {
484				return 0
485			}
486			oldFilename, err := r.readString(m, "DiffWithParent oldFilename", oldFilenamePtr, oldFilenameSize)
487			if err != nil {
488				return 0
489			}
490			newFilename, err := r.readString(m, "DiffWithParent newFilename", newFilenamePtr, newFilenameSize)
491			if err != nil {
492				return 0
493			}
494			return f(m, hash, oldFilename, newFilename)
495		}
496	} else {
497		return func(_ context.Context, m api.Module, hashPtr, oldFilenamePtr, newFilenamePtr uint32) uint64 {
498			hash, err := r.readASString(m, "DiffWithParent hash", hashPtr)
499			if err != nil {
500				return 0
501			}
502			oldFilename, err := r.readASString(m, "DiffWithParent oldFilenamePtr", oldFilenamePtr)
503			if err != nil {
504				return 0
505			}
506			newFilename, err := r.readASString(m, "DiffWithParent newFilename", newFilenamePtr)
507			if err != nil {
508				return 0
509			}
510			return f(m, hash, oldFilename, newFilename)
511		}
512	}
513}
514
515func (r *runtime) LogBuilder(forAS bool) interface{} {
516	f := func(msg string, err error) {
517		if err != nil {
518			return
519		}
520		repoName := ""
521		if r.repo != nil { //in forconf scenario their is no repo
522			repoName = r.repo.Name()
523		}
524		r.logger.Debug(msg, logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("plugin", r.plugin.Name))
525	}
526	if !forAS {
527		return func(_ context.Context, m api.Module, offset, byteCount uint32) {
528			f(r.readString(m, "log", offset, byteCount))
529		}
530	} else {
531		return func(_ context.Context, m api.Module, offset uint32) {
532			f(r.readASString(m, "log", offset))
533		}
534	}
535}
536
537func (r *runtime) LogErrorBuilder(forAS bool) interface{} {
538	f := func(msg string, err string) {
539		repoName := ""
540		if r.repo != nil { //in forconf scenario their is no repo
541			repoName = r.repo.Name()
542		}
543		r.logger.Error(msg, errors.New(err), logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("plugin", r.plugin.Name))
544	}
545	if !forAS {
546		return func(_ context.Context, m api.Module, offset, byteCount, errPtr, errSize uint32) {
547			msg, err := r.readString(m, "LogError msg", offset, byteCount)
548			if err != nil {
549				return
550			}
551			errMsg, err := r.readString(m, "LogError err", errPtr, errSize)
552			if err != nil {
553				return
554			}
555			f(msg, errMsg)
556		}
557	} else {
558		return func(_ context.Context, m api.Module, offset, errPtr uint32) {
559			msg, err := r.readASString(m, "LogError msg", offset)
560			if err != nil {
561				return
562			}
563			errMsg, err := r.readASString(m, "LogError err", errPtr)
564			if err != nil {
565				return
566			}
567			f(msg, errMsg)
568		}
569	}
570}
571
572func (r *runtime) MergeBuilder(forAS bool) interface{} {
573	f := func(from string, to string) {
574		r.logger.Debug("try to merge", logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
575		cmd, err := r.repoWriter.Merge(from, to, r.plugin.commiter, r.command.pusher)
576		if err != nil {
577			r.logger.Error("can't Merge", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
578		}
579		if r.mergeHook != nil {
580			r.mergeHook(cmd, r.command.pusher, to)
581		}
582	}
583	if !forAS {
584		return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) {
585			from, err := r.readString(m, "Merge from", fromPtr, fromSize)
586			if err != nil {
587				return
588			}
589			to, err := r.readString(m, "Merge to", toPtr, toSize)
590			if err != nil {
591				return
592			}
593			f(from, to)
594		}
595	} else {
596		return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) {
597			from, err := r.readASString(m, "Merge from", fromPtr)
598			if err != nil {
599				return
600			}
601			to, err := r.readASString(m, "Merge to", toPtr)
602			if err != nil {
603				return
604			}
605			f(from, to)
606		}
607	}
608}
609
610func (r *runtime) GetCommitsBuilder(forAS bool) interface{} {
611	f := func(m api.Module, from string, to string) uint64 {
612		commits := []commitForDiffCommit{}
613		if err := r.repoWriter.WalkCommit(plumbing.NewHash(to), plumbing.NewHash(from), func(c *object.Commit) error {
614			r.logger.Debug("found commit", logger.NewLoggerPair("hash", c.Hash.String()), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
615			commits = append(commits, commitToCommitForDiff(c, nil))
616			return nil
617		}); err != nil {
618			r.logger.Error("can't WalkCommit", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
619		}
620		r.logger.Debug("found commits", logger.NewLoggerPair("nb", len(commits)), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
621		j, err := Marshall(r.command.branch.Short(), commits)
622		if err != nil {
623			r.logger.Error("can't Marshall", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
624		}
625		ptrSize, err := r.sendData(m, j)
626		if err != nil {
627			r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j))
628			return 0
629		}
630		return ptrSize
631	}
632	if !forAS {
633		return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) uint64 {
634			from, err := r.readString(m, "Merge from", fromPtr, fromSize)
635			if err != nil {
636				return 0
637			}
638			to, err := r.readString(m, "Merge to", toPtr, toSize)
639			if err != nil {
640				return 0
641			}
642			return f(m, from, to)
643		}
644	} else {
645		return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) uint64 {
646			from, err := r.readASString(m, "Merge from", fromPtr)
647			if err != nil {
648				return 0
649			}
650			to, err := r.readASString(m, "Merge to", toPtr)
651			if err != nil {
652				return 0
653			}
654			return f(m, from, to)
655		}
656	}
657}
658
659func (r *runtime) ExecBuilder(forAS bool) interface{} {
660	f := func(m api.Module, cmd string) uint64 {
661		exec := pluginLib.Exec{}
662		err := json.Unmarshal([]byte(cmd), &exec)
663		if err != nil {
664			r.logger.Error("can't exec bad format", err)
665			return 0
666		}
667		if ok, err := checkExec(r.pluginRun.write.exec, exec); !ok {
668			r.logger.Warn("plugin can't exec", logger.NewLoggerPair("err", err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name))
669			return 0
670		}
671		j, err := r.manager.execManager.Exec(r.repo, r.command.branch.Short(), exec)
672		if err != nil {
673			r.logger.Error("can't exec", err)
674			return 0
675		}
676		jjson, err := json.Marshal(j)
677		if err != nil {
678			r.logger.Error("can't marshal execStatus", err)
679			return 0
680		}
681		ptrSize, err := r.sendData(m, string(jjson))
682		if err != nil {
683			r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j))
684			return 0
685		}
686		return ptrSize
687	}
688	if !forAS {
689		return func(_ context.Context, m api.Module, commandPtr, commandSize uint32) uint64 {
690			cmd, err := r.readString(m, "exec cmd", commandPtr, commandSize)
691			if err != nil {
692				return 0
693			}
694			return f(m, cmd)
695		}
696	} else {
697		return func(_ context.Context, m api.Module, commandPtr uint32) uint64 {
698			cmd, err := r.readASString(m, "exec cmd", commandPtr)
699			if err != nil {
700				return 0
701			}
702			return f(m, cmd)
703		}
704	}
705}
706
707func (r *runtime) ReportBuilder(forAS bool) interface{} {
708	f := func(reportJson string) {
709		if r.command == nil || r.commit == nil {
710			r.logger.Error("can't report in conf stage", errors.New("report not allowed"))
711			return
712		}
713		report := pluginLib.ReportToGitroot{}
714		err := json.Unmarshal([]byte(reportJson), &report)
715		if err != nil {
716			r.logger.Error("can't unmarshal reportJson bad format", err)
717			return
718		}
719		re := pluginLib.Report{
720			Level:      report.Level,
721			Content:    report.Content,
722			FromPlugin: r.plugin.Name,
723			FromBranch: r.command.branch.Short(),
724			FromCommit: r.commit.hash.String(),
725		}
726		reJson, err := json.Marshal(re)
727		if err != nil {
728			r.logger.Error("can't marshal reJson bad format", err)
729			return
730		}
731		r.logger.Debug("report added", logger.NewLoggerPair("report", reJson))
732		r.reports = append(r.reports, re)
733	}
734	if !forAS {
735		return func(_ context.Context, m api.Module, reportPtr, reportSize uint32) {
736			reportJson, err := r.readString(m, "report json", reportPtr, reportSize)
737			if err != nil {
738				return
739			}
740			f(reportJson)
741		}
742	} else {
743		return func(_ context.Context, m api.Module, reportPtr uint32) {
744			reportJson, err := r.readASString(m, "report json", reportPtr)
745			if err != nil {
746				return
747			}
748			f(reportJson)
749		}
750	}
751}
752
753func (r *runtime) Close() error {
754	return r.wazRun.Close(r.ctx)
755}
756
757func (r *runtime) sendData(module api.Module, message string) (ptrSizeToSend uint64, err error) {
758	malloc := module.ExportedFunction("gitrootAlloc")
759	if malloc == nil {
760		malloc = module.ExportedFunction("malloc")
761	}
762
763	s := uint64(len(message))
764
765	results, err := malloc.Call(r.ctx, s)
766	if err != nil {
767		return 0, oops.Wrapf(err, "can't malloc memory")
768	}
769	ptrA := results[0]
770
771	// The pointer is a linear memory offset, which is where we write the name.
772	if !module.Memory().WriteString(uint32(ptrA), message) {
773		return 0, oops.Errorf("can't write memory")
774	}
775
776	r.memoryToGarbage = append(r.memoryToGarbage, ptrSize{ptr: ptrA, size: s})
777
778	return (ptrA << uint64(32)) | s, nil
779}
780
781func (r *runtime) readString(m api.Module, errorMsg string, ptr uint32, size uint32) (string, error) {
782	mem := m.Memory()
783	if mem == nil {
784		err := errors.New("memory not exist")
785		r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size))
786		return "", err
787	}
788	txt, ok := mem.Read(ptr, size)
789	if !ok {
790		err := errors.New("memory read fail")
791		r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size))
792		return "", err
793	}
794	return string(txt), nil
795}
796
797func (r *runtime) readASString(m api.Module, errorMsg string, offset uint32) (string, error) {
798	// Length is four bytes before pointer.
799	byteCount, ok := m.Memory().ReadUint32Le(offset - 4)
800	if !ok || byteCount%2 != 0 {
801		err := errors.New("Memory.ReadUint32Le fail")
802		r.logger.Error(errorMsg, err, logger.NewLoggerPair("byteCount", byteCount), logger.NewLoggerPair("offset", offset))
803		return "", err
804	}
805	buf, ok := m.Memory().Read(offset, byteCount)
806	if !ok {
807		err := errors.New("Memory.Read fail")
808		r.logger.Error(errorMsg, err, logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
809		return "", err
810	}
811	u16s := make([]uint16, len(buf)/2)
812
813	lb := len(buf)
814	for i := 0; i < lb; i += 2 {
815		u16s[i/2] = uint16(buf[i]) + (uint16(buf[i+1]) << 8)
816	}
817	return string(utf16.Decode(u16s)), nil
818}
819
820type ptrSize struct {
821	ptr  uint64
822	size uint64
823}
824
825func (r *runtime) free(module api.Module, ptrSize []ptrSize) error {
826	free := module.ExportedFunction("gitrootFree")
827	if free == nil {
828		free = module.ExportedFunction("free")
829	}
830
831	if free != nil && len(free.Definition().ParamTypes()) == 1 {
832		for _, p := range ptrSize {
833			_, err := free.Call(r.ctx, p.ptr)
834			if err != nil {
835				r.logger.Error("can't free pluginConf", err)
836			}
837		}
838	} else if free != nil {
839		for _, p := range ptrSize {
840			_, err := free.Call(r.ctx, p.ptr, p.size)
841			if err != nil {
842				r.logger.Error("can't free pluginConf", err)
843			}
844		}
845	}
846
847	return nil
848}