// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package main // compile cmd // tinygo build -o grafter-0.0.1.wasm -scheduler=none --no-debug -target=wasi ./ import ( "bytes" _ "embed" "fmt" "io" "io/fs" "os" "path/filepath" "regexp" "strings" "github.com/tidwall/gjson" gitroot "gitroot.dev/libs/golang/plugin" "gitroot.dev/libs/golang/plugin/model" ) type Plugin struct { server model.Server conf pluginConf newGraft *graft } type pluginConf struct { defaultTargetBranch string } const ( statusNew = "" statusDraft = "draft" statusReview = "review" statusMerge = "merge" ) type graft struct { status string filename string branch string commitsLines []string pushContent []string content string currentHash string } var defaultRun = []model.PluginRun{{ Path: "**/*", Branch: []string{"*"}, When: model.PluginRunWhenAll, Write: model.PluginWrite{ Git: []model.PluginWriteRight{{ Path: "**/*", Can: model.PluginWriteRightCanAll, }}, Web: []model.PluginWriteRight{}, }, Configuration: map[string]any{"defaultTargetBranch": "main"}, }} var defaultGraft = `--- target: %s status: draft --- # Want to merge %s Please add description of your work here. When your changes are ready to be reviewed, change the status from 'status: draft' to 'status: review'. ` var defaultReviewMessage = ` --- Please review changes with: ` + strings.Repeat("`", 3) + `shell git fetch origin %s git checkout %s git diff origin/HEAD..HEAD ` + strings.Repeat("`", 3) + ` Make your changes about the code inside the code directly (remarks, questions can be made in the form of comments). General remarks on the why can be made here. When done commit and push directly on this branch. Graft plugin will make the diff and display it here. When then changes are ready to be merged, change the status from 'status: review' to 'status: merge'. ` var targetRegexp = regexp.MustCompile(`- target:\s(.*)`) func (p *Plugin) Init(repoName string, confHasChanged bool, serializedConf string) error { defaultTargetBranch := gjson.Parse(serializedConf).Get("defaultTargetBranch").String() if defaultTargetBranch == "" { defaultTargetBranch = "main" } p.conf = pluginConf{ defaultTargetBranch: defaultTargetBranch, } p.newGraft = nil return nil } func (p *Plugin) StartCommit(commit model.Commit) error { //does nothing if user push on main if commit.Branch == p.conf.defaultTargetBranch { return nil } commitLine := fmt.Sprintf("### %s (%s)\n", commit.Message, commit.Hash) if p.newGraft == nil { graftFilepath := fmt.Sprintf("grafts/%s.md", commit.Branch) file, err := p.server.Worktree().Open(graftFilepath) if err != nil && !os.IsNotExist(err) { return err } if file == nil { p.newGraft = &graft{ status: statusNew, filename: graftFilepath, branch: commit.Branch, commitsLines: []string{commitLine}, content: fmt.Sprintf(defaultGraft, p.conf.defaultTargetBranch, commit.Branch), } } else { filecontent, err := io.ReadAll(file) if err != nil { return err } metas, needMeta := p.parseMetas(filecontent) if needMeta { if bytes.HasPrefix(filecontent, []byte("---\n")) { filecontent = bytes.TrimPrefix(filecontent, []byte("---\n")) filecontent = filecontent[bytes.Index(filecontent, []byte("---\n"))+4:] } filecontent = append(p.serializeMetas(metas), bytes.TrimPrefix(filecontent, []byte("\n"))...) } p.newGraft = &graft{ status: metas["status"], filename: graftFilepath, branch: commit.Branch, commitsLines: []string{commitLine}, content: string(filecontent), } formattedReviewMessage := fmt.Sprintf(defaultReviewMessage, commit.Branch, commit.Branch) if metas["status"] == statusReview && !strings.Contains(p.newGraft.content, formattedReviewMessage) { p.newGraft.content = fmt.Sprintf("%s%s", p.newGraft.content, formattedReviewMessage) } } } else { p.newGraft.commitsLines = append(p.newGraft.commitsLines, commitLine) } p.newGraft.pushContent = []string{} p.newGraft.currentHash = commit.Hash return nil } func (p *Plugin) serializeMetas(metas map[string]string) []byte { m := strings.Builder{} m.WriteString("---\n") if target, ok := metas["target"]; ok { m.WriteString(fmt.Sprintf("target: %s\n", target)) } if status, ok := metas["status"]; ok { m.WriteString(fmt.Sprintf("status: %s\n", status)) } if reviewers, ok := metas["reviewers"]; ok { m.WriteString(fmt.Sprintf("reviewers: %s\n", reviewers)) } m.WriteString("---\n\n") return []byte(m.String()) } func (p *Plugin) parseMetas(filecontent []byte) (map[string]string, bool) { metas := map[string]string{} fileSplited := strings.SplitN(string(filecontent), "---", 3) needMeta := false if len(fileSplited) > 2 { metasStr := strings.Split(fileSplited[1], "\n") for _, m := range metasStr { keyVal := strings.Split(m, ":") if len(keyVal) > 1 { metas[strings.TrimSpace(keyVal[0])] = strings.TrimSpace(keyVal[1]) } } } if _, ok := metas["status"]; !ok { needMeta = true metas["status"] = statusDraft } if _, ok := metas["target"]; !ok { needMeta = true metas["target"] = p.conf.defaultTargetBranch } if _, ok := metas["reviewers"]; !ok && metas["status"] == statusReview { needMeta = true metas["reviewers"] = "[]" } return metas, needMeta } func (p *Plugin) AddFile(path string) error { //does nothing if user push on main if p.newGraft == nil { return nil } if path != p.newGraft.filename { from, _ := filepath.Abs(path) relPath, _ := filepath.Rel("/grafts/", from) p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- ++ [%s](%s)", path, filepath.Clean(relPath))) } return nil } func (p *Plugin) ModFile(fromPath string, toPath string) error { //does nothing if user push on main if p.newGraft == nil { return nil } if toPath != p.newGraft.filename { from, _ := filepath.Abs(toPath) relPath, _ := filepath.Rel("/grafts/", from) if fromPath != toPath { p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- %s -> [%s](%s)", fromPath, toPath, filepath.Clean(relPath))) } p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- +- [%s](%s)", toPath, filepath.Clean(relPath))) if p.newGraft.status == statusReview { if diff, err := p.server.DiffWithParent(p.newGraft.currentHash, fromPath, toPath); err == nil { p.newGraft.pushContent = append(p.newGraft.pushContent, diff) } else { p.server.LogError("can't get diff", err) } } } return nil } func (p *Plugin) DelFile(path string) error { //does nothing if user push on main if p.newGraft == nil { return nil } if path != p.newGraft.filename { from, _ := filepath.Abs(path) relPath, _ := filepath.Rel("/grafts/", from) p.newGraft.pushContent = append(p.newGraft.pushContent, fmt.Sprintf("- -- [%s](%s)", path, filepath.Clean(relPath))) } return nil } func (p *Plugin) EndCommit(commit model.Commit) error { //does nothing if user push on main if p.newGraft == nil { return nil } if len(p.newGraft.pushContent) > 0 { p.newGraft.commitsLines[len(p.newGraft.commitsLines)-1] = fmt.Sprintf("%s\n%s\n", p.newGraft.commitsLines[len(p.newGraft.commitsLines)-1], strings.Join(p.newGraft.pushContent, "\n")) } else { // a commit which touch others files or only graft file is ignored p.newGraft.commitsLines = p.newGraft.commitsLines[:len(p.newGraft.commitsLines)-1] } return nil } func (p *Plugin) Finish() error { // does nothing if user push on main if p.newGraft == nil { return nil } content := p.newGraft.content if len(p.newGraft.commitsLines) > 0 { commit := "commit" if len(p.newGraft.commitsLines) > 1 { commit = "commits" } content = fmt.Sprintf("%s\n---\n\n## Push %d %s\n\n%s", p.newGraft.content, len(p.newGraft.commitsLines), commit, strings.Join(p.newGraft.commitsLines, "\n")) } p.server.ModifyContent(p.newGraft.filename, content) message := "graft updated" if p.newGraft.status == statusNew { message = "graft created" } p.server.CommitAllIfNeeded(message) if p.newGraft.status == statusMerge { all := targetRegexp.FindStringSubmatch(p.newGraft.content) if len(all) > 1 { p.server.Merge(all[1], p.newGraft.branch) } else { p.server.Merge(p.conf.defaultTargetBranch, p.newGraft.branch) } } return nil } func (p *Plugin) Report(report model.Report) error { if report.FromBranch == p.conf.defaultTargetBranch { return nil } graftFilepath := fmt.Sprintf("grafts/%s.md", report.FromBranch) fs.WalkDir(p.server.Worktree(), "/", func(path string, d fs.DirEntry, err error) error { p.server.Log(path) return nil }) filecontent, err := fs.ReadFile(p.server.Worktree(), graftFilepath) if err != nil { return err } content := fmt.Sprintf("%s\n---\n\n## Report **%s** from %s\n\n%s", filecontent, report.Level, report.FromPlugin, strings.Join(report.Content, "\n")) p.server.ModifyContent(graftFilepath, content) p.server.CommitAllIfNeeded("graft updated with report") return nil } func Build(server model.Server) model.Plugin { p := &Plugin{ server: server, } server.PluginOption(model.WithPluginReporter(p.Report)) return p } //go:wasmexport install func main() { gitroot.Register(defaultRun, Build) }