detached 1fb2421

merge-requests.md RawHistoryFolder

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]

Naming an issue with #N links it, and Fixes #N closes it on merge: see issues.

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.

A draft cannot merge. ermite mr ready <repo> <n> removes the WIP: or [WIP] title prefix; it needs mr:write and an open draft.

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.
  • A merge needs passing checks on the head: no failed, queued or running checks. A head with no checks can merge. --ignore-checks bypasses this, needs mr:force-merge, and cannot be combined with --auto or --cancel-auto. The owner web merge form offers Ignore checks when checks fail or are pending.
  • 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, mr:force-merge to bypass checks. The cli preset includes the bypass scope; agent does not. Adding --scope mr:merge lets an agent merge only with passing checks.

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.

Auto-merge

A merge request can merge by itself once its checks passed.

  • The repository setting allow_auto_merge is off by default: ermite repo edit <repo> --allow-auto-merge (=false to turn it off).
  • Enable it with ermite mr merge <repo> <n> --head <commit> --auto --mode squash (--head is required, as for a plain merge; a head that moved is refused with "branch moved"), or with Merge when ready in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs mr:merge and a server with CI. Re-enabling replaces the previous mode and enabler.
  • The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks. The mode must still be allowed.
  • The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow mr:merge on the repository and the target branch. A token that no longer qualifies cancels the auto-merge.
  • A pending auto-merge shows in mr show (auto_merge), in the web merge box and as merge_request_auto_merge_enabled in ermite watch. A merge request merged this way has auto_merged true.
  • A failing, unfinished or conflicting head waits with auto-merge still on. A re-run or a target fix can make it pass; pushing a new source head cancels the recorded intent (the reason names the new head).
  • A manual merge supersedes auto-merge and publishes merge_request_merged with auto=false, without a cancelled event. A manual close clears it and publishes merge_request_closed followed by merge_request_auto_merge_cancelled with reason closed.
  • Cancellation reasons are cancelled by <author>, closed, auto-merge was turned off in the repository settings, the <mode> merge mode is no longer allowed, the source branch was deleted, the branch moved to <short-head>, and the principal that enabled auto-merge can no longer merge <target>. If a change wins the merge lock after the evaluator's first read, the merge-time reason is instead auto-merge is off in <repo>, the <mode> merge mode is not allowed in <repo>, the source branch "<source>" no longer exists, or branch moved, read again: "<source>" is at <head>, not <recorded-head>. Other permanent merge-time preconditions report !<n> is <state>, !<n> is a draft: mark it ready for review first, the target branch "<target>" no longer exists, nothing to merge, "<target>" already is at <head>, or nothing to merge, "<target>" already contains <head>.
  • Cancel it with ermite mr merge <repo> <n> --cancel-auto or Cancel auto-merge in the web merge box.

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.

Review page panels

