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 main
6
7import (
8 "fmt"
9 "path/filepath"
10 "regexp"
11 "strings"
12
13 "github.com/tidwall/gjson"
14 "gitroot.dev/libs/golang/glob"
15 "gitroot.dev/libs/golang/plugin/model"
16)
17
18type Conf struct {
19 Title string `json:"title"`
20 Description string `json:"description"`
21 Format string `json:"format"`
22 TableHeader string `json:"tableHeader"`
23 For string `json:"for"`
24 glob *glob.Glob
25 Where string `json:"where"`
26 Selects []string `json:"selects"`
27 selections []*regexp.Regexp
28 To string `json:"to"`
29 Paginator int64 `json:"paginator"`
30 Sort string `json:"sort"`
31 SortOrder string `json:"sortOrder"`
32 Fs string `json:"filesystem"`
33 GroupBy string `json:"groupBy"`
34 Metadata map[string]string `json:"metadata"`
35 CustomLayout string `json:"customLayout"`
36}
37
38const roadmapDescription = `This file provides an overview of the direction this project is heading.
39
40> When it is obvious that the goals cannot be reached, donβt adjust the goals, adjust the action steps.
41
42_[Confucius](https://en.wikipedia.org/wiki/Confucius)_
43
44## List`
45
46var defaultConf = map[string]any{"boards": []Conf{
47 {Title: "Roadmaps", Description: roadmapDescription, Format: "table", TableHeader: "| | priority |", For: "**/*.md", Where: "kind: 'roadmap'", Selects: []string{"priority: (\\d+)"}, To: "boards/roadmap.md", Paginator: 0, Sort: "title", SortOrder: "asc", Fs: "git"},
48 {Title: "Todo is lie", Description: "", Format: "embed", TableHeader: "", For: "**/*|!.gitroot/**", Where: "// TODO", Selects: []string{}, To: "boards/todos.md", Paginator: 0, Sort: "file", SortOrder: "asc", Fs: "git"},
49 {Title: "All Issues", Description: "All\n[New](./triage.md)", Format: "table", TableHeader: "| | status | priority |", For: "**/*.md", Where: "kind: issue", Selects: []string{"status: (.*)", "priority: (\\d+)"}, To: "boards/issues.md", Paginator: 10, Sort: "select[1]", SortOrder: "desc", Fs: "git"},
50 {Title: "New Issues", Description: "[All](./issues.md)\nNew", Format: "list", TableHeader: "", For: "**/*.md", Where: "status: triage", Selects: []string{}, To: "boards/triage.md", Paginator: 10, Sort: "", SortOrder: "asc", Fs: "git"},
51}}
52
53var defaultRun = []model.PluginRun{{
54 Path: "**/*",
55 Branch: []string{"main"},
56 When: model.PluginRunWhenAll,
57 Func: []model.PluginFunc{},
58 Write: model.PluginWrite{
59 Git: []model.PluginWriteRight{{
60 Path: "boards/*.md",
61 Can: model.PluginWriteRightCanAll,
62 }},
63 Web: []model.PluginWriteRight{},
64 Exec: []model.PluginExecRight{},
65 CallFunc: []model.PluginCallFuncRight{
66 {PluginPURL: PLUGIN_APEX, FuncName: PLUGIN_APEX_FUNC, Configuration: map[string]any{}},
67 {PluginPURL: PLUGIN_MARKDOWN, FuncName: PLUGIN_MARKDOWN_FUNC, Configuration: map[string]any{}},
68 },
69 },
70 Configuration: defaultConf,
71}}
72
73func (p *Plugin) unmarshalConf(serializedConf string) []Conf {
74 c := []Conf{}
75 for _, meta := range gjson.Parse(serializedConf).Get("boards").Array() {
76 selects := make([]string, 0)
77 for _, s := range meta.Get("selects").Array() {
78 selects = append(selects, s.String())
79 }
80 metadatas := map[string]string{}
81 meta.Get("metadata").ForEach(func(key, value gjson.Result) bool {
82 if key.String() != "" && value.String() != "" {
83 metadatas[key.String()] = value.String()
84 }
85 return true
86 })
87 myConf := Conf{
88 Title: meta.Get("title").String(),
89 Description: meta.Get("description").String(),
90 Format: meta.Get("format").String(),
91 TableHeader: meta.Get("tableHeader").String(),
92 For: meta.Get("for").String(),
93 Where: meta.Get("where").String(),
94 Selects: selects,
95 To: meta.Get("to").String(),
96 Paginator: meta.Get("paginator").Int(),
97 Sort: meta.Get("sort").String(),
98 SortOrder: meta.Get("sortOrder").String(),
99 Fs: meta.Get("filesystem").String(),
100 GroupBy: meta.Get("groupBy").String(),
101 Metadata: metadatas,
102 CustomLayout: meta.Get("customLayout").String(),
103 }
104 //default format
105 if myConf.Format == "" {
106 myConf.Format = "list"
107 }
108 // default fs
109 if myConf.Fs == "" {
110 myConf.Fs = "git"
111 }
112 //select to selections
113 selections := make([]*regexp.Regexp, 0)
114 for _, se := range myConf.Selects {
115 if r, err := regexp.Compile(se); err != nil {
116 p.server.LogError("conf regexp.Compile select="+se, err)
117 } else {
118 selections = append(selections, r)
119 }
120 }
121 myConf.selections = selections
122 //default paginator
123 if myConf.Paginator == 0 {
124 myConf.Paginator = -1
125 }
126 //for glob
127 glob, err := glob.NewGlob(myConf.For)
128 if err != nil {
129 p.server.LogError("error glob", err)
130 }
131 myConf.glob = glob
132
133 c = append(c, myConf)
134 }
135 return c
136}
137
138func (c Conf) relativePath(toPath string) string {
139 absFromPath, _ := filepath.Abs(c.To)
140 dirFromPath, _ := filepath.Split(absFromPath)
141 absToPath, _ := filepath.Abs(toPath)
142 dirToPath, fileToPath := filepath.Split(absToPath)
143 path, err := filepath.Rel(dirFromPath, dirToPath)
144 if err != nil {
145 return toPath
146 }
147 return fmt.Sprintf("%s/%s", path, fileToPath)
148}
149
150func findTitle(path string, content []byte) string {
151 if strings.HasSuffix(path, ".md") { //special case, search first title
152 re := regexp.MustCompile(`(?m)^#\s(.*)$`)
153 t := re.FindSubmatch(content)
154 if len(t) > 1 {
155 return string(t[1])
156 }
157 }
158 return path
159}