Version: 1
Status: Draft
Author: Emerson Soares remenoscodes@gmail.com
Date: 2025-04-23
Minimum Git Version: 2.17 (April 2018) — required for
%(trailers:key=...,valueonly) format support.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
This document specifies a format for storing issue tracking data natively in Git repositories using only Git's existing object model: commits, refs, and trailers. The format requires no external database, no custom binary format, and no tools beyond Git's standard plumbing commands.
Issues are stored as chains of commits under the refs/issues/ namespace.
Each commit's subject line, body, and trailers carry structured issue data.
The format is designed for distributed operation with deterministic
conflict resolution, enabling issues to travel with the repository across
hosting providers via standard git push and git fetch.
-
Git-native: Use only Git's existing primitives (commits, refs, trailers, trees). No custom binary formats, no JSON, no external databases.
-
Distributed-first: Any operation that works locally must also work correctly when repositories are merged from multiple clones with no central coordinator.
-
Tooling-friendly: Issue metadata must be queryable using standard Git plumbing commands (
git for-each-ref,git log,git interpret-trailers) without spawning additional processes. -
Simple over clever: Prefer straightforward data models over mathematically optimal but complex ones. Three-way set merge over CRDTs. Last-writer-wins over Lamport clocks.
-
Format over tool: This specification defines a data format, not a tool. Any implementation that produces conforming refs and commits is a valid implementation.
The following features are explicitly OUT OF SCOPE for this format:
-
Access Control: The format does not define authorization models. Any user with write access to the repository can create, modify, or close issues. Access control must be enforced at the Git repository level (filesystem permissions, server-side hooks, hosting platform controls).
-
Real-Time Synchronization: Issues are synchronized via standard Git push/fetch operations. There is no real-time event stream, no webhook protocol, and no live collaborative editing. Changes propagate when refs are explicitly pushed or fetched.
-
Non-Developer Participation: This format requires Git proficiency. Creating or modifying issues requires command-line Git access and understanding of refs, commits, and SHA identifiers. There is no provision for web-only users or issue creation via email.
-
Rich Media in v1: Binary attachments (images, videos, PDFs) are not supported in Format-Version 1. All issue data must be text-based commit messages and trailers. Binary attachments are deferred to a future format version (see Section 12).
-
Hosting Platform Integration: The format does not mandate how hosting platforms (GitHub, GitLab, Forgejo) should display or interact with
refs/issues/*. Platform support is optional and implementation- defined. The format can be used without any platform support. -
Guaranteed Conflict-Free Resolution: While the merge rules (Section 6) provide deterministic conflict resolution, they use heuristics (LWW, three-way set merge) that may not match user intent in all cases. The format does not use CRDTs or Lamport clocks for mathematically proven conflict-free behavior.
Issues are stored under the refs/issues/ namespace:
refs/issues/<uuid>
Where <uuid> is a full UUID version 4 (RFC 4122), lowercase,
hyphenated. Example:
refs/issues/a7f3b2c1-4e5d-4a8b-9c1e-2f3a4b5c6d7e
For human interaction, implementations SHOULD support abbreviated identifiers using the first 7 characters of the UUID (before the first hyphen). Implementations MUST expand abbreviated identifiers unambiguously, prompting the user if multiple issues match a prefix.
Example: a7f3b2c refers to the issue above.
There is no next-id file, no sequential counter, and no coordination
mechanism. UUIDs are generated independently on each clone. This is the
only identity scheme that guarantees zero collisions in a distributed
system without coordination.
Each issue is a chain of one or more commits pointed to by its ref. The ref always points to the latest commit in the chain (the issue HEAD).
refs/issues/<uuid>
|
v
[commit N] <-- issue HEAD (latest state)
|
[commit N-1]
|
...
|
[commit 1] <-- root commit (issue creation)
All issue commits use the empty tree. For SHA-1 repositories, this
is 4b825dc642cb6eb9a060e54bf899d15006578022. The empty tree hash
depends on the repository's hash algorithm; implementations SHOULD
compute it via git hash-object -t tree /dev/null.
Issues carry no file content; all data is in the commit message.
Implementations MUST use the empty tree. This ensures:
- No disk space wasted on tree objects
- Clear semantic distinction from code commits
- Compatibility with bare repositories
The commit author and committer fields carry the identity of the person
who created the issue or comment. Implementations SHOULD use the same
author identity as configured for regular code commits (user.name and
user.email).
Issue commits follow Git's standard commit message convention with trailers:
<subject line>
<-- blank line
<body>
<-- blank line
<trailer-key>: <trailer-value>
<trailer-key>: <trailer-value>
...
The root commit (first commit in the chain, with no parent) defines the issue:
<title>
<description>
State: open
Labels: <comma-separated labels>
Assignee: <email>
Format-Version: 1
Required fields:
- Subject line: The issue title (max 72 characters recommended)
State:trailer: MUST beopenFormat-Version:trailer: MUST be1
Optional fields:
- Body: Detailed description of the issue
Labels:trailer: Comma-separated list of labels (see Section 4.7)Assignee:trailer: Email address of the assigneePriority:trailer:low,medium,high, orcriticalMilestone:trailer: Milestone name
Example:
Fix login crash with special characters
The login page crashes when the user enters special characters
in the password field. Steps to reproduce:
1. Go to login page
2. Enter "pa$$w0rd" as password
3. Click submit
State: open
Labels: bug, auth
Priority: high
Format-Version: 1
A comment commit has the previous issue HEAD as its parent:
<comment summary>
<comment body>
Comment commits carry no trailers unless they also change issue state (see Section 4.4). The subject line is a short summary; the body contains the full comment text.
Example:
I can reproduce this on Firefox too
Tested on Firefox 120 and Chrome 119. Both crash with the same
error. The issue is in the password sanitizer function at
src/auth/sanitize.js:42.
A state change commit modifies issue metadata:
<description of change>
State: closed
Fixed-By: <commit-sha>
Recognized state values: open, closed
Implementations MAY support additional states but MUST recognize
open and closed.
Optional trailers for state changes:
Fixed-By:-- SHA of the commit that fixes the issueRelease:-- Version in which the fix shipsReason:-- Reason for closing (e.g.,duplicate,wontfix,invalid,completed)
Example:
Close issue -- fix deployed
The fix in commit abc123 handles special characters properly.
Verified in staging environment.
State: closed
Fixed-By: abc123def456789
Release: v2.1.0
Reason: completed
A single commit MAY contain both a comment and a state change. This is achieved by including trailers in a comment commit:
Fix deployed, closing
The fix in commit abc123 handles special characters properly.
State: closed
Fixed-By: abc123def456789
Implementations MAY use custom trailers prefixed with X-:
X-Severity: critical
X-Component: auth
X-Upstream-Bug: https://bugs.example.com/123
Custom trailers MUST NOT conflict with standard trailer names defined in this specification.
Labels in the Labels: trailer are comma-separated, with optional
whitespace after each comma. The canonical format is:
Labels: label1, label2, label3
Rules:
- Labels are case-sensitive:
Bugandbugare distinct labels - Labels MUST NOT contain commas (the separator character)
- Labels MUST NOT contain newlines
- Labels MUST NOT be empty strings
- Leading and trailing whitespace around each label is trimmed
- The canonical serialization uses comma-space (
,) as separator Labels: bug, authandLabels: bug,authare equivalent
When comparing labels for merge operations (Section 6.3), implementations MUST trim whitespace and compare the resulting strings exactly (case-sensitive).
The commit message format can be expressed in Augmented Backus-Naur Form (ABNF, RFC 5234):
issue-commit = root-commit / comment-commit / state-commit / merge-commit
root-commit = title CRLF CRLF description CRLF CRLF
"State: " state-value CRLF
[optional-trailers]
"Format-Version: 1" CRLF
comment-commit = comment-subject CRLF CRLF comment-body
[CRLF CRLF state-trailers]
state-commit = change-subject CRLF CRLF change-body CRLF CRLF
"State: " state-value CRLF
[state-trailers]
merge-commit = "Merge issue from " remote-name CRLF
[CRLF body]
CRLF CRLF
"State: " state-value CRLF
[merge-trailers]
title = TEXT-NO-LF ; max 72 characters recommended
comment-subject = TEXT-NO-LF
change-subject = TEXT-NO-LF
comment-body = *( TEXT-NO-LF CRLF )
description = *( TEXT-NO-LF CRLF )
change-body = *( TEXT-NO-LF CRLF )
body = *( TEXT-NO-LF CRLF )
state-value = "open" / "closed"
remote-name = TEXT-NO-LF
optional-trailers = *( trailer )
state-trailers = *( trailer )
merge-trailers = *( trailer )
trailer = trailer-key ": " trailer-value CRLF
trailer-key = "Labels" / "Assignee" / "Priority" / "Milestone" /
"Fixed-By" / "Release" / "Reason" / "Provider-ID" /
"Provider-Comment-ID" / "Parent-ID" / "Title" /
custom-trailer-key
custom-trailer-key = "X-" TEXT-NO-LF
trailer-value = TEXT-NO-LF ; must not contain actual LF
TEXT-NO-LF = *( %x20-7E / UTF8-2 / UTF8-3 / UTF8-4 )
; Any UTF-8 text except LF (0x0A)
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0-EF 2UTF8-tail
UTF8-4 = %xF0-F7 3UTF8-tail
UTF8-tail = %x80-BF
CRLF = %x0A ; Git uses LF, not CRLFNotes:
- This grammar uses
CRLFas a placeholder for line endings, but Git commit messages use LF (\n, 0x0A) not CRLF (\r\n). - The
TEXT-NO-LFproduction allows any UTF-8 text except newline. - Trailer values must not contain embedded newlines (security requirement from Section 4.9 and Section 11.1).
- The
Format-Version:trailer only appears in root commits.
Trailer values MUST NOT contain newline characters (LF or CRLF). Multi-line text in trailers creates security vulnerabilities (trailer injection attacks) and violates Git's trailer format specification.
Encoding Rules:
-
Single-line fields (State, Assignee, Priority, Milestone):
- MUST NOT contain newlines
- Implementations MUST reject values containing
\nor\r
-
Multi-line content (comments, long descriptions):
- MUST use separate commits instead of trailers
- Comments are commits (Section 4.2), not trailers
-
Multi-value fields (Labels):
- Use comma-separated values in a single trailer line
- MUST NOT use multiple
Labels:trailers
Validation Example (POSIX shell):
case "$value" in
*$'\n'*|*$'\r'*)
echo "error: trailer values cannot contain newlines" >&2
exit 1
;;
esacImplementations that encounter trailers with embedded newlines SHOULD reject the commit as malformed and warn the user. They MUST NOT silently accept such values, as this enables injection attacks.
To link a code commit to an issue, use the Fixes-Issue: trailer in
the code commit message:
Fix password sanitizer for special chars
The sanitizer was not escaping $ and other regex metacharacters.
Fixes-Issue: a7f3b2c
This is a trailer in a regular code commit (on a code branch), not in
an issue commit. Implementations SHOULD recognize these trailers and
display cross-references when showing issues. The Fixed-By: trailer
in issue commits (Section 4.3) complements this by recording the code
commit SHA from the issue side.
The current state of an issue is determined by walking the commit chain
from HEAD backward and taking the value of the State: trailer from
the most recent commit that contains one.
git log --format='%(trailers:key=State,valueonly)' refs/issues/<uuid> \
| grep -m1 .If no commit in the chain contains a State: trailer, the issue is
malformed. Implementations SHOULD treat such issues as open and
SHOULD warn the user.
Other fields follow similar rules:
| Field | Computation | Source |
|---|---|---|
| Title | Subject line of root commit, or most recent Title: trailer |
Root or any commit |
| Description | Body of root commit | Root commit only |
| State | Most recent State: trailer |
Any commit |
| Labels | See Section 6 (merge rules) | Most recent Labels: trailer |
| Assignee | Most recent Assignee: trailer |
Any commit |
| Priority | Most recent Priority: trailer |
Any commit |
| Milestone | Most recent Milestone: trailer |
Any commit |
| Comments | All non-root commits | Ordered by commit date |
For issues with no comments, a single for-each-ref command can list
issues:
git for-each-ref \
--format='%(refname:short) %(contents:subject) %(trailers:key=State,valueonly)' \
refs/issues/Note: %(contents:subject) returns the tip commit's subject line.
For issues with comments, this is the latest comment's subject, NOT
the issue title. To reliably obtain the issue title, implementations
MUST either:
- Walk to the root commit and read its subject line, or
- Use the most recent
Title:trailer if present (see Section 6.5), falling back to the root commit's subject line.
For state-only listing (without titles), the for-each-ref approach
above is efficient and correct, since State: trailers propagate to
the tip.
When two clones of a repository independently modify issues and then
synchronize (via git fetch + ref update), conflicts may arise.
The following rules define deterministic resolution:
Comments are individual commits. Both sides' commits are included in the merged chain, ordered by author timestamp. Conflicts are impossible because each comment is a distinct commit object.
When merging divergent issue branches, create a merge commit combining both chains. All comments from both sides are preserved.
If both sides changed the state, the commit with the later author timestamp wins. If timestamps are equal, the commit with the lexicographically greater SHA wins.
Labels use three-way set merge relative to the merge base:
- Compute the label set at the merge base (ancestor)
- Compute additions and removals for each side:
added_A = labels_A - ancestorremoved_A = ancestor - labels_A- (same for side B)
- Result =
ancestor + added_A + added_B - removed_A - removed_B - Tie-breaking: if one side added a label and the other removed it, the addition wins (bias toward keeping data)
The three-way set merge strategy uses case-sensitive string comparison and does NOT perform semantic analysis. This creates known limitations:
1. Case Variants:
bugandBugare distinct labels- Concurrent addition of both creates duplicates:
bug, Bug - Resolution: Manual cleanup or project conventions
2. Semantic Duplicates:
enhancementandfeatureare distinct labelsuianduser-interfaceare distinct labels- Merge does not detect that these represent the same concept
- Resolution: Projects SHOULD establish label naming conventions
3. Alias Collisions:
- If one branch renames
bug→defect(removebug, adddefect) - And another branch adds a comment with label
bug - Result: both
buganddefectlabels present - Resolution: Manual conflict resolution required
4. No Conflict Detection:
- The merge always produces a result (never fails)
- Semantically conflicting labels may coexist
- Implementations SHOULD provide a
git issue lintcommand to detect potential label conflicts
Recommendations for Projects:
- Document label naming conventions (CONTRIBUTING.md)
- Use lowercase labels consistently (
bug, notBug) - Avoid overlapping labels (
featureXORenhancement, not both) - Run
git issue lintperiodically to detect duplicates
Projects with strict label requirements SHOULD use access control (repository hooks) to enforce label naming, not rely on merge heuristics.
For Assignee:, Priority:, Milestone:, and other scalar fields:
the commit with the later author timestamp wins. Equal timestamps are
broken by lexicographically greater SHA.
The root commit's subject line is the canonical title. The root
commit's body is the canonical description. Implementations MAY
override the display title using a Title: trailer in a subsequent
commit. The most recent Title: trailer takes precedence (LWW).
Descriptions are not overridable.
When creating a merge commit to resolve a divergent issue, the commit MUST have two parents (local HEAD and remote HEAD) and use the empty tree. The commit message format:
Merge issue from <remote>
State: <resolved-state>
Labels: <resolved-labels>
Assignee: <resolved-assignee>
The subject line SHOULD be Merge issue from <remote>. The merge
commit MUST include trailers for all resolved fields that have
non-empty values. The merge commit MUST NOT include a
Format-Version: trailer (only the root commit carries this).
Status: This section describes a PROPOSED mechanism for unresolvable conflicts. It is NOT implemented in v1.0.0 and is deferred to a future format version.
In practice, the field-specific merge rules (Sections 6.1-6.4) resolve all conflicts automatically using heuristics (LWW, union, three-way set merge). After 8+ months of production use with multi-contributor testing, zero unresolvable conflicts have been encountered.
Future Mechanism (Format-Version 2+):
If a merge produces a conflict that cannot be resolved automatically (e.g., both sides changed the title to different values):
- Create a merge commit with a
Conflict:trailer listing the conflicting fields - The issue gets a
conflictpseudo-label until resolved - Implementations SHOULD provide a mechanism for manual conflict resolution that allows the user to select a value for each conflicting field
v1.0.0 Behavior: All conflicts are resolved automatically. If the heuristics produce an undesirable result, users can manually create a follow-up commit to correct the merged state
Git supports "octopus" merges with N parents (N ≥ 3). The merge rules in Sections 6.1-6.4 define two-way merge only (local HEAD + remote HEAD). For N-way merges, implementations MUST reduce the operation to pairwise merges:
Reduction Algorithm:
-
Compute the merge base of all N parents:
base=$(git merge-base --octopus parent1 parent2 ... parentN) -
Perform pairwise merge between base and first parent:
result1 = merge(base, parent1) -
Iteratively merge each subsequent parent:
result2 = merge(result1, parent2) result3 = merge(result2, parent3) ... resultN-1 = merge(resultN-2, parentN) -
The final result becomes the merge commit's metadata
Order Dependency:
The pairwise reduction is not commutative. Results MAY differ based on parent ordering when conflicts exist. For example:
merge(merge(base, A), B)may differ frommerge(merge(base, B), A)- This occurs when A and B both modify the same field differently
Implementations SHOULD process parents in chronological order (sorted by commit timestamp) to produce consistent results across different clones. If timestamps are equal, sort parents lexicographically by SHA.
Example (3-way merge):
Alice changes State: open → closed (timestamp: 1000)
Bob changes State: open → wontfix (timestamp: 1001)
Carol changes Labels: bug → bug, critical (timestamp: 1002)
Chronological order: Alice, Bob, Carol
Result:
State: wontfix (Bob's LWW wins over Alice)
Labels: bug, critical (Carol's change applied)
Practical Occurrence:
N-way merges are rare in practice for issue tracking (they require 3+ clones to independently modify the same issue before synchronizing). Most merges are 2-way. Implementations MAY warn users when performing N-way merges and suggest reviewing the result.
Issues travel via standard Git transport. No custom protocol is needed.
git fetch origin 'refs/issues/*:refs/issues/*'git push origin 'refs/issues/*'By default, git clone does NOT fetch refs/issues/*. To include
issues in a clone, configure the fetch refspec:
git config --add remote.origin.fetch '+refs/issues/*:refs/issues/*'
git fetch originImplementations SHOULD configure this refspec automatically when initialized in a repository.
When issues are fetched from a remote, the local and remote refs may diverge (both sides made changes since the last sync). Implementations SHOULD resolve divergent refs automatically using the merge rules in Section 6.
The merge workflow:
-
Fetch remote issue refs into a staging namespace:
git fetch <remote> '+refs/issues/*:refs/remotes/<remote>/issues/*'
-
For each remote ref, compare with the corresponding local ref:
- New issue (no local ref): create local ref pointing to remote HEAD
- Fast-forward (local is ancestor of remote): update local ref
- Up-to-date (remote is ancestor of local): no action
- Diverged: create a merge commit with two parents (local HEAD and remote HEAD), resolving metadata per Section 6
-
Clean up remote tracking refs after merge.
Merge commits use the empty tree (same as all issue commits) and carry the resolved metadata as trailers.
Issues are compatible with shallow clones. A shallow fetch of
refs/issues/* with --depth=1 provides the current state of all
issues without full history. This is useful for large repositories
where full issue history is not needed locally.
Each issue creates one Git ref. For repositories with many issues
(1000+), the ref advertisement during git fetch can become a
bottleneck on Git protocol v1, which advertises ALL refs.
Implementations SHOULD recommend Git protocol v2, which uses server-side ref filtering:
git config protocol.version 2With protocol v2, only requested refs are transferred, keeping fetch performance constant regardless of issue count.
External issue trackers (GitHub, GitLab, Jira) can be bridged via import/export operations. The current implementation uses direct provider integrations; a plugin protocol is planned for when multiple provider backends exist.
Import creates local refs/issues/ commits from an external provider:
- Fetch issue list from the provider (paginated)
- For each issue not already imported (checked via
Provider-ID:trailer):- Generate a UUID and create a root commit with full metadata
- Set
GIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL,GIT_AUTHOR_DATEfrom the original author - Import comments as child commits preserving original authorship
- If the issue is closed, append a
State: closedcommit - Record the source via
Provider-ID:trailer (see Section 9)
- Skip issues whose
Provider-ID:already exists locally (idempotent)
Implementations MUST filter out non-issue items (e.g., GitHub pull requests are not issues).
Export creates provider issues from local refs/issues/ data:
- For each local issue ref:
- If
Provider-ID:matches the target provider: sync state changes - If
Provider-ID:matches a different provider: skip (foreign import) - If no
Provider-ID:: create a new issue on the provider, export comments, sync state, and record theProvider-ID:locally by appending a child commit
- If
- Comment export: commits without trailers are treated as comments; commits with trailers are metadata changes (skipped)
When the --cross-platform flag is passed, export implementations
MUST NOT skip issues with foreign Provider-ID: values. Instead,
they fall through to the new-issue creation path. After the first
cross-platform export, the issue gains a target Provider-ID: (e.g.,
github:owner/repo#42), and subsequent exports enter normal sync mode.
This enables importing from one provider (e.g., Azure DevOps) and exporting to another (e.g., GitHub):
git issue import azuredevops:org/project --state all
git issue export github:owner/repo --cross-platform
The Provider-ID: trailer ensures:
- Import then export does not create duplicates
- Re-import skips already-imported issues
- Re-export syncs state without duplicating
When a second provider backend is needed, implementations MAY adopt a
plugin protocol where git-issue-remote-<provider> is a separate
executable speaking a line-oriented text protocol on stdin/stdout.
The protocol SHOULD support capabilities, list, fetch, and push
commands. The protocol MUST use line-oriented text (NOT JSON) to
eliminate JSON injection vulnerabilities.
When issues are imported from external providers, the source is
recorded via a Provider-ID: trailer:
Provider-ID: github:owner/repo#42
Format: <provider>:<identifier>
| Provider | Format | Example |
|---|---|---|
| GitHub | github:<owner>/<repo>#<number> |
github:myorg/myrepo#42 |
| GitLab | gitlab:<group>/<project>#<number> |
gitlab:team/app#17 |
| Gitea | gitea:<owner>/<repo>#<number> |
gitea:dev/api#5 |
| Forgejo | forgejo:<owner>/<repo>#<number> |
forgejo:forge/core#12 |
| Azure DevOps | azuredevops:<org>/<project>#<id> |
azuredevops:contoso/MyProject#1234 |
Comments use Provider-Comment-ID: with a #comment-<id> suffix:
Provider-Comment-ID: github:owner/repo#comment-123456
Provider-Comment-ID: azuredevops:org/project#comment-789
For providers with hierarchical work items (e.g., Azure DevOps Epics,
Features, User Stories, Tasks), the Parent-ID: trailer records the
parent relationship:
Parent-ID: azuredevops:org/project#42
This preserves the hierarchy from the source provider. Implementations MAY use this trailer to reconstruct parent-child relationships.
Provider-ID enables:
- Round-trip import/export without duplication
- Cross-reference between local and remote issue IDs
- Detecting already-imported issues during subsequent imports
- Cross-platform export (see Section 8.2)
This format does NOT use git notes. While git notes can attach
metadata to objects, notes are mutable (violating append-only semantics)
and do not support the commit-chain model needed for issue history.
This format is intentionally simpler than git-bug's operation-based CRDT model. git-bug stores JSON blobs in git objects with Lamport clocks. This format stores human-readable commit messages with standard Git trailers. The two formats are not interoperable, but a bridge between them is straightforward.
Fossil's ticket system uses immutable artifacts in a G-Set CRDT, materialized into SQLite tables. This format achieves similar properties (append-only commits, deterministic state computation) using Git's native object model instead of SQLite.
This format uses proper Git refs (refs/issues/*) and is fully
compatible with the reftable backend. No plain files outside the
ref system are used.
Implementations MUST validate that user-supplied values for trailer fields do not contain newline characters. A newline in a trailer value would inject arbitrary trailers.
Implementations MUST validate that issue titles do not contain newline characters, as this would corrupt the commit message format.
Implementations MUST NOT pass user-supplied values through shell
expansion. Use printf '%s' instead of echo for user content.
Use -- end-of-options markers in all git commands that accept
user-supplied arguments.
The following features are deferred to Format-Version 2 or later:
-
Conflict representation:
Conflict:trailer for unresolvable merge conflicts (Section 6.8). Not implemented in v1.0.0; all conflicts currently resolved automatically via heuristics. -
Binary attachments: Store as blobs in the issue commit's tree object (instead of using the empty tree)
-
Reactions: Emoji reactions as a
Reaction:trailer -
Templates: Issue templates as blobs in
refs/issues/templates/ -
Access control: Per-issue access control lists
-
Live sync: Bidirectional real-time synchronization with external providers
-
Advanced label merging: Semantic duplicate detection (e.g.,
enhancement=feature) and case normalization. Currently uses exact string matching (Section 6.3.1).
Extensions MUST increment the Format-Version: trailer. Implementations
MUST ignore trailers they do not recognize. Implementations MUST NOT
reject issues with a Format-Version: higher than they support; they
SHOULD process what they understand and warn about unrecognized fields.
This specification was inspired by:
- Linus Torvalds's 2007 vision of "a git for bugs"
- D. Richard Hipp's Fossil ticket system
- Michael Mure's git-bug CRDT data model
- Julian Ganz and Matthias Beyer's git-dit commit-based approach
- Google's git-appraise
refs/notes/devtools/architecture - Diomidis Spinellis's git-issue pragmatic shell implementation