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).