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