Merge requests
A merge request proposes a branch for a target (the default branch unless set).
ermite mr create myrepo --source feature --title "Add feature"
ermite mr show myrepo 1 --diff
ermite mr merge myrepo 1 --head <commit> [--mode fast-forward|squash|merge] [--delete-branch]
ermite mr create and ermite mr close print a confirmation followed by the full
merge request web URL on its own line. With --json, merge request objects
(create, show, list, close and merge) include url alongside the other fields.
Merge modes
| Mode |
Result |
Needs |
fast-forward (default) |
the target moves to the head, no commit |
the target is an ancestor of the head |
squash |
one new commit with the merged tree, the target tip as only parent |
the branch merges without conflicts |
merge |
a merge commit with two parents, the target tip and the head |
the branch merges without conflicts |
- The server never rebases. A branch behind its target merges with
squash or merge, or is rebased and pushed again for a fast-forward.
- The server checks the mode again when it merges, whatever the badge showed, and refuses with the reason (the target moved, a conflict with the paths).
- The merge is also refused when the branch moved since the reviewed
--head.
- With
failing_checks_block on (default), the merge is refused while a check of the head failed or is still running.
- The merge answers as soon as the target has moved.
--delete-branch then deletes the source branch right after, in the background (unless it is the default branch or another open request uses it); if the branch moved meanwhile, it is kept.
- Every mode is allowed by default, including in repositories that had only
fast-forward and squash before merge existed: those gain merge. Disable a mode per repository with ermite repo edit <repo> --merge-modes fast-forward,squash (list the modes to keep).
- Scopes:
mr:write to open and comment, mr:review to give a verdict, mr:merge to merge.
Signed commits
Squash and merge commits are signed by ermite's own key. The key is generated automatically the first time the server starts, belongs to the server, and its committer identity is ERMITE_COMMITTER_NAME / ERMITE_COMMITTER_EMAIL. ermite signing-key show prints its fingerprint and the allowed_signers line; use it to verify locally, or register the public key on GitHub or Forgejo.
ermite signing-key show --allowed-signers > allowed_signers
git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commit <commit>
The merge box of the web page and the output of mr merge --mode squash|merge repeat this with the fingerprint.
Mergeability
The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. mr show prints it; the web merge box shows it as a live badge; ermite watch reports each change.
| State |
Meaning |
Modes possible |
checking |
no result yet for the current branches |
none (wait) |
up_to_date |
the target is an ancestor of the head |
all |
behind |
the head lacks behind commits of the target (behind_capped: at least 1000) but merges without conflicts |
squash, merge |
conflict |
the merge conflicts; up to 20 conflict_paths are listed (conflict_paths_truncated when there are more); no paths means no common history |
none (rebase) |
ermite mr show myrepo 1
# mergeability behind by 2, merges without conflicts (squash or merge commit; fast-forward needs a rebase)
In mr show --json, mergeability holds state, behind, behind_capped, conflict_paths, conflict_paths_truncated, target_id and head_id; it is null unless the request is open. ermite watch prints merge_request_mergeability events when the state changes (not on every push):
2026-10-19T14:00:00Z merge_request_mergeability myrepo!1 mergeability: behind by 2
In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box.
Large diffs on the web
The merge request, commit and compare pages list every changed file. Generated files (lockfiles, vendor/, node_modules/, minified or generated code) and large files (over 2,000 changed lines) are collapsed: the header and the counts show, the lines do not. "load diff" fetches that one file in place, in full, and you can comment on its lines. Without scripts the link reloads the page with that file loaded. After the first 25 files of a merge request, the other files load as you scroll.
A commit page reads the hunks of its first 300 files with the page; the other files are listed collapsed and load one at a time. A file that cannot be shown in the browser, over 8 MiB of text or over 50,000 changed lines when loaded alone, has no "load diff": the page links to its raw blob instead, and its lines take no comment.
The API and mr show --diff --json keep their limits (300 files, 2,000 changed lines per file, 20,000 in total, 8 MiB). A file read without its hunks has truncated set and omitted giving the reason: large, budget (the earlier files used the limits) or unavailable (over 8 MiB of text, or a line over 64 KiB, even alone). Files past the 300th are not listed and diff_truncated is set.
Notes
A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
ermite mr comment myrepo 1 -m "General remark"
ermite mr comment myrepo 1 -m "Why?" --path main.go --line 10-14 --side new
ermite mr comment myrepo 1 -m "Fixed in ^3" --reply 2
ermite mr resolve myrepo 1 2
ermite mr edit-comment myrepo 1 2 -m "New text"
ermite mr delete-comment myrepo 1 2
- Notes are Markdown, up to 8000 characters, rendered without raw HTML. Quote with
> .
^N in a note or a description links to note N of the same merge request. A reference to a missing note stays plain text.
- A note is general, on one line, or on a range
A-B of lines on one side (old or new) of the diff. The lines must be in the diff.
- Threads have one level: a reply (
--reply <id>) takes the anchor of its root and cannot carry a verdict. Resolving or reopening is done on the root and covers its replies.
- Only the author edits or deletes a note. An edited note shows "edited". A deleted note loses its text; a deleted root with replies stays as "deleted note".
Verdicts
A root note can carry a verdict: --approve or --request-changes. The text may then be empty. A verdict needs the mr:review scope (or the owner); mr:write comments but cannot give one.
- A verdict ends a review: agents and
ermite watch act on it, not on each note.
- Verdicts are informative: none blocks or allows a merge.
mr show lists the latest verdict of each author (verdicts in --json).
- An approval is stale once the branch head differs from the head it was given on. Changes requested stay current until their author gives a new verdict, whatever is pushed.
- Verdicts are immutable: editing or deleting the note keeps its verdict. Only a new verdict of the same author replaces it.
Shared read position and history queries
The read position is shared, not per principal. MergeRequestDetail.HeadUnread
is a fact: the nonzero head differs from that position. The web presentation
still decides who sees the mark-read button.
Repository comparisons return at most ports.MaxCompareCommits (500) commits.
Push summaries count at most ports.MaxPushSummaryCommits (100) commits and
report whether more exist; ancestry, rather than the bounded list, detects a
rewrite. Merge-request details include at most ports.MaxMergeRequestCommits
(100) commits. These history queries remain on the merge-request service
because it owns the merge-git port.
source
1# Merge requests
2
3A merge request proposes a branch for a target (the default branch unless set).
4
5```sh
6ermite mr create myrepo --source feature --title "Add feature"
7ermite mr show myrepo 1 --diff
8ermite mr merge myrepo 1 --head <commit> [--mode fast-forward|squash|merge] [--delete-branch]
9```
10
11`ermite mr create` and `ermite mr close` print a confirmation followed by the full
12merge request web URL on its own line. With `--json`, merge request objects
13(create, show, list, close and merge) include `url` alongside the other fields.
14
15## Merge modes
16
17| Mode | Result | Needs |
18|---|---|---|
19| `fast-forward` (default) | the target moves to the head, no commit | the target is an ancestor of the head |
20| `squash` | one new commit with the merged tree, the target tip as only parent | the branch merges without conflicts |
21| `merge` | a merge commit with two parents, the target tip and the head | the branch merges without conflicts |
22
23- The server never rebases. A branch behind its target merges with `squash` or `merge`, or is rebased and pushed again for a fast-forward.
24- The server checks the mode again when it merges, whatever the badge showed, and refuses with the reason (the target moved, a conflict with the paths).
25- The merge is also refused when the branch moved since the reviewed `--head`.
26- With `failing_checks_block` on (default), the merge is refused while a check of the head failed or is still running.
27- The merge answers as soon as the target has moved. `--delete-branch` then deletes the source branch right after, in the background (unless it is the default branch or another open request uses it); if the branch moved meanwhile, it is kept.
28- Every mode is allowed by default, including in repositories that had only `fast-forward` and `squash` before `merge` existed: those gain `merge`. Disable a mode per repository with `ermite repo edit <repo> --merge-modes fast-forward,squash` (list the modes to keep).
29- Scopes: `mr:write` to open and comment, `mr:review` to give a verdict, `mr:merge` to merge.
30
31## Signed commits
32
33Squash and merge commits are signed by ermite's own key. The key is generated automatically the first time the server starts, belongs to the server, and its committer identity is `ERMITE_COMMITTER_NAME` / `ERMITE_COMMITTER_EMAIL`. `ermite signing-key show` prints its fingerprint and the `allowed_signers` line; use it to verify locally, or register the public key on GitHub or Forgejo.
34
35```sh
36ermite signing-key show --allowed-signers > allowed_signers
37git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commit <commit>
38```
39
40The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.
41
42## Mergeability
43
44The server checks in the background whether each open request merges with its target, and again whenever the source or the target moves. `mr show` prints it; the web merge box shows it as a live badge; `ermite watch` reports each change.
45
46| State | Meaning | Modes possible |
47|---|---|---|
48| `checking` | no result yet for the current branches | none (wait) |
49| `up_to_date` | the target is an ancestor of the head | all |
50| `behind` | the head lacks `behind` commits of the target (`behind_capped`: at least 1000) but merges without conflicts | `squash`, `merge` |
51| `conflict` | the merge conflicts; up to 20 `conflict_paths` are listed (`conflict_paths_truncated` when there are more); no paths means no common history | none (rebase) |
52
53```sh
54ermite mr show myrepo 1
55# mergeability behind by 2, merges without conflicts (squash or merge commit; fast-forward needs a rebase)
56```
57
58In `mr show --json`, `mergeability` holds `state`, `behind`, `behind_capped`, `conflict_paths`, `conflict_paths_truncated`, `target_id` and `head_id`; it is `null` unless the request is open. `ermite watch` prints `merge_request_mergeability` events when the state changes (not on every push):
59
60```text
612026-10-19T14:00:00Z merge_request_mergeability myrepo!1 mergeability: behind by 2
62```
63
64In the web merge box a mode that is not possible is disabled with its reason, and the Merge button too when no mode is possible. A merge the server refuses (target moved, conflict, branch moved) shows the page again with the reason in the box.
65
66## Large diffs on the web
67
68The merge request, commit and compare pages list every changed file. Generated files (lockfiles, `vendor/`, `node_modules/`, minified or generated code) and large files (over 2,000 changed lines) are collapsed: the header and the counts show, the lines do not. "load diff" fetches that one file in place, in full, and you can comment on its lines. Without scripts the link reloads the page with that file loaded. After the first 25 files of a merge request, the other files load as you scroll.
69
70A commit page reads the hunks of its first 300 files with the page; the other files are listed collapsed and load one at a time. A file that cannot be shown in the browser, over 8 MiB of text or over 50,000 changed lines when loaded alone, has no "load diff": the page links to its raw blob instead, and its lines take no comment.
71
72The API and `mr show --diff --json` keep their limits (300 files, 2,000 changed lines per file, 20,000 in total, 8 MiB). A file read without its hunks has `truncated` set and `omitted` giving the reason: `large`, `budget` (the earlier files used the limits) or `unavailable` (over 8 MiB of text, or a line over 64 KiB, even alone). Files past the 300th are not listed and `diff_truncated` is set.
73
74## Notes
75
76A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
77
78```sh
79ermite mr comment myrepo 1 -m "General remark"
80ermite mr comment myrepo 1 -m "Why?" --path main.go --line 10-14 --side new
81ermite mr comment myrepo 1 -m "Fixed in ^3" --reply 2
82ermite mr resolve myrepo 1 2
83ermite mr edit-comment myrepo 1 2 -m "New text"
84ermite mr delete-comment myrepo 1 2
85```
86
87- Notes are Markdown, up to 8000 characters, rendered without raw HTML. Quote with `> `.
88- `^N` in a note or a description links to note N of the same merge request. A reference to a missing note stays plain text.
89- A note is general, on one line, or on a range `A-B` of lines on one side (`old` or `new`) of the diff. The lines must be in the diff.
90- Threads have one level: a reply (`--reply <id>`) takes the anchor of its root and cannot carry a verdict. Resolving or reopening is done on the root and covers its replies.
91- Only the author edits or deletes a note. An edited note shows "edited". A deleted note loses its text; a deleted root with replies stays as "deleted note".
92
93## Verdicts
94
95A root note can carry a verdict: `--approve` or `--request-changes`. The text may then be empty. A verdict needs the `mr:review` scope (or the owner); `mr:write` comments but cannot give one.
96
97- A verdict ends a review: agents and `ermite watch` act on it, not on each note.
98- Verdicts are informative: none blocks or allows a merge.
99- `mr show` lists the latest verdict of each author (`verdicts` in `--json`).
100- An approval is stale once the branch head differs from the head it was given on. Changes requested stay current until their author gives a new verdict, whatever is pushed.
101- Verdicts are immutable: editing or deleting the note keeps its verdict. Only a new verdict of the same author replaces it.
102
103## Shared read position and history queries
104
105The read position is shared, not per principal. `MergeRequestDetail.HeadUnread`
106is a fact: the nonzero head differs from that position. The web presentation
107still decides who sees the mark-read button.
108
109Repository comparisons return at most `ports.MaxCompareCommits` (500) commits.
110Push summaries count at most `ports.MaxPushSummaryCommits` (100) commits and
111report whether more exist; ancestry, rather than the bounded list, detects a
112rewrite. Merge-request details include at most `ports.MaxMergeRequestCommits`
113(100) commits. These history queries remain on the merge-request service
114because it owns the merge-git port.