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	"errors"
 12	"fmt"
 13	"os"
 14	"path/filepath"
 15	"time"
 16	"unicode/utf16"
 17
 18	"github.com/go-git/go-git/v5/plumbing"
 19	"github.com/go-git/go-git/v5/plumbing/object"
 20	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
 21	"github.com/samber/oops"
 22	"github.com/tetratelabs/wazero"
 23	"github.com/tetratelabs/wazero/api"
 24	"github.com/tetratelabs/wazero/imports/assemblyscript"
 25	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
 26	"github.com/tetratelabs/wazero/sys"
 27	pluginLib "gitroot.dev/libs/golang/plugin/model"
 28	grfs "gitroot.dev/server/fs"
 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	wazRun       wazero.Runtime
 59	fsWorktree   grfs.UpdatableFs
 60	fsWebcontent grfs.UpdatableFs
 61	fsCache      grfs.UpdatableFs
 62	commitHook   func(h plumbing.Hash)
 63	mergeHook    func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string)
 64}
 65
 66func newRuntime(ctx context.Context, manager *Manager, logger *logger.Logger) (*runtime, error) {
 67	r := &runtime{
 68		ctx:          ctx,
 69		manager:      manager,
 70		logger:       logger,
 71		wazRun:       wazero.NewRuntime(ctx),
 72		fsWorktree:   grfs.NewUpdatableFs(ctx, nil),
 73		fsWebcontent: grfs.NewUpdatableFs(ctx, nil),
 74		fsCache:      grfs.NewUpdatableFs(ctx, nil),
 75		commitHook:   nil,
 76		mergeHook:    nil,
 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		Instantiate(ctx)
108	if err != nil {
109		return nil, err
110	}
111	_, err = wasi_snapshot_preview1.Instantiate(ctx, r.wazRun)
112	if err != nil {
113		return nil, err
114	}
115	return r, nil
116}
117
118func (r *runtime) listen(c chan runtimeInputs) {
119	for i := range c {
120		r.logger.Debug("listen call", logger.NewLoggerPair("repo", i.repoName), logger.NewLoggerPair("isDiff", i.kind == runtimeInputsKindDiff), logger.NewLoggerPair("isWorktree", i.kind == runtimeInputsKindWorktree))
121		repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.listen"), i.repoName)
122		if err != nil {
123			r.logger.Error("open error in listen", err)
124			repo.Close()
125			continue
126		}
127		repoWriter, err := repo.WillWrite(plumbing.HEAD) //TODO should mount good branc directly inside i.commands
128		if err != nil {
129			r.logger.Error("will write error in listen", err, logger.NewLoggerPair("repo", repo.Name()))
130			repo.Close()
131			continue
132		}
133
134		if i.kind == runtimeInputsKindDiff {
135			timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins", repo.Name()))
136			err := r.start(i.ctx, repo, repoWriter, i.plugins, i.commands, func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string) {
137				if cmd != nil {
138					go func() {
139						time.Sleep(10 * time.Millisecond) //TODO find better to defer it until repo.close() is called (to be sur transactional writer has writen in disk)
140						r.manager.backgroundManager.DeleteBranch(i.repoName, toDeleteBranchName)
141						r.manager.backgroundManager.PostPush(pusher, i.repoName, []*packp.Command{cmd})
142					}()
143				}
144			})
145			if err != nil {
146				r.logger.Error("start error", err)
147			}
148			timerStop()
149		} else if i.kind == runtimeInputsKindWorktree {
150			r.logger.Info("start worktree", logger.NewLoggerPair("repo", repo.Name()))
151			timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins worktree", repo.Name()))
152			if err := r.worktree(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil {
153				r.logger.Error("start error", err)
154			}
155			timerStop()
156		}
157		repo.Close()
158	}
159}
160
161func (r *runtime) conf(ctx context.Context, plugin Plugin) ([]pluginLib.PluginRun, error) {
162	r.fsWorktree.Clear()
163	r.fsWebcontent.Clear()
164	r.fsCache.Clear()
165	r.repo = nil
166	r.plugin = plugin
167	r.command = nil
168	timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name))
169
170	r.logger.Debug("start plugin conf", logger.NewLoggerPair("name", plugin.Name))
171	m := r.wazRun.Module(plugin.uuid())
172	if m == nil {
173		r.logger.Debug("instantiate plugin conf", logger.NewLoggerPair("name", plugin.Name))
174		config := wazero.NewModuleConfig().
175			WithStdout(os.Stdout).WithStderr(os.Stderr).
176			WithSysWalltime().
177			WithSysNanotime().
178			WithRandSource(rand.Reader).
179			WithFSConfig(
180				wazero.NewFSConfig().
181					WithFSMount(r.fsWorktree, "worktree").
182					WithFSMount(r.fsWebcontent, "webcontent").
183					WithFSMount(r.fsCache, "cache"))
184		mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.uuid()).WithStartFunctions("_initialize", "install"))
185		if err != nil {
186			if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
187				fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
188				return nil, err
189			} else if !ok {
190				return nil, err
191			}
192		}
193		m = mod
194	} else {
195		r.logger.Debug("already exist conf", logger.NewLoggerPair("name", plugin.Name))
196	}
197	conf, err := r.callPluginForConf(ctx, m, r.logger.NewSubLogger(plugin.Name))
198	if err != nil {
199		r.logger.Error("finish plugin conf with error", err, logger.NewLoggerPair("name", plugin.Name))
200	}
201
202	r.logger.Debug("finish plugin conf", logger.NewLoggerPair("name", plugin.Name))
203	timerStop()
204	return conf, err
205}
206
207type runAfter struct {
208	cmd    *packp.Command
209	pusher user.SimpleUser
210}
211
212func (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 {
213	r.fsWorktree.Update(repoWriter.ToFs(ctx))
214	r.fsWebcontent.Update(r.manager.conf.DataWeb(repo.Name()))
215
216	r.repo = repo
217	r.repoWriter = repoWriter
218	r.command = nil
219
220	r.commitHook = func(hash plumbing.Hash) {
221		command, err := repoWriter.GetLastCommit(hash)
222		if err != nil {
223			r.logger.Error("can't GetLastCommit in start runtime", err)
224			return
225		}
226		newCmd, err := CommandForDiffFromCommitCmd(ctx, r.plugin.commiter.SimpleUser, command, r.command.branch)
227		if err != nil {
228			r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err)
229			return
230		}
231		commands = append(commands, newCmd)
232	}
233
234	r.mergeHook = mergeHook
235	defer func() {
236		r.commitHook = nil
237		r.mergeHook = nil
238	}()
239
240	for _, plugin := range plugins {
241		r.fsCache.Update(r.manager.conf.Cache(repo.Name(), plugin.Name))
242		r.plugin = plugin
243		timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name))
244		r.logger.Debug("start plugin", logger.NewLoggerPair("repo", repo.Name()), logger.NewLoggerPair("name", plugin.Name))
245		m := r.wazRun.Module(plugin.uuid())
246		if m == nil {
247			r.logger.Debug("instantiate plugin", logger.NewLoggerPair("name", plugin.Name))
248			config := wazero.NewModuleConfig().
249				WithSysWalltime().
250				WithSysNanotime().
251				WithRandSource(rand.Reader).
252				WithStdout(os.Stdout).WithStderr(os.Stderr).
253				WithFSConfig(wazero.NewFSConfig().
254					WithFSMount(r.fsWorktree, "worktree").
255					WithFSMount(r.fsWebcontent, "webcontent").
256					WithFSMount(r.fsCache, "cache"))
257			mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.uuid()).WithStartFunctions("_initialize", "install"))
258			if err != nil {
259				if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
260					fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
261					return nil
262				} else if !ok {
263					return err
264				}
265			}
266			m = mod
267		} else {
268			r.logger.Debug("already exist", logger.NewLoggerPair("name", plugin.Name))
269		}
270		l := logger.NewLogger(logger.WASM)
271		l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name))
272		cp := callPlugin{
273			manager:    r.manager,
274			plugin:     plugin,
275			repo:       repo,
276			repoWriter: repoWriter,
277			module:     m,
278			logger:     r.logger.NewSubLogger(plugin.Name),
279		}
280		if err := cp.callPluginForDiff(ctx, r, commands); err != nil {
281			r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Name))
282		}
283		l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name))
284		r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Name))
285		timerStop()
286	}
287	return nil
288}
289
290func (r *runtime) ForgeConf(_ context.Context, m api.Module) uint64 {
291	// TODO need issues/a716-conf-external-addr.md to work
292	_, ptrSize, err := r.sendData(m, "{\"domain\": \"\"}") //TODO free ptr (first arg)
293	if err != nil {
294		r.logger.Error("ForgeConf can't send data", err)
295		return 0
296	}
297	return ptrSize
298}
299
300func (r *runtime) ModifyContentBuilder(forAS bool) interface{} {
301	f := func(filename string, content string) {
302		if ok, err := checkWrite(r.pluginRun.write.git, r.fsWorktree, filename); !ok {
303			r.logger.Error("plugin can't write in git", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
304			return
305		}
306		r.logger.Debug("modifyContent", logger.NewLoggerPair("file", filename))
307		if err := r.repoWriter.Write(filename, []byte(content)); err != nil {
308			r.logger.Error("modifyContent can't open file", err, logger.NewLoggerPair("filepath", filename))
309			return
310		}
311	}
312
313	if !forAS {
314		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
315			filename, err := r.readString(m, "ModifyContent filename", filenamePtr, filenameSize)
316			if err != nil {
317				return
318			}
319			content, err := r.readString(m, "ModifyContent content", contentPtr, contentSize)
320			if err != nil {
321				return
322			}
323			f(filename, content)
324		}
325	} else {
326		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
327			filename, err := r.readASString(m, "ModifyContent filename", filenamePtr)
328			if err != nil {
329				return
330			}
331			content, err := r.readASString(m, "ModifyContent content", contentPtr)
332			if err != nil {
333				return
334			}
335			f(filename, content)
336		}
337	}
338}
339
340func (r *runtime) ModifyWebContentBuilder(forAS bool) interface{} {
341	f := func(filename string, content string) {
342		if ok, err := checkWrite(r.pluginRun.write.web, r.fsWebcontent, filename); !ok {
343			r.logger.Warn("plugin can't write in web", logger.NewLoggerPair("err", err), logger.NewLoggerPair("plugin", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
344			return
345		}
346		fullPath := r.repo.PathDataWeb(filename)
347		dir, _ := filepath.Split(fullPath)
348		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
349			r.logger.Error("modifyWebContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
350			return
351		}
352		if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
353			r.logger.Error("modifyWebContent can't open file", err, logger.NewLoggerPair("filepath", filename))
354			return
355		}
356		r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
357	}
358
359	if !forAS {
360		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
361			filename, err := r.readString(m, "ModifyWebContent filename", filenamePtr, filenameSize)
362			if err != nil {
363				return
364			}
365			content, err := r.readString(m, "ModifyWebContent content", contentPtr, contentSize)
366			if err != nil {
367				return
368			}
369			f(filename, content)
370		}
371	} else {
372		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
373			filename, err := r.readASString(m, "ModifyWebContent filename", filenamePtr)
374			if err != nil {
375				return
376			}
377			content, err := r.readASString(m, "ModifyWebContent content", contentPtr)
378			if err != nil {
379				return
380			}
381			f(filename, content)
382		}
383	}
384}
385
386func (r *runtime) ReplaceWebContentBuilder(forAS bool) interface{} {
387	f := func(filename string, oldContent string, content string) {
388		if ok, err := checkWrite(r.pluginRun.write.web, r.fsWebcontent, filename); !ok {
389			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))
390			return
391		}
392		fullPath := r.repo.PathDataWeb(filename)
393		dir, _ := filepath.Split(fullPath)
394		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
395			r.logger.Error("ReplaceWebContentBuilder can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
396			return
397		}
398		fileContent, err := os.ReadFile(fullPath)
399		if err != nil {
400			r.logger.Error("ReplaceWebContentBuilder can't ReadFile", err, logger.NewLoggerPair("filepath", filename))
401			return
402		}
403		if err := os.WriteFile(fullPath, bytes.Replace(fileContent, []byte(oldContent), []byte(content), 1), 0666); err != nil {
404			r.logger.Error("ReplaceWebContentBuilder can't open file", err, logger.NewLoggerPair("filepath", filename))
405			return
406		}
407		r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
408	}
409
410	if !forAS {
411		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, oldContentPtr, oldContentSize, contentPtr, contentSize uint32) {
412			filename, err := r.readString(m, "ReplaceWebContentBuilder filename", filenamePtr, filenameSize)
413			if err != nil {
414				return
415			}
416			oldContent, err := r.readString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr, oldContentSize)
417			if err != nil {
418				return
419			}
420			content, err := r.readString(m, "ReplaceWebContentBuilder content", contentPtr, contentSize)
421			if err != nil {
422				return
423			}
424			f(filename, oldContent, content)
425		}
426	} else {
427		return func(_ context.Context, m api.Module, filenamePtr, oldContentPtr, contentPtr uint32) {
428			filename, err := r.readASString(m, "ReplaceWebContentBuilder filename", filenamePtr)
429			if err != nil {
430				return
431			}
432			oldContent, err := r.readASString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr)
433			if err != nil {
434				return
435			}
436			content, err := r.readASString(m, "ReplaceWebContentBuilder content", contentPtr)
437			if err != nil {
438				return
439			}
440			f(filename, oldContent, content)
441		}
442	}
443}
444
445func (r *runtime) ModifyCacheContentBuilder(forAS bool) interface{} {
446	f := func(filename string, content string) {
447		fullPath := filepath.Join(r.manager.conf.PathCache(), r.repo.Name(), r.plugin.Name, filename)
448		dir, _ := filepath.Split(fullPath)
449		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
450			r.logger.Error("ModifyCacheContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
451			return
452		}
453		if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
454			r.logger.Error("ModifyCacheContent can't open file", err, logger.NewLoggerPair("filepath", filename))
455			return
456		}
457		r.logger.Debug("Write in cache", logger.NewLoggerPair("fullPath", fullPath))
458	}
459	if !forAS {
460		return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
461			filename, err := r.readString(m, "ModifyCacheContent filename", filenamePtr, filenameSize)
462			if err != nil {
463				return
464			}
465			content, err := r.readString(m, "ModifyCacheContent content", contentPtr, contentSize)
466			if err != nil {
467				return
468			}
469			f(filename, content)
470		}
471	} else {
472		return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
473			filename, err := r.readASString(m, "ModifyCacheContent filename", filenamePtr)
474			if err != nil {
475				return
476			}
477			content, err := r.readASString(m, "ModifyCacheContent content", contentPtr)
478			if err != nil {
479				return
480			}
481			f(filename, content)
482		}
483	}
484}
485
486func (r *runtime) CommitAllBuilder(forAS bool) interface{} {
487	f := func(msg string, err error) {
488		if err != nil {
489			return
490		}
491		if h, err := r.repoWriter.CommitAll(msg, r.plugin.commiter); err != nil {
492			r.logger.Error("commitAll can't commit", err)
493			return
494		} else {
495			r.logger.Debug("Commit", logger.NewLoggerPair("message", msg), logger.NewLoggerPair("hash", h.String()))
496			if r.commitHook != nil && !h.IsZero() {
497				r.commitHook(h)
498			}
499		}
500	}
501	if !forAS {
502		return func(_ context.Context, m api.Module, offset, byteCount uint32) {
503			f(r.readString(m, "commitAll", offset, byteCount))
504		}
505	} else {
506		return func(_ context.Context, m api.Module, offset uint32) {
507			f(r.readASString(m, "commitAll", offset))
508		}
509	}
510}
511
512func (r *runtime) DiffWithParentBuilder(forAS bool) interface{} {
513	f := func(m api.Module, hash string, oldfile string, newfile string) uint64 {
514		r.logger.Info("In diffWithParent before found ancestor")
515		diffStr, err := r.repoWriter.GetDiff(plumbing.NewHash(string(hash)), string(oldfile), string(newfile))
516		if err != nil {
517			r.logger.Error("GetDiff", err)
518			return 0
519		}
520		r.logger.Debug("In diffWithParent send diff", logger.NewLoggerPair("diff", diffStr))
521		_, ptrSize, err := r.sendData(m, diffStr) //TODO free ptr (first arg)
522		if err != nil {
523			r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", diffStr))
524			return 0
525		}
526		return ptrSize
527	}
528	if !forAS {
529		return func(_ context.Context, m api.Module, hashPtr, hashSize, oldFilenamePtr, oldFilenameSize, newFilenamePtr, newFilenameSize uint32) uint64 {
530			hash, err := r.readString(m, "DiffWithParent hash", hashPtr, hashSize)
531			if err != nil {
532				return 0
533			}
534			oldFilename, err := r.readString(m, "DiffWithParent oldFilename", oldFilenamePtr, oldFilenameSize)
535			if err != nil {
536				return 0
537			}
538			newFilename, err := r.readString(m, "DiffWithParent newFilename", newFilenamePtr, newFilenameSize)
539			if err != nil {
540				return 0
541			}
542			return f(m, hash, oldFilename, newFilename)
543		}
544	} else {
545		return func(_ context.Context, m api.Module, hashPtr, oldFilenamePtr, newFilenamePtr uint32) uint64 {
546			hash, err := r.readASString(m, "DiffWithParent hash", hashPtr)
547			if err != nil {
548				return 0
549			}
550			oldFilename, err := r.readASString(m, "DiffWithParent oldFilenamePtr", oldFilenamePtr)
551			if err != nil {
552				return 0
553			}
554			newFilename, err := r.readASString(m, "DiffWithParent newFilename", newFilenamePtr)
555			if err != nil {
556				return 0
557			}
558			return f(m, hash, oldFilename, newFilename)
559		}
560	}
561}
562
563func (r *runtime) LogBuilder(forAS bool) interface{} {
564	f := func(msg string, err error) {
565		if err != nil {
566			return
567		}
568		r.logger.Debug(msg, logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name))
569	}
570	if !forAS {
571		return func(_ context.Context, m api.Module, offset, byteCount uint32) {
572			f(r.readString(m, "log", offset, byteCount))
573		}
574	} else {
575		return func(_ context.Context, m api.Module, offset uint32) {
576			f(r.readASString(m, "log", offset))
577		}
578	}
579}
580
581func (r *runtime) LogErrorBuilder(forAS bool) interface{} {
582	f := func(msg string, err string) {
583		r.logger.Error(msg, errors.New(err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name))
584	}
585	if !forAS {
586		return func(_ context.Context, m api.Module, offset, byteCount, errPtr, errSize uint32) {
587			msg, err := r.readString(m, "LogError msg", offset, byteCount)
588			if err != nil {
589				return
590			}
591			errMsg, err := r.readString(m, "LogError err", errPtr, errSize)
592			if err != nil {
593				return
594			}
595			f(msg, errMsg)
596		}
597	} else {
598		return func(_ context.Context, m api.Module, offset, errPtr uint32) {
599			msg, err := r.readASString(m, "LogError msg", offset)
600			if err != nil {
601				return
602			}
603			errMsg, err := r.readASString(m, "LogError err", errPtr)
604			if err != nil {
605				return
606			}
607			f(msg, errMsg)
608		}
609	}
610}
611
612func (r *runtime) MergeBuilder(forAS bool) interface{} {
613	f := func(from string, to string) {
614		r.logger.Debug("try to merge", logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
615		cmd, err := r.repoWriter.Merge(from, to, r.plugin.commiter, r.command.pusher)
616		if err != nil {
617			r.logger.Error("can't Merge", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
618		}
619		if r.mergeHook != nil {
620			r.mergeHook(cmd, r.command.pusher, to)
621		}
622	}
623	if !forAS {
624		return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) {
625			from, err := r.readString(m, "Merge from", fromPtr, fromSize)
626			if err != nil {
627				return
628			}
629			to, err := r.readString(m, "Merge to", toPtr, toSize)
630			if err != nil {
631				return
632			}
633			f(from, to)
634		}
635	} else {
636		return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) {
637			from, err := r.readASString(m, "Merge from", fromPtr)
638			if err != nil {
639				return
640			}
641			to, err := r.readASString(m, "Merge to", toPtr)
642			if err != nil {
643				return
644			}
645			f(from, to)
646		}
647	}
648}
649
650func (r *runtime) GetCommitsBuilder(forAS bool) interface{} {
651	f := func(m api.Module, from string, to string) uint64 {
652		commits := []commitForDiffCommit{}
653		if err := r.repoWriter.WalkCommit(plumbing.NewHash(to), plumbing.NewHash(from), func(c *object.Commit) error {
654			r.logger.Debug("found commit", logger.NewLoggerPair("hash", c.Hash.String()), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
655			commits = append(commits, commitToCommitForDiff(c, nil))
656			return nil
657		}); err != nil {
658			r.logger.Error("can't WalkCommit", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
659		}
660		r.logger.Debug("found commits", logger.NewLoggerPair("nb", len(commits)), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
661		j, err := Marshall("//TODO", commits)
662		if err != nil {
663			r.logger.Error("can't Marshall", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
664		}
665		_, ptrSize, err := r.sendData(m, j) //TODO free ptr (first arg)
666		if err != nil {
667			r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j))
668			return 0
669		}
670		return ptrSize
671	}
672	if !forAS {
673		return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) uint64 {
674			from, err := r.readString(m, "Merge from", fromPtr, fromSize)
675			if err != nil {
676				return 0
677			}
678			to, err := r.readString(m, "Merge to", toPtr, toSize)
679			if err != nil {
680				return 0
681			}
682			return f(m, from, to)
683		}
684	} else {
685		return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) uint64 {
686			from, err := r.readASString(m, "Merge from", fromPtr)
687			if err != nil {
688				return 0
689			}
690			to, err := r.readASString(m, "Merge to", toPtr)
691			if err != nil {
692				return 0
693			}
694			return f(m, from, to)
695		}
696	}
697}
698
699func (r *runtime) Close() error {
700	return r.wazRun.Close(r.ctx)
701}
702
703func (r *runtime) sendData(module api.Module, message string) (ptr uint32, ptrSize uint64, err error) {
704	malloc := module.ExportedFunction("malloc")
705
706	s := uint64(len(message))
707
708	results, err := malloc.Call(r.ctx, s)
709	if err != nil {
710		return 0, 0, oops.Wrapf(err, "can't malloc memory")
711	}
712	ptrA := results[0]
713
714	// The pointer is a linear memory offset, which is where we write the name.
715	if !module.Memory().WriteString(uint32(ptrA), message) {
716		return 0, 0, oops.Errorf("can't write memory")
717	}
718
719	return uint32(ptrA), (ptrA << uint64(32)) | s, nil
720}
721
722func (r *runtime) readString(m api.Module, errorMsg string, ptr uint32, size uint32) (string, error) {
723	txt, ok := m.Memory().Read(ptr, size)
724	if !ok {
725		err := errors.New("Memory.Read fail")
726		r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size))
727		return "", err
728	}
729	return string(txt), nil
730}
731
732func (r *runtime) readASString(m api.Module, errorMsg string, offset uint32) (string, error) {
733	// Length is four bytes before pointer.
734	byteCount, ok := m.Memory().ReadUint32Le(offset - 4)
735	if !ok || byteCount%2 != 0 {
736		err := errors.New("Memory.ReadUint32Le fail")
737		r.logger.Error(errorMsg, err, logger.NewLoggerPair("byteCount", byteCount), logger.NewLoggerPair("offset", offset))
738		return "", err
739	}
740	buf, ok := m.Memory().Read(offset, byteCount)
741	if !ok {
742		err := errors.New("Memory.Read fail")
743		r.logger.Error(errorMsg, err, logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
744		return "", err
745	}
746	u16s := make([]uint16, len(buf)/2)
747
748	lb := len(buf)
749	for i := 0; i < lb; i += 2 {
750		u16s[i/2] = uint16(buf[i]) + (uint16(buf[i+1]) << 8)
751	}
752	return string(utf16.Decode(u16s)), nil
753}