Issues from the command line¶
Everything you do to an issue on a forge, you can do with git board,
offline: create one, read it with its comments, comment, edit, set its
fields, close and reopen it. Every change is written to the board at once
and reaches the forges with the next git board sync. No command but a
sync touches the network, unless it says so (init, lint
--create-labels, forge status --check).
git board X and git-board X are the same command: the binary is a git
subcommand.
-C <dir> runs any command as if started in <dir>, as git -C does:
git-board -C ~/src/co2-calculator ls. git -C <dir> board ls is the
same.
Naming an issue¶
Every command taking <id> takes any of:
| Form | Example | Note |
|---|---|---|
| a prefix of its id | a1b2c3d |
7 characters or more, as ls shows them |
| its project number | CO2-3075 |
once the project numbers its issues (git board numbers) |
| a forge's number | "#2528" |
quoted: # starts a shell comment |
A prefix matching two issues is refused (exit code 3); give more of it.
Who you write as¶
Every change is signed with a person of the board. A fresh board has none chosen, and every write fails until you choose (exit code 4):
git board me # who you write as
git board me --new # create you from git user.name and user.email
git board me --new --name "Ada Lovelace" --email ada@example.com
git board me --ls # everyone on the board, * marks you
git board me 3f2a9c1 # write as someone already there (a forge sync made them)
--new refuses to create a second person with your name and email: it
names the one there, to choose with git board me <id>. The choice is
kept in the clone's git config (board.me).
| Flag | |
|---|---|
--new |
create a person and write as them |
--name, --email |
with --new: instead of git's user.name, user.email |
--ls |
list everyone on the board |
--json |
{"id", "name", "login", "avatar_url"}; with --ls an array, "me": true on you |
Texts: -b, -F, -e¶
new, comment and edit take a text the way gh does:
| Flag | |
|---|---|
-b, --body <text> |
the text itself |
-F, --body-file <file> |
read it from a file; - reads stdin |
-e, --editor |
write it in your editor: git's (GIT_EDITOR, core.editor, VISUAL, EDITOR) |
With -e, new and edit put the title on the first line and the
description below it. A text saved empty writes nothing. When a write is
refused after you wrote the text, the error names a file keeping it:
nothing you typed is lost.
new: create an issue¶
git board new "Login fails on Safari"
git board new "Login fails on Safari" prio=p1 assignee=+@me sprint=@current
git board new "Export to CSV" -b "As ls --md, but CSV."
git board new "Crash report" -F crash.md label=+bug
git board new -e prio=p2
git board new "Login fails" --template "Bug report"
git board new # on a terminal: asks
id=$(git board new "Follow-up")
The title is the first argument (or -t, --title), then field=value
assignments, read and checked as set reads them, written
in the same change as the issue. A first argument that reads as
field=value is an assignment: a title like a=b goes with --title.
It prints the new issue's id alone on its line. --json prints the issue
as ls --json prints each one. --force writes values the config
doesn't allow; --me is @me.
Templates¶
-T, --template <name> starts from one of the repository's issue
templates, by its name or its file's, without case:
| Where | Read |
|---|---|
.github/ISSUE_TEMPLATE/*.md |
the front matter's name, title, labels, assignees; the text below it |
.github/ISSUE_TEMPLATE/*.yml |
an issue form: name, title, labels, assignees, and a ### label heading per field, as GitHub writes the issue; markdown elements, dropdown choices and validations are left out |
.gitlab/issue_templates/*.md |
named by the file; /label ~x and /assign @x lines are labels and assignees, the rest the text |
The template's text is the description, unless -b or -F gives one.
Its labels and assignees (assignee:<login>) are set first, without
the config's check; the field=value arguments go over them. Its title
starts the title you are asked for, or the editor's first line; a title
you give wins. A name no template has is exit code 2, and the error
lists the names. config.yml is GitHub's chooser, not a template.
Prompts¶
With no title on a terminal, new asks. A template first, when the
repository has some; then the title, after the template's title; then
whether to write the description in your editor. Questions go to
stderr: the id still prints alone on stdout. -e asks nothing: the
editor holds the title.
It never asks when stdin is not a terminal, with --no-prompt, or when
GIT_BOARD_NO_PROMPT is set. No title is exit code 2 then, as in a
script.
show: an issue, with its comments¶
git board show a1b2c3d
git board show CO2-3075 --md
git board show "#2528" --json
git board show a1b2c3d --no-comments
git board show a1b2c3d --web # in the web UI
git board show a1b2c3d --forge # on GitHub or GitLab
Title; number, id, state, author, date and comment count; the fields, the
plain labels, the inherited ones and the forges; the description; then
each comment, headed by its author, date, (edited) and its ref
[abc1234:1], which comment --edit takes.
| Flag | |
|---|---|
--md |
as Markdown: a heading, a fields table, a section per comment |
--json |
the whole thread (below) |
--no-comments |
leave the comments out |
-w, --web |
open it in this repository's web UI |
--forge |
open it on its forge |
--at <when> |
as it was then (see The board as it was) |
--web opens the issue's page where the repository's board already
runs; with none running, it starts one here, as a bare git board does,
until Ctrl-C. --forge opens it on the first forge it is on; an issue on
no forge yet is exit code 3. Both print the URL.
get <id> stays what it was: the issue's labels, one per line, for
scripts.
comment: comment, or edit a comment¶
git board comment a1b2c3d -b "Fixed in 4e5f6a7"
git board comment a1b2c3d # the editor, as git commit
git log -1 --format=%B | git board comment a1b2c3d -F -
git board comment a1b2c3d --edit-last
git board comment a1b2c3d --edit 9c8b7a6:1 -b "Fixed in 4e5f6a7, in v1.2"
With no text flag the editor opens. --edit <ref> edits a comment by the
ref show prints (a prefix of its hash is enough when it names one
comment); --edit-last edits your last comment on the issue. The editor
opens on the comment's text.
An edit carries the text you started from. If someone changed the comment meanwhile, here or through a sync, your edit is refused with exit code 5 instead of replacing theirs, and your text is kept in a file the error names.
It prints the comment's ref; --json prints {"id", "ref", "issue"}.
The description is not a comment: edit changes it.
--delete <ref> deletes a comment: see Deleting.
delete: an issue or a comment¶
git board delete a1b2c3d # asks first
git board delete a1b2c3d --yes
git board comment a1b2c3d --delete 9c8b7a6:1 --yes
A deleted issue leaves every list, search, page, export and sync; its id answers not found (exit code 3). A deleted comment leaves its issue's page, its log and search.
A delete hides; it doesn't erase. The text stays in the board's history, in every clone that fetched it, and on the forges it was synced to: a forge keeps its copy. A secret pasted into an issue is leaked: revoke it. Deleting it keeps it from being shown, searched or synced again. A pull never brings a deleted issue or comment back. ADR 0019 says why.
On a terminal it asks first. --yes doesn't ask, and is needed when no
one is at a terminal (a script, CI): without it, exit code 2. Deleting is
in the command line only, not yet in the web UI.
undo: your last change¶
git board undo a1b2c3d # your last change to it
git board undo # your last change, on any issue
git board undo a1b2c3d --force # though someone wrote after you
It writes the inverse of your last change: a label added is removed and
one removed added back; a status, a title or a text goes back to what it
was; a new comment is deleted. A change is what one command wrote, so
edit -e's title and description go back together. Undoing twice redoes,
but for a new comment: its undo is a delete, and a delete is final (ADR
0019), so a second undo refuses
and says so; post the text again with git board comment. Nothing is
erased: the change and its undo are both in git board log.
Refused, exit code 2, with the reason:
- creating an issue:
git board deletedeletes it; - a delete: never undone, the one undoing a comment wrote too;
- a change of metadata (a review thread resolved, a Project field);
- someone else wrote to the issue after you, or a sync pulled into it:
--forceundoes yours anyway, over theirs.
Changes a sync pulled are not yours to undo here. --json prints
{"issue", "title", "undone", "what"}: the operations undone and a line
for each thing put back.
edit: title and description¶
git board edit a1b2c3d --title "Login fails on Safari 17"
git board edit a1b2c3d -b "Steps: ..."
git board edit a1b2c3d -e # title and description, one buffer
Refused, exit code 5, when the title or the description changed since
you read it; your text is kept. Every earlier version stays in the
issue's history (git board log).
set: fields¶
git board set a1b2c3d status=ready prio=p1
git board set a1b2c3d assignee=+@me # add yourself
git board set a1b2c3d assignee=-guilbep # remove
git board set a1b2c3d prio= # clear the field
git board set a1b2c3d label=+regression # a plain label
git board set a1b2c3d milestone=v1 --force # a field nothing configures yet
git board set --filter "sprint:@current status:ready no:assignee" assignee=+@me --apply
| Assignment | |
|---|---|
field=value |
a single field (status, prio) swaps to value; a multi field (assignee) adds it |
field=+value |
a multi field adds value (refused on a single field) |
field=-value |
removes value, from either |
field= |
clears the field: every value it holds goes |
label=+name, label=-name |
adds or removes the plain label name |
Values resolve first: @me (--me, GIT_BOARD_ME, else who you write
as: their forge login, else their name, so assignee=+@me works on a
board that never met a forge), @current, @next, @previous
(sprints), and a configured alias.
Every assignment is read before anything is written. One that is
malformed (=x, status=-, a space around the value) is refused. One the
config doesn't allow is refused too: a value outside the field's
options, not a number for a number field, not a YYYY-MM-DD date for a
date field, not a sprint for a sprint field, a plain label outside
[labels] when the project lists them, a field neither configured nor on
any issue. Nothing is written then, and the exit code is 2. --force
writes it anyway. A removal or a clear is never refused: it is how a
wrong value goes.
All the assignments land in one change. Setting status to its last
option closes the issue, as close does; another option reopens a closed
one: the state and the status never disagree.
--filter <filter> sets the fields on every issue the filter matches
(ls's grammar). It prints what each issue would get and writes nothing
until --apply.
labels: what is in use¶
git board labels # every label, with counts
git board labels --plain --json
git board labels rename regresion regression
git board labels rename prio:p0 prio:p1
labels lists every label on the board's issues, with how many open and
closed issues have it: the plain labels first, then each field's values,
by field. Deleted issues don't count. --json prints [{"label",
"field", "value", "open", "closed"}].
labels rename <old> <new> renames a label on every issue that has it,
one change each. A field's value is a label too. It doesn't check the new
name against .git-board.toml: git board lint does.
init: a new board's file¶
git board init # the global file's template, else the default
git board init --template enac # a built-in template, a file or an http(s) URL
init writes .git-board.toml at the repository's root, and never
overwrites one. When origin is on GitHub, the file starts as its labels
and its first Project's fields (names, colors, descriptions, each
field's values in order), written over the template, which fills what
the forge lacks: A new board.
On GitLab, its labels: GitLab has no Projects. It says what it read:
from EPFL-ENAC/test: 12 labels, Project EPFL-ENAC/53, ENAC-IT4R Template
wrote /home/me/src/test/.git-board.toml: edit it in Settings › Project file (git board), or by hand
and a line for each thing it left to the template: a Project field kept in labels, a label whose field lives in the Project, a second Project. The forge is read with the token a sync uses: an environment variable, else git's credential helper (ADR 0010). No token, or nothing read: the template, and a line saying what wasn't read.
state: the board's state, and what to do next¶
git board state # each part's state in a word, then the next actions
git board state --json # the same, with each action's command and route
git board state --jq '.next[0].cli' # the command to run first
state says the state of each part of the board, and nothing goes to
the network:
board new
file none (/home/me/src/x/.git-board.toml)
me none
datastore git
forge github: first (github o/r, both, found in the git remotes)
project none
run idle
issues 0
next:
Choose who you are: until then the board is read-only.
git board me --new
Run the first sync with github (o/r). It creates issues there only with your consent.
git board sync --check
The words are these. board is new (no identity, or no issue and no
forge synced yet) or ready. file is none (the defaults hold),
current, older (config upgrade moves it) or newer. forge is
off, no-token, first, on (a GitLab project the remotes name by
its path: the first sync says) or synced. run is idle, running
(in this process or another) or stopped: a run stopped before it went
through, and the next sync resumes it. The web app's GET /api/state
answers the same.
config: settings¶
git board config list # the file's keys, then this clone's
git board config get views.backlog.filter
git board config set views.backlog.filter "is:open prio:p1"
git board config set sprints.length_days 14
git board config unset github-project.write
git board config set board.avatars gravatar # this clone's git config
git board config schema # the file's schema version, and git-board's
git board config upgrade --dry-run # the diff an upgrade writes
git board config downgrade --to 1 # for an older git-board
A dotted key, like github-project.owner or views.backlog.filter, is
in .git-board.toml, which the team shares and commits. set changes
that key's line alone: every other line and comment stays. It refuses
(exit code 2) a key git-board doesn't read, and a value that leaves the
file broken. A value is TOML when it is one (true, 14, ["a", "b"]),
else a string: quote '"14"' for a string of digits. A key inside an
array of tables ([[sprints.list]]) is edited in the file.
A key board.<name> is this clone's own, in git config:
board.avatars, board.datastore, board.experimental, and board.me,
which git board me sets. set writes it with --local; get reads it
at every level git does. A datastore URL is printed without its password.
get of a key not set exits with code 3.
schema, upgrade and downgrade (v0.12) move the file between the
versions of its format: The file's schema version.
set and unset write an older file upgraded, and say so.
forge status, forge login: tokens¶
git board forge status # each forge, and whether a token is found
git board forge status --check # and whose each token is, asking the forge
git board forge login github # in the browser on github.com, else a hidden prompt
git board forge login github --paste # a hidden prompt
gh auth token | git board forge login github --login guilbep
forge status lists the forges (those of .git-board.toml, else those
found in the git remotes) and, for each, whether a token is found for its
posting account and for your own account on that host, and where: an
environment variable or git's credential helper. No token is printed.
It doesn't touch the network, unless --check. --json prints it.
forge login <forge> stores a token in git's credential helper (git
credential approve), as Settings › Sync does: git-board keeps no token
of its own (ADR 0010).
The token is read at a hidden prompt on a terminal, else from the first
line of stdin; never from the command line. It is for --login, by
default your own account on that host, else the forge's posting account.
On github.com, in a build with git-board's OAuth App, a terminal logs
in in the browser instead. The command prints an address and a code.
Type the code at that address and approve. The token GitHub gives is
stored for the login GitHub says it is. --paste, --login or a token
on stdin keep the prompt.
forge backup <url> is the one command to a backup: it adds the forge
with way = "push" — the issues go there, and nothing comes back — and
stores its token the same way
(The way of a mirror):
close, reopen, move¶
git board close a1b2c3d -c "Fixed in 4e5f6a7"
git board close a1b2c3d --reason not-planned -c "Out of scope"
git board close a1b2c3d --duplicate-of "#2518"
git board reopen a1b2c3d -c "Still fails on Safari"
git board move a1b2c3d in-progress
git board move a1b2c3d --next
git board move a1b2c3d --prev
close sets the status to its last option and closes the issue.
reopen opens it and, when its status is that last option or none, sets
the first option; another status stays. Both take -c, --comment, added
first.
close -r, --reason says why it closed, as GitHub does: completed
(the default), not-planned or duplicate. The reason is a field like
any other, the label closed-as:not-planned, so ls
"closed-as:not-planned" finds them; completed writes none.
--duplicate-of <id> closes as duplicate, sets the issue field
duplicate-of to the other issue (its forge number #2518 when both
are on that forge, else its id), and comments Duplicate of #2518,
which GitHub reads as well. reopen takes closed-as away and leaves
duplicate-of. The forges get closed-as as a label, not as their own
close reason.
move <id> <status> is set <id> status=<status>, checked the
same way. move --next and --prev step the status through its
options. Onto the last one the issue closes; back from it, it reopens.
branch: the issue's branch¶
git board branch a1b2c3d # the name, and the command
git board branch a1b2c3d --create # create it from the base, switch to it
The name comes from [branches] in .git-board.toml: by default the
issue's folder, then the branch's place in it and the title,
2518/1-login-fails-on-safari. branch prints it and the git switch
command creating it from the base, and creates nothing. --create runs
that command. A second --create makes the issue's next branch,
2518/2-….
log: everything that happened to an issue¶
git board log a1b2c3d
git board log a1b2c3d --field status # its path through the workflow
git board log CO2-3075 --since 7d
git board log a1b2c3d --json
The board keeps every operation, with who made it and when: the creation, each comment, each edit of the description or a comment (with the text), each title, state and label change, in the order they were made. A forge shows label events at best, and keeps no earlier text.
| Flag | |
|---|---|
--field <name> |
only that field's changes: who set prio:p0, when, what it was before |
--since <when> |
from a date (2026-10-01) or a while ago (36h, 7d) |
--json |
the entries (below) |
history is the same command.
status, follow, unfollow: what needs you¶
git board status # assigned to you, then your inbox
git board status --mark-read # and mark that inbox read
git board status --json
git board follow a1b2c3d # every change reaches your inbox
git board unfollow a1b2c3d # only what is addressed to you
git board follow a1b2c3d --auto # back to automatic
status prints three parts. The open issues assigned to you. Then your
inbox, as the web UI's Inbox has it: what someone else changed since you
last looked, in the last 14 days. It is split in two: the issues you
opened, and the others you follow or are mentioned in. Each line says
what is new (2 comments, closed, mentioned you), by whom, and when.
Nothing is marked read: the Inbox still shows the same. --mark-read
marks what status printed read. --json prints {"me", "assigned",
"opened", "follows"}: assigned holds issues, the others inbox rows
(below).
You follow an issue automatically while you are in it: you opened it,
are assigned, commented, are mentioned, or hold a role. follow and
unfollow override that, as the web UI's bell does; follow --auto
goes back to it. Each prints whether you follow the issue now, and why;
--json prints {"following", "reasons", "set"}. Your follows stay in
this clone, as the web UI's do. ls "follows:@me" lists what you
follow.
The board as it was¶
git board show a1b2c3d --at 2026-10-01 # the issue at the end of that day
git board ls "sprint:@current" --at 2026-10-01 # the board that day
git board diff a1b2c3d --since 2026-10-01 # what changed since
git board diff CO2-3075 --since 7d --json
The board keeps every operation with its time, so it can show what it
was. show --at and ls --at fold only the operations made by then.
- Each operation counts at its own time, its writer's clock, to the second. The log still applies them in its own order (by clock), so the past is what the people writing saw. When clocks disagree, an edit of a comment made "before" the comment waits for it.
<when>is a date, a time (2026-10-01T14:30, or with seconds, or RFC 3339 with a zone), or a while ago (36h,7d,2w). For--at, a date or a minute means its end:--at 2026-10-01is that evening.ls --atlists the issues created by then, each as it was.@todayand@currentmean that day and that day's sprint. Free text looks in the texts as they were. Who you are, what you follow and who is mentioned are as now.- An issue not created yet is not found (exit code 3).
- What waits for a forge isn't marked: that is now's.
diff <id> --since <when> compares the issue before --since (a date
from its start) with now: the title, the state, the labels added and
removed, the description as a line diff (- removed, + added), the
comments written, the comments edited. An issue created since says so,
and shows everything as new. --json prints:
| Field | |
|---|---|
id, since |
the issue, and from when |
created |
true when the issue is newer than since |
title, status, description |
{"was", "now"} when they changed; description with its diff |
added, removed |
labels |
comments |
the comments written since: {"id", "author", "at", "body"} |
edited_comments |
the comments edited since, with was, their text before |
ls and view¶
git board ls "assignee:@me is:open" --sort "prio asc"
git board ls --sort "updated desc" --limit 10
git board ls "login" --json
git board view backlog --md
ls [filter] lists the issues a filter matches; the grammar is in
Configuration › Filters: fields, people,
words (in:title), dates (created:>=@today-7d, closed:2026-10-01..2026-10-31)
and counts (comments:>5). --at <when> lists the board as it was. --sort takes created,
updated, title or a field, then asc or desc; any other key or
direction is refused (exit code 2). -L, --limit keeps the first N after
sorting. --group <field> groups, --md renders as Markdown, and
--json, --jq and --template as JSON. view
<name> runs a view from .git-board.toml.
insights: flow, throughput, cycle time¶
git board insights # the last 90 days, by status
git board insights --days 30 --field prio
git board insights --view backlog --json
What the web UI's Insights page shows, as text: the headlines, the open
issues by the field's value today and --days ago, the issues opened and
closed each week (the last --weeks, 8), the cycle time's median and
85th percentile, and the current sprint's burn-up. --view takes a
view's issues alone, open and closed. --json prints what the page reads
(GET /api/insights).
sync: git, the forges, the GitHub Project¶
git board sync [path...] runs the whole sync on each path, as Settings ›
Sync does: a git pull from the remote, the forge sync of every forge whose
sync is on, the GitHub Project's fields when [github-project] names one,
then a git push, which numbers the issues origin has. A failed pull stops
the steps after it; a failed forge or Project step is told, and the push
still runs. --check runs the same steps writing nothing, and ends with
a count of each part. A check goes on past a failed fetch. --only <parts> runs some parts of the forges'
sync and leaves the others for the next one: pull-changes (the forge
issues that changed), pull-new (the forge issues new here), create
(the issues to create on a forge) and send (the changes to send). The
Project pulls with a pull part, and is written to with send. A
release is published with send. --git syncs with git alone, a pull then a push. With no path it syncs the current
directory; it goes on past a path that fails and reports every failure at
the end. Each step has its own commands: fetch, pull and push (the
git remote), forge pull|push|sync and project pull|push|sync
(Forges).
git board sync --check
git board sync --only pull-changes,pull-new # push nothing to the forges
git board sync
git board sync --git ../co2-calculator ../git-time
project: the GitHub Project¶
git board project keeps a GitHub Project's field values and the
ns:value labels in step, on the issues a sync already linked to their
GitHub issue: pull takes the Project's changes into the labels, push
writes git-board's to the Project (with write = true), sync does both,
and is the third step of git board sync. Each takes --check. A field
is decided against the value both sides last agreed on: only one side
changed, the other follows; both changed, it is a conflict, left alone
until resolve takes a side. Assignees merge person by person. The
fields, the table and the token: Configuration › GitHub
Project.
git board project configure --owner EPFL-ENAC --project 40
git board project sync --check
git board project sync
git board project resolve 2605 status --theirs # or --ours
git board project fit is for a team whose Project came first. It checks
the Project and says what .git-board.toml lacks for it: a status the
Project has and the file doesn't, the kind a Category names, a sprint's
dates. It writes nothing without --apply.
git board project fit # the diff, and the questions
git board project fit --apply # write what needs no answer
git board project fit --apply --map 'status:QA Validation=qa' --map category:TASK=feat
git board project fit --apply --add-kind category:DATA --unmapped category:SPECS
A change git-board derives with no guess is in the diff. Another is a
question, printed with the flag that settles it: --map <field>:<the
Project's name>=<value>, --add-kind category:<name> (the Category is a
new kind), or --unmapped <field>:<name> (the sync skips that value, and
warns of nothing). With a terminal, --apply asks each open question. A
question left open writes nothing. A change only adds: no option, kind,
sprint or comment of the file is removed, and the order stays. If the
file changed after the check, nothing is written. While the file lacks
something, project sync --check and sync --check end with config: 12
changes clear 82 warnings: git board project fit.
lint: drift between the issues and the file¶
git board lint is read-only: it reports, and writes nothing. It checks
the open issues against .git-board.toml: keys git-board doesn't read
(unknown-key), two values of a single field (double-single), a value
outside a field's options (unknown-value, with the option meant when
only case or punctuation differs), two spellings of one value on a field
without options (near-value), a single field with no value
(missing-value), a sprint: label no sprint has (orphan-sprint), an
option no issue carries, closed ones included (dead-option), with
[branches] kinds an issue with two kinds, a title naming another kind
or none at all (two-kinds, kind-title, no-kind), with [labels] a
plain label outside the list (label-outside), and the labels' and issue
templates' own problems. --strict exits 1
when there are any, for CI.
git board lint --create-labels # what each forge lacks, writing nothing
git board lint --create-labels --apply # make and update those labels there
--create-labels checks something else: that every label the file
implies exists on each forge, so the forge's issues show and filter it
before the first sync. Those are a label per option of each
[fields.<name>], a sprint:<slug> per [[sprints.list]] entry (a
flat kind as its plain label) and [labels] list's, each with the color
the board shows and the file's description. The forges are [repos]
list's GitHub repositories and the [forges] tables, GitHub's or
GitLab's; with neither, the forge origin names. A label whose field
lives elsewhere on a forge (a GitHub Project, the assignees, a
milestone, to = "none") isn't made there. Each forge goes through its
adapter with the forge sync's token, the table's posting account's
(GITHUB_TOKEN, GITLAB_TOKEN, else git's credential helper:
Forges). A line per label:
EPFL-ENAC/co2-calculator: missing status:ready #00897f
EPFL-ENAC/co2-calculator: update prio:p0: color d73a4a → e00000
o/other: all labels present
dry run — nothing written. Re-run with --apply to create and update these labels.
A missing label is made with its color and description; one the forge
has is updated only where .git-board.toml sets its color or
description, so an organization's labels keep their colors. --apply
writes, and says created and updated. It also reports the labels an
issue template (.github/ISSUE_TEMPLATE) applies that origin's GitHub
repository doesn't have, which GitHub drops without a word; --apply
doesn't make those (the fix is in the template), and a template check
that can't run only warns. A forge that can't be read or written is
told on its line, the others go on, and the run exits 1. --apply
without --create-labels is an error.
sprint: the configured sprints¶
git board sprint ls # every sprint, with its computed dates
git board sprint current # the sprint @current resolves to (next, previous)
git board sprint new q1-hardening --title "Q1 Hardening" --start 2026-10-05
git board sprint report delivery # its issues, open and closed, counted by status
A sprint's end is the next one's start, or start + [sprints] length_days
for the last (Sprints). No sprint resolving is
not an error: current says so and exits 0. new adds a
[[sprints.list]] entry, keeping the file's comments and order, and
refuses a slug already used or a date that isn't YYYY-MM-DD.
tui: the terminal browser¶
| Key | Does |
|---|---|
↑/↓, / |
scroll, and a fuzzy search of titles and labels |
enter |
the issue's fields |
f |
a filter, in the grammar of ls |
s |
a sort, as ls --sort (prio desc) |
c |
a quick filter on the cat field's options, when the file has one |
i, then o |
preview git board project pull, then apply it, decided again from the issues as they are then |
e |
in an issue: edit a field, from its options or as text (+value, -value, as set) |
esc, q |
back; q or ctrl+c on the list quits |
The TUI shows an issue's fields; its description and comments are the
web app's and show's.
JSON output¶
git board ls --json # every field
git board ls --json id,title,labels # only these
git board ls "assignee:@me" --jq '.[].title'
git board ls --template '{{range .}}{{.title}} ({{timeago .updated_at}}){{"\n"}}{{end}}'
git board show a1b2c3d --jq '.events[] | select(.kind == "comment") | .body'
Every command that prints JSON takes the same --json: ls, view,
show, log, diff, status, me, new, comment, follow,
unfollow, undo, labels, insights, config list, forge status.
Alone, it prints every field. With a list, --json id,title, it keeps
those: in a list, of each item. A field an item lacks is null. A name
the command doesn't print is refused (exit code 2), with the list of
those it does, before anything is written. --json=id,title says the
same; the word after --json is taken as fields when it has a comma or
is one of them, so ls --json title prints titles and ls title --json
searches for "title".
--jq <expression> filters the JSON with jq (gojq, built in: no jq to
install). A string prints as it is, anything else as JSON on one line.
--template <template> prints it through a Go template
(text/template), with gh's helpers:
join <sep> <list>, pluck <field> <list>, timeago <time>, timefmt
<layout> <time> (a Go layout, 2006-01-02), truncate <n> <text>. Each
implies --json, and goes with its fields: --json id,title --jq ....
A wrong expression or template is refused (exit code 2). new has no
--template: it is kept for an issue template, as gh's.
The field names below are stable: new ones may come, these keep their names and meanings. Times are RFC 3339.
An issue (ls --json, view --json, new --json, and the top of
show --json):
| Field | |
|---|---|
id |
the whole id, 40 hex characters |
title |
as stored; shown_title without a trailing (#n) of its own forge number |
status |
open or closed |
labels |
its own labels: status:ready, assignee:ada, bug |
inherited |
labels it takes from its project's roles (when any) |
number |
its project number, CO2-3075 (when numbered) |
forges |
[{"name", "url", "number"}], where it is on forges (when any) |
github_url |
its GitHub issue (when any) |
created_at, updated_at |
|
closed_at |
when it was last closed (closed issues only) |
comments |
how many comments it has |
A thread (show --json) is the issue plus author (a person),
edited_at, participants (people) and events. Each event has kind
(created, comment, labels, status, title), id, author and
at; then as its kind has them body, edited and history (the
text's versions, [{"id", "author", "at", "body"}]), added and
removed (labels), status, title and was, meta. A person is
{"id", "name", "login", "avatar_url"}.
A log entry (log --json): at, author, kind (created,
comment, edit, title, state, labels), id; then target (an
edit's text: the created entry's id for the description), title and
was, state (open, closed), added and removed, body (the text
written).
An inbox row (status --json): issue (an issue), reasons (why
you follow it: author, assignee, followed, view: <name>; none when
it is only addressed to you), news (2 comments, closed, mentioned
you, added you as reviewer), who (people), latest and first (the
oldest new event's id).
Shell completion¶
git board completion bash > ~/.local/share/bash-completion/completions/git-board
git board completion zsh > "${fpath[1]}/_git-board"
git board completion fish > ~/.config/fish/completions/git-board.fish
git board completion <shell> prints the script for bash, zsh, fish or
PowerShell; git board completion <shell> --help says where it goes. It
completes the git-board form: git board ... goes through git's own
completion, which doesn't know this command's words. Tab then completes:
| Where | What |
|---|---|
<id> (show, comment, edit, set, close, reopen, move, log, branch, get) |
the issues by short id or project number, each with its title |
set and new's field=value |
the fields, then their options; @current, @next and the sprints for a sprint; @me for a person; else the values on the issues. After + or - too |
move, after the id |
the status options |
a filter (ls, tui, set --filter) |
the qualifiers, is: and the fields, then their values, for the filter's last word |
view |
the views |
me |
the people on the board |
--group, --sort, --field, --json= |
the fields |
A completion reads the board and .git-board.toml, writes nothing, and
offers nothing when it can't read them.
Logs¶
The server behind the web app (git board, git board serve) writes its
logs to stderr: one line an event, its start, each request, what went
wrong (the HTTP API). v0.11 and before wrote a
few lines with no level.
git board serve --log-level debug # the queries, each change event
git board serve --log-format json 2> board.log # one JSON object a line
GIT_BOARD_LOG_LEVEL=warn git board # only what went wrong
| Level | What |
|---|---|
error |
a request that failed on the server's side, with its code; a sync that failed |
warn |
a request refused (another site's page, a name that isn't this machine's); a request slower than a second; a sync's warnings; an issue that can't be read |
info (the default) |
the start (version, board, datastore, address); a line a request (method, path, status, duration, bytes); a sync's start and end |
debug |
a request's query, its secrets redacted; each change event |
--log-level and --log-format (text, the default, or json) win
over GIT_BOARD_LOG_LEVEL and GIT_BOARD_LOG_FORMAT. A line never holds
a token, a cookie, a header or a request's body.
Exit codes¶
| Code | |
|---|---|
| 0 | done; an empty ls too |
| 1 | an error not below |
| 2 | a wrong command line: a flag, an argument, an assignment malformed or not allowed by the config; a config key git-board doesn't read; an undo refused; a delete with no terminal to ask and no --yes |
| 3 | no such issue, comment or person, or a prefix naming several |
| 4 | no one chosen to write as: git board me |
| 5 | someone changed the text meanwhile: your edit is refused, your text kept |
Errors go to stderr, prefixed error:.
Coming from gh / glab¶
| gh | glab | git board |
|---|---|---|
gh auth status |
glab auth status |
git board me (who you write as), git board forge status (the forges' tokens) |
gh auth login --with-token |
glab auth login --stdin |
git board forge login <forge> (into git's credential helper) |
gh issue create -t T -b B -l bug -a @me |
glab issue create -t T -d B -l bug -a @me |
git board new T -b B label=+bug assignee=+@me |
gh issue create -e |
glab issue create |
git board new -e |
gh issue create (asks) |
glab issue create (asks) |
git board new (asks on a terminal; --no-prompt) |
gh issue create -T name |
glab issue create --template name |
git board new -T name |
gh issue list -a @me -l bug -S text |
glab issue list -a @me -l bug --search text |
git board ls "assignee:@me label:bug text" |
gh issue list -L 10 --json … |
glab issue list -P 10 -O json |
git board ls -L 10 --json |
gh issue list --json number,title |
git board ls --json number,title |
|
gh issue list --json title --jq '.[].title' |
git board ls --jq '.[].title' |
|
gh issue list --json title -t '{{range .}}…' |
git board ls --template '{{range .}}…' |
|
gh issue list -S "created:>2026-10-01 comments:>5" |
git board ls "created:>2026-10-01 comments:>5" |
|
gh issue list -S "crash in:title" |
git board ls "crash in:title" |
|
gh issue view N -c |
glab issue view N -c |
git board show N |
gh issue view N --json … |
glab issue view N -F json |
git board show N --json |
gh issue view N -w |
glab issue view N -w |
git board show N -w (the web UI), show N --forge (the forge) |
gh issue comment N -b B |
glab issue note N -m B |
git board comment N -b B |
gh issue comment N --edit-last |
git board comment N --edit-last |
|
gh issue edit N -t T -b B |
glab issue update N -t T -d B |
git board edit N --title T -b B |
gh issue edit N --add-label x --remove-label y |
glab issue update N -l x -u y |
git board set N label=+x label=-y |
gh issue edit N --add-assignee @me |
glab issue update N -a @me |
git board set N assignee=+@me |
gh issue edit N -m v1 |
glab issue update N -m v1 |
git board set N milestone=v1 (a field), or sprint=@next |
gh issue edit 1 2 3 --add-label x |
git board set --filter "<filter>" label=+x --apply |
|
gh issue close N -c C |
glab issue close N |
git board close N -c C |
gh issue close N -r "not planned", --duplicate-of M |
git board close N -r not-planned, --duplicate-of M |
|
gh issue reopen N |
glab issue reopen N |
git board reopen N |
gh status, gh issue status |
git board status, or git board ls "involves:@me is:open" |
|
glab issue subscribe N, unsubscribe N |
git board follow N, git board unfollow N |
|
gh issue develop N -c |
glab mr create --related-issue N |
git board branch N --create (branch N prints the branch and the command) |
glab issue view N -s (system notes) |
git board log N (every change, text edits too) |
|
git board show N --at 2026-10-01, git board ls --at 2026-10-01 (the board as it was) |
||
git board diff N --since 7d (what changed: fields, a text diff, new comments) |
||
gh issue delete N --yes |
glab issue delete N |
git board delete N --yes (hidden, kept in history) |
gh issue comment N --delete-last |
glab issue note delete |
git board comment N --delete <ref> |
git board undo N (your last change, inverted) |
||
gh -R owner/repo issue list |
glab -R owner/repo issue list |
git board -C <dir> ls (a clone: the board is in it) |
gh config get/set |
glab config get/set |
git board config get/set/list |
gh label list |
glab label list |
git board labels (with counts) |
gh label edit old --name new |
glab label edit |
git board labels rename old new (on every issue) |
glab issue board view (counts) |
git board insights (flow, throughput, cycle time, burn-up) |
|
gh project item-edit … |
glab issue board … |
git board move N in-progress, move N --next, git board project |
gh completion -s zsh |
glab completion -s zsh |
git board completion zsh |
N is any name of an issue. Not there yet: pinning,
locking (no permissions in a board kept in git), transfer between
boards.