Where a value lives¶
The board is the one truth. Each forge and the GitHub Project is a mirror of it: a copy of some of its values, on GitHub or GitLab. Every write a mirror accepts ends in a recorded agreement, the value both sides last held. This page says where each value lives, and what happens when you switch a mirror on, off, or round. Each case names the test that proves it. Sync gives the run's rules, Forges what each forge needs, Datastores where the board is kept.
The board and its mirrors¶
the board: issues, people, fields as ns:value
labels, in one datastore (board.datastore)
|
┌────────────────┼────────────────┐
a GitHub forge a GitLab forge the GitHub Project
its issues its issues its columns
- The board's value is the truth. A field's value is a label in the
board (
status:ready, ADR 0004). A pull writes a mirror's value into the board, and from then on the board holds it as its own. A mirror can be where a change starts; it is never the truth. - The board is your clone's. Two clones meet through git (origin, a fetch, a push), not through a forge. Three repositories that share one forge have three boards; the forge is where they meet.
- The datastores are homes, not levels. git, PostgreSQL and Markdown hold the same board; a clone has one. The forge-only datastore is the exception: a board kept only as the forges' copy, with no store of its own (ADR 0002, section 4).
- The mirrors are peers, not levels. A forge and the Project each
compare the board with themselves, never with each other. GitLab has
no Project, so a field the Project holds on GitHub is a label on
GitLab. One
git board syncruns git pull, the forges, the Project, then git push.
The three decisions¶
Where a value is follows from three decisions, and from nothing else.
| Decision | Where it is written | When it is not written |
|---|---|---|
| Where the board is kept | board.datastore, in the clone's git config (it can hold a password, so never in .git-board.toml) |
git |
| Which forges, and which way | [forges.<name>] tables in .git-board.toml, with way = "push", "pull" or "both" |
the projects origin names, both ways |
| Where each field lives | [forges.<name>.fields.<field>] mappings, and [github-project] fields for the Project |
the field type's default on that forge; for the Project, the default seven |
- Which forges. With no table, the forges are the projects
originnames. Writing one table turns that off. A forge found this way never starts creating issues silently: a first sync with a forge, or 50 creations or more, waits for consent (--allow-create, the Sync page's checkbox). - Which way.
bothpulls and pushes.pullmakes a read-only mirror: the run reads it, and writes nothing there.pushmakes a backup: the run writes to it, and pulls nothing; an edit made there is put back to the board's value at the next run, and counted. - Where a field lives. On a forge, a field's home is its mapping's
to, else its type's default: a label, the due date on GitLab, the assignees, sub-issues on GitHub, the milestone. On GitHub with a Project, the fields[github-project] fieldsnames live in the Project's columns, not in labels.project configureand the Project page write that list with the Project. A table without it holds the default seven (status,prio,mod,est,sprint,moscow,milestone), and the Project page says the file does not state them yet. Settings › Fields shows the resolved home of each field on each forge.
The agreement, and the two conflict rules¶
For each field, a run compares three values: the board's, the mirror's,
and the agreement. On a forge, the agreement is a seen record in the
forge's sync entity (Last seen); on the Project, it
is the issue's git-board:<owner>/<number>:<field> metadata.
- Neither side changed: nothing moves.
- One side changed: that side's value goes to the other.
- Both changed, differently, on a forge: the later change wins, the forge's at the same second. A forge's change is already public.
- Both changed, differently, on the Project: the field is held on
both sides, named on the Conflicts page, until a person keeps one side
(
git board project resolve, or the page's buttons). The Project never wins by itself. - Labels are sets, compared label by label, so two sides that add different labels both keep theirs.
- An accepted write is recorded at once, so a run that stops loses nothing and never writes the same change twice (Crash recovery).
The reversibility guarantees¶
- An agreement survives its mirror. Turn a forge or the Project off, and its records stay. Turn it on again, and a sync moves only what changed since.
- A held label comes back. When a field stops living in the Project, it flows to GitHub as a label again, and the board's own change wins over a stale copy.
- A home moves only when you ask. Changing a field's home moves no
value by itself.
git board sync --migrate <field>says what it would set and remove, then does it. - A Project conflict is never won by itself. It waits for a person.
Switching, case by case¶
| You do | What moves | What stays | Proved by |
|---|---|---|---|
| Add a GitHub Project | its fields leave GitHub's labels and live in its columns; a pull fills the board from the columns | the board's labels; GitHub's old copies, neither pulled nor removed | TestConfigureProject, TestHeldLabels, TestSyncFirstRunImports |
Hold fewer fields in the Project (fields = ["status", "prio"]) |
the others are labels on GitHub again | the Project's records of every field | TestConfigureProject |
| Remove the Project | its fields flow to GitHub as labels again; the board's change since wins over a stale copy | the Project's records, for when it comes back | TestHeldAfterSeen |
| The board and the Project change one field differently | nothing: the field is held on both sides | both values, on the Conflicts page, until you keep one | TestSyncConflictIsHeld, TestResolveKeepsOneSide, TestProjectRun |
| The board and a forge change one field differently | the later change; the forge's at the same second | the agreement, updated | TestPullKeepsLocalChange |
| Move a field's home on a forge (a label to the assignees, say) | nothing, until the migration runs; then the old labels go | the old target, read until then | TestMigrateField |
Make a forge a backup (way = "push") |
from then on, only the board's values go there; an edit made there is put back | its records | TestWayPushOnly, TestWayBackupPutBack, TestWayBackupNeverFeedsTheBoard |
| Make a backup both ways again | what is edited there from then on is pulled; no issue is created twice | its records, so it is not a first sync | TestWayBackupBecomesBoth |
Make a forge read-only (way = "pull") |
its issues are pulled; nothing is written there | its records | TestWayPullOnly, TestWayRuns |
| Keep the board in Markdown, from git | Copy, then Keep the board here: every issue, person and record, ids kept | the board in git, untouched | TestDatastoreCopy, TestDatastoreChoose |
| Keep the board in git again, from Markdown | the key is unset, and the clone reads git | the Markdown folder; what was written only there is not copied back | TestDatastoreChoose; the copy back is not proven |
The use cases¶
| You want | The config | What happens |
|---|---|---|
1. A backup of the issues in github.com/org/repo |
[forges.backup] with way = "push", or Settings › Sync's way select |
the run writes there, pulls nothing, and puts back what is edited there |
2. Code in org/x, issues in org/issue-repo |
a [forges.issues] table naming org/issue-repo |
works: a table names any project |
3. Repositories x, y, z sharing one issue repository |
each repository's table names org/issue-repo |
works: each board links to the same forge issues; between two boards, the later change wins |
4. x, y, z sharing one GitHub Project |
each [github-project] names the same Project |
works per issue: each board bridges its own repository's items |
| 5. A GitHub Project into a PostgreSQL board | board.datastore = postgres://…, [github-project] set |
works: the bridge does not depend on the datastore |
| 6. Code on GitHub, issues on GitLab | origin on GitHub, a [forges.gl] table naming the GitLab project |
works; the table also stops origin being found as a forge. way = "pull" makes GitLab read-only |
| 7. Issues in PostgreSQL or S3, not in git | board.datastore = postgres://… |
PostgreSQL works. S3 is not built: a store would implement the Datastore interface (heads, load, add, versions) |