The main request card ends with a footer of two tabs, Commits and Checks, both collapsed by default. Clicking a tab opens its panel, clicking the open tab folds it again, clicking the other tab switches. Arrow keys (and Home / End) move between the tabs. Without scripts the tabs are anchor links and the URL fragment (#mr-commits, #mr-checks) shows the panel. The checks summary above the diff opens the Checks panel and continues to update live. Commit totals use N+ when the branch list is capped: the reader's existing count operation counts all reachable ancestors, not the source-minus-target range.

The rail keeps Files (including its l shortcut) and Verdicts. Merge settings are read-only information in the More (⋯) menu, available to every viewer. Signing-key information is also there when a merge box offers signed modes; merge actions remain restricted to their existing permissions.

Live source-branch updates

A push to the source branch refreshes the web review in place: the diff, files rail, commits/checks footer, diff range and merge controls follow the new head. The review keeps its visible file/line, folded files and open footer panel. The page request still reads only one 25-file window of hunks; the visible anchor's window is fetched too if needed, and other windows remain lazy.

While a comment is focused or has unsent edits, the refresh waits. A small “branch updated — refreshing after your comment” status explains the delay. Sending, cancelling, or reverting the draft and leaving its form lets the pending refresh proceed. Refresh never preserves or discards an unsent draft.

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
11Naming an issue with `#N` links it, and `Fixes #N` closes it on merge: see [issues](issues.md#merge-request-links).
12
13`ermite mr create` and `ermite mr close` print a confirmation followed by the full
14merge request web URL on its own line. With `--json`, merge request objects
15(create, show, list, close and merge) include `url` alongside the other fields.
16
17A draft cannot merge. `ermite mr ready <repo> <n>` removes the `WIP:` or `[WIP]` title prefix; it needs `mr:write` and an open draft.
18
19## Merge modes
20
21| Mode | Result | Needs |
22|---|---|---|
23| `fast-forward` (default) | the target moves to the head, no commit | the target is an ancestor of the head |
24| `squash` | one new commit with the merged tree, the target tip as only parent | the branch merges without conflicts |
25| `merge` | a merge commit with two parents, the target tip and the head | the branch merges without conflicts |
26
27- The server never rebases. A branch behind its target merges with `squash` or `merge`, or is rebased and pushed again for a fast-forward.
28- 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).
29- The merge is also refused when the branch moved since the reviewed `--head`.
30- A merge needs passing checks on the head: no failed, queued or running checks. A head with no checks can merge. `--ignore-checks` bypasses this, needs `mr:force-merge`, and cannot be combined with `--auto` or `--cancel-auto`. The owner web merge form offers **Ignore checks** when checks fail or are pending.
31- 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.
32- 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).
33- Scopes: `mr:write` to open and comment, `mr:review` to give a verdict, `mr:merge` to merge, `mr:force-merge` to bypass checks. The `cli` preset includes the bypass scope; `agent` does not. Adding `--scope mr:merge` lets an agent merge only with passing checks.
34
35## Signed commits
36
37Squash 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.
38
39```sh
40ermite signing-key show --allowed-signers > allowed_signers
41git -c gpg.format=ssh -c gpg.ssh.allowedSignersFile=allowed_signers verify-commit <commit>
42```
43
44The merge box of the web page and the output of `mr merge --mode squash|merge` repeat this with the fingerprint.
45
46## Auto-merge
47
48A merge request can merge by itself once its checks passed.
49
50- The repository setting `allow_auto_merge` is off by default: `ermite repo edit <repo> --allow-auto-merge` (`=false` to turn it off).
51- Enable it with `ermite mr merge <repo> <n> --head <commit> --auto --mode squash` (`--head` is required, as for a plain merge; a head that moved is refused with "branch moved"), or with **Merge when ready** in the web merge box. Its separate mode choice offers every repository-allowed mode, even while the head conflicts or is behind. Auto-merge keeps the source branch; the delete-source checkbox applies only to an immediate merge. It needs `mr:merge` and a server with CI. Re-enabling replaces the previous mode and enabler.
52- The server merges when CI has settled on the head with every run complete and at least one check passed. A failing or unfinished check always blocks. The mode must still be allowed.
53- The merge is made as the person who enabled it. Their token is checked at every evaluation, even while checks are pending, and again at merge time under the merge lock: it must still be valid and allow `mr:merge` on the repository and the target branch. A token that no longer qualifies cancels the auto-merge.
54- A pending auto-merge shows in `mr show` (`auto_merge`), in the web merge box and as `merge_request_auto_merge_enabled` in `ermite watch`. A merge request merged this way has `auto_merged` true.
55- A failing, unfinished or conflicting head waits with auto-merge still on. A re-run or a target fix can make it pass; pushing a new source head cancels the recorded intent (the reason names the new head).
56- A manual merge supersedes auto-merge and publishes `merge_request_merged` with `auto=false`, without a cancelled event. A manual close clears it and publishes `merge_request_closed` followed by `merge_request_auto_merge_cancelled` with reason `closed`.
57- Cancellation reasons are `cancelled by <author>`, `closed`, `auto-merge was turned off in the repository settings`, `the <mode> merge mode is no longer allowed`, `the source branch was deleted`, `the branch moved to <short-head>`, and `the principal that enabled auto-merge can no longer merge <target>`. If a change wins the merge lock after the evaluator's first read, the merge-time reason is instead `auto-merge is off in <repo>`, `the <mode> merge mode is not allowed in <repo>`, `the source branch "<source>" no longer exists`, or `branch moved, read again: "<source>" is at <head>, not <recorded-head>`. Other permanent merge-time preconditions report `!<n> is <state>`, `!<n> is a draft: mark it ready for review first`, `the target branch "<target>" no longer exists`, `nothing to merge, "<target>" already is at <head>`, or `nothing to merge, "<target>" already contains <head>`.
58- Cancel it with `ermite mr merge <repo> <n> --cancel-auto` or **Cancel auto-merge** in the web merge box.
59
60## Mergeability
61
62The 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.
63
64| State | Meaning | Modes possible |
65|---|---|---|
66| `checking` | no result yet for the current branches | none (wait) |
67| `up_to_date` | the target is an ancestor of the head | all |
68| `behind` | the head lacks `behind` commits of the target (`behind_capped`: at least 1000) but merges without conflicts | `squash`, `merge` |
69| `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) |
70
71```sh
72ermite mr show myrepo 1
73# mergeability  behind by 2, merges without conflicts (squash or merge commit; fast-forward needs a rebase)
74```
75
76In `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):
77
78```text
792026-10-19T14:00:00Z  merge_request_mergeability  myrepo!1 mergeability: behind by 2
80```
81
82In 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.
83
84## Large diffs on the web
85
86The 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.
87
88A 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.
89
90The 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.
91
92## Notes
93
94A note is the only review object. It is published as soon as it is written: there is no draft and no submit step.
95
96```sh
97ermite mr comment myrepo 1 -m "General remark"
98ermite mr comment myrepo 1 -m "Why?" --path main.go --line 10-14 --side new
99ermite mr comment myrepo 1 -m "Fixed in ^3" --reply 2
100ermite mr resolve myrepo 1 2
101ermite mr edit-comment myrepo 1 2 -m "New text"
102ermite mr delete-comment myrepo 1 2
103```
104
105- Notes are Markdown, up to 8000 characters, rendered without raw HTML. Quote with `> `.
106- `^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.
107- 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.
108- 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.
109- 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".
110
111## Verdicts
112
113A 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.
114
115- A verdict ends a review: agents and `ermite watch` act on it, not on each note.
116- Verdicts are informative: none blocks or allows a merge.
117- `mr show` lists the latest verdict of each author (`verdicts` in `--json`).
118- 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.
119- Verdicts are immutable: editing or deleting the note keeps its verdict. Only a new verdict of the same author replaces it.
120
121## Shared read position and history queries
122
123The read position is shared, not per principal. `MergeRequestDetail.HeadUnread`
124is a fact: the nonzero head differs from that position. The web presentation
125still decides who sees the mark-read button.
126
127Repository comparisons return at most `ports.MaxCompareCommits` (500) commits.
128Push summaries count at most `ports.MaxPushSummaryCommits` (100) commits and
129report whether more exist; ancestry, rather than the bounded list, detects a
130rewrite. Merge-request details include at most `ports.MaxMergeRequestCommits`
131(100) commits. These history queries remain on the merge-request service
132because it owns the merge-git port.
133
134### Review page panels
135
136The main request card ends with a footer of two tabs, Commits and Checks, both
137collapsed by default. Clicking a tab opens its panel, clicking the open tab
138folds it again, clicking the other tab switches. Arrow keys (and Home / End)
139move between the tabs. Without scripts the tabs are anchor links and the URL
140fragment (`#mr-commits`, `#mr-checks`) shows the panel. The checks summary above
141the diff opens the Checks panel and continues to update live. Commit totals use
142`N+` when the branch list is capped: the reader's existing count operation counts all reachable
143ancestors, not the source-minus-target range.
144
145The rail keeps Files (including its `l` shortcut) and Verdicts. Merge settings
146are read-only information in the More (⋯) menu, available to every viewer.
147Signing-key information is also there when a merge box offers signed modes;
148merge actions remain restricted to their existing permissions.
149
150## Live source-branch updates
151
152A push to the source branch refreshes the web review in place: the diff, files
153rail, commits/checks footer, diff range and merge controls follow the new head.
154The review keeps its visible file/line, folded files and open footer panel.
155The page request still reads only one 25-file window of hunks; the visible
156anchor's window is fetched too if needed, and other windows remain lazy.
157
158While a comment is focused or has unsent edits, the refresh waits. A small
159“branch updated — refreshing after your comment” status explains the delay.
160Sending, cancelling, or reverting the draft and leaving its form lets the
161pending refresh proceed. Refresh never preserves or discards an unsent draft.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close