Pull requests and review feedback stored as commits on the gitmsg/review branch. PRs live on the author's repository and are discovered by reviewers via fetch/follow.
Spec: GITREVIEW.md — wire format for pull requests, feedback, review states, and version tracking.
gitsocial review init # creates refs/gitmsg/review/config and the gitmsg/review branch
gitsocial review init -b <branch> # initialize on a custom branch
gitsocial review config get / set / list
gitsocial review pr create "Add dark mode" \
--base '#branch:main' --head '#branch:dark-mode' \
--reviewers [email protected],[email protected] \
--closes <issue-ref> \
--stack # auto-set depends-on from base→head matching
gitsocial review pr list
gitsocial review pr show <ref>
gitsocial review pr update <ref> # capture new branch tips as a new version
gitsocial review pr sync <ref> [--strategy rebase|merge]
gitsocial review pr merge <ref> [--strategy fast-forward|squash|rebase|merge]
gitsocial review pr close <ref>
gitsocial review pr retract <ref>
gitsocial review pr draft <ref> # open → draft
gitsocial review pr ready <ref> # draft → open
gitsocial review pr diff <ref> # range-diff between PR versions
gitsocial review pr stack <ref> # show the full stack
gitsocial review pr rebase-stack <ref> # cascade rebase PRs above
gitsocial review pr sync-stack <ref> # snapshot tips for all PRs in stack
--base and --head accept any ref (#branch:<name> for local, <url>#branch:<name> for cross-forge — see §2 Cross-Forge Contribution). --depends-on sets stack relationships explicitly when --stack auto-detection isn't enough.
gitsocial review feedback approve <pr-ref> [-m "LGTM"]
gitsocial review feedback request-changes <pr-ref> -m "Reasoning..."
gitsocial review feedback comment "Consider caching this" \
--pr <pr-ref> --commit <12-char-sha> --file path/to.go \
--new-line 42 [--new-line-end 50] [--old-line 40] [--suggest]
Reviews are tied to the version the reviewer saw; force-push doesn't auto-dismiss them. See Version Tracking and Review Staleness.
gitsocial review fork add <fork-url> # alias for `gitsocial fork add`
gitsocial review fork list
gitsocial review fork remove <fork-url>
PRs from registered forks appear in pr list and trigger fork-pr notifications. See Fork PR Discovery for the full flow.
The TUI's review section (R from any screen — Pull Requests) provides the PR list, detail, files-changed, interdiff, history, and feedback flows.
The remainder of this document walks through usage scenarios in detail.
GitSocial stores PRs as commits on a gitmsg/review branch inside the repo itself. The repo is the source of truth. Everything else follows from that.
Cross-forge PRs. base and head fields are URLs, not internal IDs. A GitLab contributor can submit a PR targeting a GitHub upstream. On merge, fork PRs are copied to upstream with the original author preserved — surviving fork deletion.
Rebase-resilient identity. The PR references head="#branch:feature" — the branch name doesn't change when its commits are rebased. State changes (edits, merge, close) are new commits linked via edits. Force-push can't destroy review context because review data lives on a separate branch.
Version tracking. Every pr update records the hash of the base and head branch tips. The edits chain is the version history — walking it gives every recorded code snapshot. pr diff runs git range-diff between any two versions. Feedback timestamps are compared against version timestamps to determine which version a reviewer saw — pure rebases (patches identical) are distinguished from actual code changes. Reviews are never auto-dismissed.
Per-PR merge strategies. Fast-forward, squash, rebase, and merge commit — chosen per-PR via --strategy, not a repo-wide setting.
| Pain Point | GitHub/GitLab | GitSocial |
|---|---|---|
| Cross-forge PR | Impossible | Protocol-native via URLs |
| PR identity survives rebase | Tied to commit SHAs | PR commit on review branch = permanent anchor |
| History of code changes | Force-push = gone | base-tip/head-tip snapshots in edits chain |
| Review lost on rebase | Comments hidden/outdated | Edits chain preserves all versions; range-diff shows what changed |
| What changed between reviews? | GitLab has version diffs; GitHub has no built-in comparison | pr diff shows range-diff between any two versions |
| Review dismissal on rebase | Unpredictable auto-dismiss | Range-diff detects actual code changes vs. pure rebase; never auto-dismiss |
| Merge strategy | Repo-level config; choose from enabled options | All 4 strategies per-PR via --strategy |
| Update branch | Merge vs rebase dilemma | pr sync with rebase or merge, auto-captures tips |
| Fork PR, fork deleted | Review orphaned | PR copied to upstream on merge |
Alice and Bob work on the same repo. Alice proposes a change, Bob reviews it.
Alice Bob
│ │
● push dark-mode branch │
● create PR │
│ open, base=main │
│ head=dark-mode │
│ │
│ ● fetch gitmsg/review
│ ● see open PR
│ ● post inline review
│ │
● read review, push fix │
│ │
│ ● approve
│ │
● merge (state=merged) │
● closes linked issues │
Alice creates pull request:
Add dark mode support
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:main"; base-tip="f1e2d3c4b5a6"; closes="#commit:abc123456789@gitmsg/pm"; head="#branch:dark-mode"; head-tip="a1b2c3d4e5f6"; reviewers="[email protected]"; v="0.1.0"
Bob posts inline review:
Consider caching this value to avoid recomputation on every render.
GitMsg: ext="review"; type="feedback"; pull-request="#commit:aaa111222333@gitmsg/review"; commit="def456789abc"; file="src/theme.js"; line-start="42"; side="right"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="#commit:aaa111222333@gitmsg/review"; v="0.1.0"
> Add dark mode support
Bob approves:
LGTM!
GitMsg: ext="review"; type="feedback"; pull-request="#commit:aaa111222333@gitmsg/review"; review-state="approved"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="#commit:aaa111222333@gitmsg/review"; v="0.1.0"
> Add dark mode support
Alice merges (edits original pull request commit):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; edits="#commit:aaa111222333@gitmsg/review"; state="merged"; base="#branch:main"; head="#branch:dark-mode"; merge-base="f1e2d3c4b5a6"; merge-head="a1b2c3d4e5f6"; v="0.1.0"
Alice works on GitLab, Bob maintains the upstream repo on GitHub. Alice forks, proposes changes, Bob reviews on GitHub.
GitLab (alice) GitHub (bob)
│ │
● push dark-mode branch │
● create PR │
│ head=gitlab/alice/repo │
│ base=github/bob/repo │
│ │
│ ● follow alice's repo
│ ● fetch gitmsg/review
│ ● see cross-forge PR
│ ● post review
│ │
● read review (fetch bob) │
● push fix │
│ │
│ ● approve
│ ● merge
Alice creates cross-forge pull request (on her GitLab repo's gitmsg/review branch):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; state="open"; base="https://github.com/bob/repo#branch:main"; base-tip="f1e2d3c4b5a6"; head="https://gitlab.com/alice/repo#branch:dark-mode"; head-tip="a1b2c3d4e5f6"; v="0.1.0"
Bob reviews (on his GitHub repo's gitmsg/review branch):
Solid approach. One suggestion on the CSS transitions.
GitMsg: ext="review"; type="feedback"; pull-request="https://gitlab.com/alice/repo#commit:bbb222333444@gitmsg/review"; review-state="approved"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="https://gitlab.com/alice/repo#commit:bbb222333444@gitmsg/review"; v="0.1.0"
> Add dark mode support
Bob merges (on his GitHub repo):
Step 1 — Copy fork PR to upstream (preserves original author via GitMsg-Ref):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:main"; base-tip="f1e2d3c4b5a6"; head="https://gitlab.com/alice/repo#branch:dark-mode"; head-tip="a1b2c3d4e5f6"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="https://gitlab.com/alice/repo#commit:bbb222333444@gitmsg/review"; v="0.1.0"
> Add dark mode support
Step 2 — Merge edit references the local copy:
Add dark mode support
GitMsg: ext="review"; type="pull-request"; edits="#commit:ccc333444555@gitmsg/review"; state="merged"; base="#branch:main"; head="https://gitlab.com/alice/repo#branch:dark-mode"; merge-base="f1e2d3c4b5a6"; merge-head="a1b2c3d4e5f6"; v="0.1.0"
A maintainer registers forks so PRs (and issues) from contributors are automatically discovered during fetch, appear in pr list, and trigger notifications.
Maintainer (upstream) Contributor (fork)
│ │
● gitsocial fork add │
│ <fork-url> │
│ │
│ ● push feature branch
│ ● create PR
│ │ base=#branch:main
│ │ head=#branch:feature
│ │
● gitsocial fetch │
│ → fetches fork's gitmsg/* branches │
│ → all extension processors │
│ │
● pr list shows fork PR │
● notification: "fork-pr" │
│ │
● review / merge │
gitsocial fork add https://github.com/contributor/repogitsocial fetch, all gitmsg/* branches from each registered fork are fetched and processed through all extension processors (review, PM, social, release)base that is a local ref (#branch:main) or explicitly targets the workspace URL are included in pr listfork-pr notifications; feedback on workspace PRs appears as feedback/approved/changes-requested notificationsForks are stored in the core config (refs/gitmsg/core/config):
{
"version": "0.1.0",
"forks": [
"https://github.com/contributor/repo"
]
}
Reviewer proposes concrete code changes that the author can apply directly.
Bob Alice
│ │
● create PR │
│ │
│ ● post suggestion
│ │
● apply suggestion │
● push updated branch │
│ │
│ ● approve
Alice suggests a change: ~~~ Use a CSS custom property for the transition
transition: background-color var(--theme-transition, 200ms) ease;
GitMsg: ext="review"; type="feedback"; pull-request="#commit:aaa111222333@gitmsg/review"; commit="def456789abc"; file="src/theme.css"; line-start="18"; side="right"; suggestion="true"; v="0.1.0" GitMsg-Ref: ext="review"; type="pull-request"; author="Bob"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="#commit:aaa111222333@gitmsg/review"; v="0.1.0"
Refactor theme transitions
~~~
A pull request requires multiple reviewers. Aggregation determines merge readiness.
Alice Bob Carol
│ │ │
● create PR │ │
│ reviewers= │ │
│ bob,carol │ │
│ │ │
│ ● changes-requested │
│ │ │
● push fix │ │
│ │ │
│ ● approved │
│ │ ● approved
│ │ │
● merge │ │
changes-requested → pull request is blockedapproved → pull request is ready to mergeA pull request closes issues when merged. The closes field contains commit references to PM issues.
Pull request that closes two issues:
Add dark mode support
Implements theme switching with system preference detection.
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:main"; base-tip="f1e2d3c4b5a6"; closes="#commit:abc123456789@gitmsg/pm,#commit:def456789012@gitmsg/pm"; head="#branch:dark-mode"; head-tip="a1b2c3d4e5f6"; v="0.1.0"
GitMsg-Ref: ext="pm"; type="issue"; author="Alice"; email="[email protected]"; time="2025-01-06T10:00:00Z"; ref="#commit:abc123456789@gitmsg/pm"; v="0.1.0"
> Add dark mode support
GitMsg-Ref: ext="pm"; type="issue"; author="Bob"; email="[email protected]"; time="2025-01-07T09:00:00Z"; ref="#commit:def456789012@gitmsg/pm"; v="0.1.0"
> Support system theme preference
When the pull request transitions to state="merged", implementations auto-close the referenced issues.
General discussion (not anchored to code) uses GitSocial comments. Nested replies use reply-to.
Alice Bob Carol
│ │ │
● create PR │ │
│ │ │
│ ● "Should we also │
│ │ handle prefers- │
│ │ contrast?" │
│ │ │
│ │ ● "Yes, good idea.
│ │ │ I can follow up
│ │ │ in a separate PR."
│ │ │
● "Filed │ │
│ issue #42" │ │
Bob comments on the pull request:
Should we also handle prefers-contrast?
GitMsg: ext="social"; type="comment"; original="#commit:aaa111222333@gitmsg/review"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="#commit:aaa111222333@gitmsg/review"; v="0.1.0"
> Add dark mode support
Carol replies to Bob's comment:
Yes, good idea. I can follow up in a separate PR.
GitMsg: ext="social"; type="comment"; original="#commit:aaa111222333@gitmsg/review"; reply-to="#commit:ccc333444555@gitmsg/social"; v="0.1.0"
GitMsg-Ref: ext="social"; type="comment"; author="Bob"; email="[email protected]"; time="2025-01-20T11:00:00Z"; ref="#commit:ccc333444555@gitmsg/social"; v="0.1.0"
> Should we also handle prefers-contrast?
Full state machine for a pull request:
create
│
v
┌──────┐ ┌────────┐
│ open │─────────> │ closed │
└──┬───┘ └────────┘
│ reopen via
v new PR
┌────────┐
│ merged │
└────────┘
open → merged: The base owner (whoever owns the target/base repo) edits the original pull request commit with state="merged"open → closed: The base owner edits with state="closed" to decline the pull requestretracted="true", hiding it from all views. Distinct from state="closed", which is the base owner's declineThe TUI provides a files changed view accessible from the PR detail via d. It shows syntax-highlighted unified diffs with colored added/removed lines and line number gutters.
Inline feedback from the diff view pre-fills the file, line number, and side parameters when creating feedback commits, producing the same file, line-start, and side headers as CLI-created inline reviews.
When an author rebases or pushes new commits, pr update captures new branch tips as a version. Reviewers see version-aware staleness — using git range-diff to distinguish pure rebases from actual code changes.
Alice Bob
│ │
● create PR (head=dark-mode) │
│ base-tip="aaa", head-tip="bbb" │
│ │
│ ● review, request changes
│ │
● push fix │
● pr update │
│ base-tip="aaa", head-tip="ccc" │
│ │
│ ● fetch → sees new version
│ ● pr show:
│ │ "changes requested
│ │ (reviewed original,
│ │ current is latest,
│ │ code changed) [stale]"
│ │
│ ● pr diff → range-diff
│ │ shows what changed
│ ● approve
Alice creates PR (version 0 = original):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:main"; base-tip="aaa111bbb222"; head="#branch:dark-mode"; head-tip="bbb222ccc333"; reviewers="[email protected]"; v="0.1.0"
Alice runs pr update after pushing fixes (version 1 = latest):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; edits="#commit:aaa111222333@gitmsg/review"; state="open"; base="#branch:main"; base-tip="aaa111bbb222"; head="#branch:dark-mode"; head-tip="ccc333ddd444"; v="0.1.0"
Updates are explicit — the author signals "new code is ready" by running pr update. Not every WIP push triggers a version.
The reviewer's feedback has a timestamp. The PR's edits chain has timestamps and head-tip values. By comparing them, we know which version the reviewer saw. When the head-tip changed, git range-diff determines if the patches actually changed:
head-tip unchanged → approval is current ("no code changes")head-tip changed, patches identical (pure rebase) → "no code changes"[stale] markerFour merge strategies available per-PR via --strategy flag. Teams pick per-PR instead of fighting over a repo-wide default.
Alice
│
● pr merge <ref> # fast-forward (default)
● pr merge <ref> --strategy squash # squash all commits
● pr merge <ref> --strategy rebase # replay commits onto base
● pr merge <ref> --strategy merge # force merge commit
After a successful merge, the base branch is pushed to origin so the remote code agrees with the merged state published on gitmsg/review. A failed base push is reported as a warning (the merge itself already happened locally); push the base manually to recover.
Merge edit (same for all strategies — the git operation differs, but the review record is the same):
Add dark mode support
GitMsg: ext="review"; type="pull-request"; edits="#commit:aaa111222333@gitmsg/review"; state="merged"; base="#branch:main"; base-tip="ddd444eee555"; head="#branch:dark-mode"; head-tip="eee555fff666"; merge-base="ddd444eee555"; merge-head="eee555fff666"; v="0.1.0"
merge-base and merge-head are captured before the merge (lost after fast-forward). They enable reconstructing the original diff of a merged PR.
Keep the head branch up-to-date with the base branch. pr sync rebases or merges, then auto-captures new tips.
Alice
│
● pr show → "3 commits behind main"
│
● pr sync <ref> # rebase head onto base (default)
● pr sync <ref> --strategy merge # merge base into head
│
│ → auto-runs pr update
│ → new version with updated tips
After sync, the version history shows the rebase via range-diff. Reviewers can see what changed (if anything) between the pre-sync and post-sync versions.
Decompose a large change into an ordered chain of small PRs. Each PR builds on the one below it — the bottom targets main, each subsequent PR targets the previous PR's branch. The depends-on field makes the relationship explicit so it survives branch renames and works across forges.
Alice Bob
│ │
● push auth-middleware │
● pr create PR1 │
│ base=main, head=auth-middleware│
│ │
● push auth-routes │
● pr create PR2 --stack │
│ base=auth-middleware │
│ head=auth-routes │
│ → auto-sets depends-on=PR1 │
│ │
● push auth-tests │
● pr create PR3 --stack │
│ → auto-sets depends-on=PR2 │
│ │
│ ● pr stack <PR2>
│ │ shows full chain
│ ● review PR1, approve
│ │
● pr merge PR1 │
│ → PR2 auto-retargets to main │
│ │
● pr rebase-stack PR2 │
│ → rebases PR2, PR3 onto main │
│ ● review PR2, approve
│ │
● pr merge PR2 │
│ → PR3 auto-retargets │
Alice creates PR1 (root of stack):
Add auth middleware
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:main"; base-tip="a1b2c3d4e5f6"; head="#branch:auth-middleware"; head-tip="b2c3d4e5f6a7"; v="0.1.0"
Alice creates PR2 (stacked on PR1):
Add API routes for auth
GitMsg: ext="review"; type="pull-request"; state="open"; base="#branch:auth-middleware"; base-tip="b2c3d4e5f6a7"; head="#branch:auth-routes"; head-tip="c3d4e5f6a7b8"; depends-on="#commit:abc123456789@gitmsg/review"; v="0.1.0"
GitMsg-Ref: ext="review"; type="pull-request"; author="Alice"; email="[email protected]"; time="2025-01-20T10:00:00Z"; ref="#commit:abc123456789@gitmsg/review"; v="0.1.0"
> Add auth middleware
Alice merges PR1 — PR2 gets auto-retargeted:
After PR1 is merged, an edit commit is created for PR2 updating its base from #branch:auth-middleware to #branch:main:
Add API routes for auth
GitMsg: ext="review"; type="pull-request"; edits="#commit:def456789012@gitmsg/review"; state="open"; base="#branch:main"; base-tip="d4e5f6a7b8c9"; head="#branch:auth-routes"; head-tip="c3d4e5f6a7b8"; depends-on="#commit:abc123456789@gitmsg/review"; v="0.1.0"
The depends-on ref still points to the now-merged PR1 — the relationship is preserved as history. Only base changes to reflect the new merge target.
View the full stack from any member:
gitsocial review pr stack <any-member-ref>
Stack (3 PRs):
✓ #1 Add auth middleware main ← auth-middleware [merged]
● #2 Add API routes for auth auth-middleware ← auth-routes [open]
● #3 Add auth integration tests auth-routes ← auth-tests [open]
Cascade rebase — when you update a PR in the middle of a stack, rebase all PRs above it:
gitsocial review pr rebase-stack <pr-ref>
Walks up the chain via depends-on. For each dependent: rebases its head branch onto its base and creates an edit with updated tips. Stops on first conflict with a clear error identifying which PR failed.
Sync stack tips — snapshot current branch tips for all open PRs (no rebase):
gitsocial review pr sync-stack <pr-ref>
pr merge refuses to merge a PR if any of its depends-on targets is unmerged. This enforces bottom-up order — you cannot merge PR2 while PR1 is still open.
Because depends-on uses the standard ref format with optional repo URLs, stacks can span forges:
PR1 on GitHub: base="#branch:main"; depends-on=(none)
PR2 on GitLab: base="https://github.com/org/repo#branch:auth-middleware";
depends-on="https://github.com/org/repo#commit:abc123@gitmsg/review"
No forge needs to understand "stacks" natively. Stack metadata lives in the git commit, readable by any GitSocial implementation regardless of hosting.
When importing PRs from GitHub or GitLab via gitsocial import review, stack relationships are auto-detected by matching base branches to head branches across imported PRs (same-repo, open PRs only). Each child PR gets an edit commit adding depends-on pointing to the parent.
The detection logic is platform-agnostic: both FetchReview adapters populate BaseBranch, HeadBranch, and HeadRepo uniformly on ImportPR, so the same matching rules apply regardless of source.
stacked badge in the subtitle[ jumps to the previous PR in the stack, ] jumps to the next — without leaving the detail view