GitRoot
Craft your forge, Build your project, Grow your community freely
1---
2og:title: Issues, Tickets, Discussions... Call them what you want!
3og:description: No database, no vendor lock-in, just pure Git. Discover how GitRoot reimagines issues, boards, and TODOs using only Markdown files and smart plugins.
4description: No database, no vendor lock-in, just pure Git. Discover how GitRoot reimagines issues, boards, and TODOs using only Markdown files and smart plugins.
5og:image: https://gitroot.dev/branches/issueBlog/blog/issues-cover.png
6---
7
8# Issues, Tickets, Discussions... Call them what you want!
9
10Many of you, when checking out [https://gitroot.dev](https://gitroot.dev), have told me that the `π Issues` button shouldn't be labeled "Issues", but rather "Tickets" or "Discussions".
11
12First of all, thanks for the feedback! I really appreciate it, that's how I can build a better forge. Of course, I will change the label, but developing a forge means having a lot of different areas to work on: issues/boards, git, code, CI/CD, and with GitRoot, even more, like plugins and WASM...
13
14So, why don't I just change this label right away? In reality, this label isn't hardcoded inside GitRoot. It's only there because I configured plugins to display it. I want to take this opportunity to explain how GitRoot works, because the platform is developed around this exact concept: how to customize a forge at the project level.
15
16## The Original Idea
17
18Imagine you are developing a forge, and to avoid vendor lock-in, you decide not to put anything outside of Git. You choose not to use any database at all. In this context, how do you design an "issue" system?
19
20In traditional forges, an "issue" consists of a title and a description created by a user. We can represent it like this:
21
22```mermaid
23classDiagram
24 class Issue{
25 int id
26 string title
27 string description
28 }
29 class User{
30 int id
31 string email
32 }
33 class Status{
34 <<enumeration>>
35 Open
36 TODO
37 DOING
38 CLOSE
39 }
40 Issue -- User : Author
41 Issue -- Status : State
42```
43
44Easy, right? Well, how do you represent that in GitRoot using only files? This question was running through my mind for months before I finally found a solution.
45
46## The Basic Solution
47
48Since I only have files at my disposal, what actually constitutes an "issue"? I need to add some metadata to a file so the system understands that this file is an issue and not, say, a `main.go`. Iβll probably want to group all these "issues" into a specific directory, because while GitRoot technically lets you do whatever you want (even dropping an issue right in the middle of your source code), I personally prefer to keep my files organized!
49
50A simple text format that handles metadata perfectly is Markdown. I use Markdown here, but any other markup language could work. Let's create a file named `issues/1.md`:
51
52```markdown
53---
54id: 1
55user: email@email.com
56status: OPEN
57---
58
59# My title
60
61My description where I can detail what my problem is.
62```
63
64Yes, it really is that simple. In GitRoot, to create an issue, you just create a file, add some metadata, commit, and push. Boom, you have an issue.
65
66Now, you might be thinking: "Well, I lose a lot of control with that, the status could be anything!" or "How are other users supposed to know what to put where?". You're right, and that is exactly where plugins enter the game.
67
68## A Better Solution
69
70If I simplify this as much as possible, what does an external contributor actually need to provide to create an issue? Just a title and a description. So, let's create a file named `issues/my_issue.md`:
71
72```markdown
73# My title
74
75My description where I can detail what my problem is.
76```
77
78Then, we run `git commit && git push`. Since I have configured the [ladybug plugin](../plugins/name/ladybug.md) like this:
79
80```yaml
81- name: ladybug
82 ...
83 - path: issues/**/*.md
84 branch:
85 - "*"
86 when:
87 - add
88 - mod
89 func: []
90 write:
91 git:
92 - path: issues/**/*.md
93 can:
94 - mod
95 web: []
96 exec: []
97 callfunc: []
98 configuration:
99 metadata:
100 - default: autogenerated
101 mandatory: true
102 name: id
103 type: crc16
104 - default: OPEN
105 mandatory: true
106 name: status
107 type: string
108 - default: email@email.com
109 mandatory: false
110 name: user
111 type: string
112```
113
114> You can read this configuration as: "For every Markdown file in the `issues` directory across all branches, if the file doesn't have these metadata fields, automatically add these default values."
115
116Now, if you are a Git purist, a server creating commits right on top of yours might sound a bit radical, or even terrifying. But look closely at the `write` block in the YAML configuration above:
117
118```yaml
119write:
120 git:
121 - path: issues/**/*.md
122 can:
123 - mod
124```
125
126GitRoot doesn't just let plugins run wild, the project configuration must explicitly grant the plugin permission to write or modify files on specific paths. If you don't declare these rights, GitRoot will block the plugin from making any changes.
127
128When granted, GitRoot intercepts your push, lets the plugin inject the missing metadata, and safely generates a follow-up commit. And don't worry about your history: if you decide to `git rebase -i` later to squash or fixup your commits, plugins won't generate duplicate commits if no metadata has changed.
129
130What about security or external users trying to forge metadata? Since contributors cannot push directly to the [default branch](../doc/technicals/default_branch.md) (like `main`), they have to go through a branch and need to be merged. This means every single change to an issue's status or metadata is subjected to a code review.
131
132If an external user tries to close someone else's issue or manipulate IDs, it will stand out like a sore thumb in the git diff. Maintainers retain absolute control and can simply reject the rogue contribution. Security doesn't rely on complex database permission tables, but on the battle-tested git review workflow you already know and trust.
133
134So, once you push, the server-side plugin does its job, and the next time you run `git pull`, your local file will automatically look like this:
135
136```markdown
137---
138id: 1
139user: email@email.com
140status: OPEN
141---
142
143# My title
144
145My description where I can detail what my problem is.
146```
147
148Bingo! You get the exact same file, with the exact same metadata you wanted. Of course, a lot more needs to be added, like defining a `type: user` to assign an issue, or perhaps a `type: status` to define an enum of allowed values. But I think you see where I'm going with this feature!
149
150_(Note: If you're wondering about the barrier to entry for non-technical contributors, my long-term vision is to allow plugins to intercept standard HTTP requests. Tomorrow, a classic web form will be able to POST to GitRoot and automatically commit a `.md` file under the hood. But for now, we are proudly dev-centric!)_
151
152## So, What About the Label?
153
154Throughout all of this, youβll notice that the term "issue" only appears within the plugins themselves. Nowhere have we talked about that `π Issues` button yet. We actually need to introduce another plugin before we can tackle that topic.
155
156First, we need a file that lists all of these "issues". Thatβs exactly why the [silo plugin](../plugins/name/silo.md) exists. It inspects all files and groups them whenever it finds a matching pattern.
157
158To list all open "issues", we configure it like this:
159
160```yaml
161- name: silo
162 ...
163 run:
164 - path: "**/*"
165 branch:
166 - main
167 when:
168 - add
169 - mod
170 - del
171 func: []
172 write:
173 git:
174 - path: boards/*.md
175 can:
176 - add
177 - mod
178 - del
179 - append
180 web: []
181 exec: []
182 callfunc: []
183 configuration:
184 boards:
185 description: All open issues
186 for: issues/*.md
187 format: table
188 paginator: 50
189 selects:
190 - "status: (.*)"
191 - "user: (.*)"
192 sort: select[0]
193 sortOrder: desc
194 tableHeader: "| | status | user |"
195 title: All issues
196 to: boards/issues.md
197 where: "status: OPEN"
198```
199
200> You can read this configuration as: "For every file in the main branch, create a board called 'All issues' containing links to all files matching `status: OPEN`. Display them as a table with `status` and `user` columns."
201
202After running `git commit && git push && git pull`, you will get a new file at `boards/issues.md` containing something like this:
203
204```markdown
205# All issues
206
207All open issues
208
209| | status | user |
210| --------------------------------- | ------ | --------------- |
211| [My title](../issues/my_issue.md) | OPEN | email@email.com |
212```
213
214Silo has aggregated all your open issues into a single dashboard. Of course, you could easily create another board for closed issues in a separate file like `boards/closed.md`, it's entirely up to you.
215
216_(Note: If you are sweating at the thought of a plugin scanning your entire repository on every single push, don't panic. `silo`, thanks to the plugin-sdk API, doesn't do a full disk scan. It only processes the incremental diffs generated by the incoming commit. This is precisely why we restrict its scope to the `main` branch in this example.)_
217
218Now that we have a page representing all our issues, we can finally render it in the browser. Because, as you might have guessed, the label causing all this debate lives in the Web UI.
219
220## The Web is Where It Happens
221
222To render my `boards/issues.md` file on the web, I use the [apex](../plugins/name/apex.md) plugin, in conjunction with the [apex_markdown](../plugins/name/apex_markdown.md) plugin, of course.
223
224These plugins take files from git and render them directly in the Web UI. I configure them like this:
225
226```yaml
227- name: apex
228 ...
229 run:
230 - path: "**/*"
231 branch:
232 - main
233 when:
234 - add
235 - mod
236 - del
237 func: []
238 write:
239 git:
240 - path: "**/*"
241 can:
242 - add
243 - mod
244 - del
245 - append
246 web:
247 - path: "**/*"
248 can:
249 - add
250 - mod
251 - del
252 - append
253 exec: []
254 callfunc:
255 - pluginname: apex_markdown
256 funcname: renderMd
257 ...
258 configuration:
259 ...
260 menu:
261 - display: π Home
262 link: /
263 - display: π Documentation
264 link: /doc/
265 - display: π Versions
266 link: /CHANGELOG.html
267 - display: π§© Plugins
268 link: /plugins/
269 - display: π Issues
270 link: /boards/issues.html
271 - display: π Code
272 link: /worktree/
273 - display: β Blog
274 link: /blog/
275 - display: π¨ Contact
276 link: /contact.html
277```
278
279Oh, do you see it now? Yes, right there: `display: π Issues`. This is the exact configuration that tells Apex to add a global menu item with the `π Issues` label, pointing to the `/boards/issues.html` file.
280
281If you are wondering how Apex magically knows where to find your "issues", the secret is: it doesn't.
282
283Apex isn't a smart router, it's just a file translator. It blindly takes the Markdown structure generated by GitRoot and renders it into HTML. Because I know Apex transforms `boards/issues.md` into `boards/issues.html`, I simply hardcode that target URL in my menu configuration (`link: /boards/issues.html`).
284
285And what about the links inside the table? Apex has a clever built-in rewrite engine. When it parses your generated board file, it automatically rewrites every internal Markdown link, turning `[My title](../issues/my_issue.md)` into a standard web-friendly `<a href="../issues/my_issue.html">`. No database, no dynamic routing, just plain old static files mapped one-to-one.
286
287## Conclusion
288
289If you change the label to `π Tickets` or `π‘ Discussions`, the "issue to rename issues into something else" can finally be closed. Ideally, you might also want to rename some directories in the other plugins' configurations, but it's really not a big deal.
290
291I'm pretty sure you now understand why I didn't immediately jump on resolving this task: everything we just saw is pure configuration, completely separated from the GitRoot core code.
292
293And to be perfectly honest, I haven't changed it yet because none of the traditional options really appeal to me. I don't particularly like "Tickets" or "Discussions", maybe it should be "Feature Requests" instead (and yes, you can now model that in GitRoot too!).
294
295In fact, I personally call them `TODO`s, because in reality, they are all just waiting for me to get them done.
296
297### Wait... Why stop at issues?
298
299This is where the true power of GitRoot will blow your mind.
300
301Because GitRoot doesn't care about databases, and because everything is just a file inside Git, the `silo` plugin can aggregate anything. I don't need a separate `TODO` page for my tickets, because I already have one that automatically tracks the `//TODO` comments inside my actual source code.
302
303Yes, you read that right. You can visit it live at [https://gitroot.dev/boards/todos.html](../boards/todos.md).
304
305Every time I write a `//TODO` in a `.go`, `.rs` or `.ts` file, GitRoot parses it, aggregates it, and generates a dynamic dashboard. Tickets, documentation, source code comments... it's all the exact same data structure.
306
307To be clear, it doesnβt magically transform your source code comments into full-blown database-like issues with IDs and statuses. It simply extracts them to give you a centralized, living view of your technical debt, right alongside your official tickets. Itβs the exact same data aggregation pipeline, tailored for two different use cases.
308
309---
310
311> If you want to discover more plugins and configurations, feel free to browse through the [π§© Plugins section](../plugins/). However, for now, the best way to explore remains checking out the actual [GitRoot plugins.yml configuration file](../.gitroot/plugins.yml).