Diffs and comparisons
Overview
See exactly what changed. You can read what a single commit introduced, what a merge request would land, what separates any two branches, every file a change touches in one response, and — for a file the diff folded away or one too large to send whole — that one file's full patch or a window of its lines. Every answer is computed from git at the moment you ask, so a diff is the repository as it stands rather than a stored summary that can quietly fall out of date.
All eight reads are read-only and share one token scope, which makes this the surface to point a review tool, a bot or a CI script at.
Before you start
- Read access to the repository.Diffs follow the repository's visibility exactly: a public repository answers anyone, including a caller with no credentials at all; an internal one answers anyone signed in; a private one answers only its members, at Reporter or above.
- A token carrying
diff:read, if you are calling the API or the CLI rather than clicking. All eight reads need that one scope and nothing more, so a token minted for reading diffs can do all of this and none of your writes. - The right kind of identifier.This is where most first attempts fail, because the eight reads deliberately do not all take the same thing:
| To read | You pass |
|---|---|
| A commit's changes | a commit SHA, 7 to 40 hex characters — an abbreviated SHA is fine, a branch name is not |
| A merge request's changes | the merge request's number, the one in its URL |
| A comparison | two branch names — a tag or a raw SHA is refused |
| A stack | the number of the merge request at the top of the chain |
| A page of a diff you already have the commit pair for | two full 40-character SHAs |
| Every file a change touches | two full 40-character SHAs |
| One file's patch | two full 40-character SHAs and a file path |
| A window of a file's lines | one full 40-character SHA, a file path and a starting line number |
The last four take their SHAs from whichever diff you are already reading, and it matters which pair you copy. A commit diff and a merge request diff carry base_sha and head_sha at the top level. A comparison and a stack diff nest theirs under diff, and it is diff.base_sha and diff.head_sha you want — a comparison's own top-level base_sha is the base branch's tip, which is a different commit and gives you a different diff. Passing the right pair back is what keeps everything you read afterwards — more pages, the file list, a file you expand — consistent with the change you were looking at, even if somebody pushes to the branch while you read.
How it works
What each diff is taken against
| Read | Starts at | Ends at |
|---|---|---|
| A commit | its first parent — or, for the very first commit in a repository, the empty tree | the commit itself |
| A merge request | the merge base of its target and source branches | the source branch tip |
| A comparison | the merge base of base and head | the head branch tip |
| A stack | the merge base of the stack's bottom target branch and the top merge request's source | that source branch tip |
| A page or the file list, read by a commit pair | the base you pass | the head you pass |
The merge request, comparison and stack reads are three-dot: they start where the two branches last agreed, not at the target's current tip. Each one's base_sha is that starting point, so reading the same diff again by its commit pair gives the same answer.
o---o---o feature (head) /o---o---o---A---o---o---o main (base) ^ merge base — where a three-dot diff startsThat is the difference between a review that shows your four commits and one that also shows the fifteen that landed on the default branch while you were working. It also means a diff never grows because somebody else merged something: only a push to your own branch changes it.
The cost of three-dot is the one thing it hides — work that is on the base and not on your branch. A comparison reports that separately as behind, and a merge request reports it in its merge check.
Stats always, patches on request
Every batch read returns two things: aggregate counts for the whole change, and one alphabetically ordered page of files. Patch text is off by default, because the counts are what most callers want and the patch bodies are nearly all of the bytes. Turn patches on with include_patch, and move through the file list with page and limit.
The numbers in stats describe the entire change, not the page you are holding — so a page of 20 files can sit under a files_changed of 300 without either being wrong. total_files counts the same set, and has_more says whether another page exists. Asking for a page past the end is not an error: it returns an empty files list. On a comparison and on a stack diff every one of those lives under diff; the divergence counts and the commit preview belong to the comparison itself.
The whole file list in one read
Paging is for patches. When what you want is the list — every file the change touches, with its status and its counts, to draw a file tree or to find one file among thousands — ask for the file list instead. It returns every changed file in path order in one response, with no patch text, and the same whole-change stats as a page.
The list is cheap because it is the part of a diff that was already computed: the counts come from the same single pass that serves every page. A few thousand files is a few hundred kilobytes of JSON and far less on the wire, where it is compressed. So the pattern for a large change is one read for the list, then patches only for the files the reader actually reaches — rather than paging through every file to discover what is there.
A renamed file carries its old_path in the list. Pass it along when you open that file on its own, so its diff reads as the rename it is.
Folded files and files that are simply too big
Two different things can leave a file without patch text, and they want different responses from you.
| Flag | What happened | How you get the content |
|---|---|---|
collapsed: true | The patch was withheld on purpose, to keep the response small. collapse_reason is generated or large. Only ever set on a read that asked for patches | Fetch that one file's diff on demand — it comes back force-expanded |
patch_truncated: true | The patch is past the hard per-file ceiling. Nothing expands it in place | Read the file in line windows, a chunk at a time |
generated: true | The file looks machine-written. Reported always, independent of whether it was folded | Nothing to do — it is there so you can badge or fold it yourself |
Your repository decides what counts as generated. A linguist-generated attribute in .gitattributes, read at the head of the change, is authoritative in both directions: it marks files the built-in list would miss, and linguist-generated=false forces a lock file to be shown in full. Only paths the attribute says nothing about fall back to the built-in list — dependency lock files by name (Cargo.lock, package-lock.json, yarn.lock, go.sum, Gemfile.lock, poetry.lock and their siblings) and .min.js, .min.css, .map and .snap by suffix. The attribute is read only on a request that is folding patches; a stats-only read reports generated from the built-in list alone.
Folding never costs you accuracy: it is decided from the file's path and its changed-line counts before any patch is produced, so a folded file's additions and deletions are the real ones. Send collapse=false when you want every patch on the page regardless.
Diffs outlive branches
A merge request carries the two branch tips it was opened at, and refreshes them when it merges. Once it has merged, or once its source branch is deleted, its diff is taken between those recorded commits rather than between live branch names — so a merged merge request keeps showing exactly what landed, long after the branch is gone.
A merged merge request's commits stay reachable from the branch it merged into, so that diff keeps working indefinitely. The one case that decays is a closed merge request whose source branch was deleted: nothing on any branch reaches those commits any more, and repository housekeeping eventually reclaims them. Compare the two branches instead, while both still exist.
While a merge request is open and its source branch still exists, the diff is taken from live branch tips — so it follows every push to the source branch.
Re-reading is nearly free
Every read returns an ETag. Send it back on the next request as If-None-Match and an unchanged answer comes back as 304 Not Modified with no body at all. The validator covers the resolved commit pair and the options you sent, so switching include_patch or turning a page gets you fresh content rather than a stale 304. The check runs after the access check, so a 304 never tells an unauthorized caller that something exists.
How to
Read what a commit changed
A commit diff is taken against the commit's first parent, so for a merge commit you get what the merge introduced relative to its first side. Start with the counts, then ask for patches once you know how big the change is.
Open the repository, choose Commits, and select the commit you want. The commit page opens on its changes: a summary bar with files changed, additions and deletions, then one expandable panel per file. Files the platform folded are shown collapsed with their counts and a Load diff control; Expand all opens every one of them.
gitmoap diffs get-commit-diff --repo-id <id> --commit-ref <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/commits/{commit_ref}/diff \ -H "Authorization: Bearer $GITMOAP_TOKEN"If the response comes back with truncated: true, the commit touched more files than one diff can carry — see Limits.
Read what a merge request changes
This is the review view: only what the source branch introduces, measured from the point it diverged from its target.
Open the merge request and choose the Changes tab. The file tree on the left lists every changed file with its counts; selecting one scrolls to its patch. The header shows the two commits the diff was taken between, and updates when somebody pushes to the source branch.
gitmoap diffs get-mr-diff --repo-id <id> --mr-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/merge-requests/{mr_id}/diff \ -H "Authorization: Bearer $GITMOAP_TOKEN"For a merge request that is part of a stack, this shows only that one merge request's own layer. Use the stack diff below for everything underneath it as well.
Compare two branches
A comparison answers two questions at once: how far the branches have diverged (ahead and behind), and what the head branch introduces. Both base and head are required and both must be branch names.
Open the repository and choose Compare. Pick the base branch on the left and the head branch on the right; the page reloads with the divergence counts, the commits the head branch adds, and the file diff underneath. Swap the two selectors to look at the divergence from the other direction.
gitmoap diffs compare-refs --repo-id <id> --base <base> --head <head>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/compare \ -H "Authorization: Bearer $GITMOAP_TOKEN"Direction matters. base is where you would merge to and head is where the work is, exactly as in a merge request — so comparing main against feature shows the feature's work, and the other way round shows nothing but what main has moved on by.
The commit list in the response is a preview rather than the whole history. When commits_truncated is true, ahead and total_commits still hold the real number — only the list was cut. There is no cursor for the rest of it: read the merge request's own commit listing if a merge request exists for this pair — that one is a merge request read, so a token needs merge_request:read as well — or run git log base..head in a clone.
Read a whole stack as one change
When merge requests are stacked, each one targets the branch below it, so each one's own diff shows only its layer. A stack diff rolls up the whole chain from the stack's base branch to the merge request you anchor on — exactly the change that merging the chain would apply.
Open the top merge request of the stack and choose Stack. The chain is drawn base-first with the anchor highlighted; Combined changes switches the diff below from this merge request's own layer to the cumulative change of everything from the base up to it.
gitmoap diffs get-stack-diff --repo-id <id> --mr-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/merge-requests/{mr_id}/stack/diff \ -H "Authorization: Bearer $GITMOAP_TOKEN"member_mr_ids names the merge requests that were rolled in, base-first with the anchor last, so you can tell at a glance how much of the chain you are reading. The chain above the anchor is deliberately excluded: it branches, and there is no single cumulative answer for a tree.
Expand a file the diff folded away
The single-file read is the other half of folding. Give it the commit pair the batch diff was built from — base_sha and head_sha on a commit or merge request diff, diff.base_sha and diff.head_sha on a comparison or a stack diff — plus the file's path exactly as the diff spelled it. It returns that one file force-expanded: folding does not apply, and collapsed comes back false.
For a renamed file, pass the diff's old_path as old_path too. Without it the read is given only the new path, so it cannot see where the file came from and answers with the whole file as added.
In any diff, a folded file shows its counts and the reason it was folded. Choose Load diff on that file to fetch its patch and open it in place. The rest of the page is untouched, so you keep your position.
gitmoap diffs get-diff-file \ --repo-id <id> \ --base <base> \ --head <head> \ --path <path>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/diff-file \ -H "Authorization: Bearer $GITMOAP_TOKEN"Widen the surrounding context with context, which accepts 0 to 100 lines and defaults to 3. A file that is over the per-file ceiling even when asked for directly comes back with patch_truncated: true — read that one in line windows instead.
Read the lines around a change
A patch shows the changed lines and a few either side. When you need more — the rest of the function, the top of the file, or a file too large to patch at all — read a bounded window of lines at a commit. Nothing is diffed: you get the file as it stands at that commit.
In an open patch, the chevrons in the gutter between two hunks fetch the lines in the gap; the ones at the top and bottom of the file walk outwards from the first and last hunk. Each activation pulls the next block, so a long gap fills in a few steps.
gitmoap diffs get-file-lines \ --repo-id <id> \ --path <path> \ --ref <ref> \ --start <n>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/file-lines \ -H "Authorization: Bearer $GITMOAP_TOKEN"Read the before side at the diff's base_sha and the after side at its head_sha — a file the change added exists only at the head, and one it deleted only at the base. has_more_before and has_more_after tell you whether another window exists in each direction, so you can walk a file without knowing its length up front.
List every file a change touches
Take the commit pair from the first page of the diff you opened — base_sha and head_sha, or diff.base_sha and diff.head_sha on a comparison or a stack — and ask for its file list. Every changed file comes back in one response, in the same order the pages use.
Open a merge request's Changes tab, or any commit or comparison. The file tree on the left lists every file the change touches with its additions and deletions, however many there are, and selecting one scrolls to it. The first files arrive with their patches; the rest show their counts and a Load diff control that fetches that file's patch in place.
gitmoap diffs get-diff-files --repo-id <id> --base <base> --head <head>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/diff-files \ -H "Authorization: Bearer $GITMOAP_TOKEN"If truncated is true, the change touches more files than one list carries — see Limits. stats still counts every one of them.
Page through a diff by its commit pair
Once you know the pair, read further pages of the diff by the pair itself rather than by the merge request or the branches. The page is the same one the merge request would return — same page, limit, include_patch and collapse — but it cannot move: a push to the branch while you page does not change what you read.
The web app has no separate control for this. It reads a diff's first page through the merge request, commit or comparison, the rest of the file list in one read, and each further file's patch as you open it — every one of those after the first by the same commit pair. Page by the pair from the CLI or the API when you want further pages with patches.
gitmoap diffs get-diff --repo-id <id> --base <base> --head <head>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/diff \ -H "Authorization: Bearer $GITMOAP_TOKEN"Re-check a diff without downloading it again
Keep the ETag from any of the eight reads and send it back as If-None-Match. An answer that has not changed costs one header exchange and no body, which is what makes polling a merge request's diff every few seconds reasonable. Every option that changes the bytes is folded into the validator, so you never get a stale 304: on a batch read, including a page read by its commit pair, that means page, limit, include_patch and collapse; on the file list it is the pair alone; on a single-file diff it means the path, old_path and context; on a line window it means the path and the window you land on after clamping.
Reference
8 endpoints
GET/repositories/{repo_id}/commits/{commit_ref}/diffdiff:read
Get diff for a commit
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
- commit_refstringCommit SHA (7-40 hex chars)
Query
- pageintegeroptionalPage number (0-based). Default: 0
- limitintegeroptionalFiles per page (1-100). Default: 20
- include_patchbooleanoptionalInclude unified diff patch content. Default: false
- collapsebooleanoptionalAuto-collapse generated/oversized file patches (fetch them via /diff-file). Default: true
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or commit not found
Receive
- base_shastring
files[]object11 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
- old_pathobjectoptional
- patchobjectoptional
- patch_truncatedbooleanThe patch exceeds the hard per-file size cap and is omitted. Unlike
collapsed, this file is not expandable in place — a client reads it piece-by-piece viaGET .../file-linesinstead. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- has_morebooleanAnother page follows this one.
- head_shastring
- pageintegerThe page returned, from 0.
statsobject3 fields
- additionsintegerLines added across the whole change.
- deletionsintegerLines deleted across the whole change.
- files_changedintegerEvery file the change touches.
- total_filesintegerHow many files the pages can reach — every changed file, unless
truncated. - truncatedbooleanThe change touches more files than any page can reach.
statsstill counts all of them.
GET/repositories/{repo_id}/merge-requests/{mr_id}/diffdiff:read
Get diff for a merge request
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
- mr_idint64Merge Request ID
Query
- pageintegeroptionalPage number (0-based). Default: 0
- limitintegeroptionalFiles per page (1-100). Default: 20
- include_patchbooleanoptionalInclude unified diff patch content. Default: false
- collapsebooleanoptionalAuto-collapse generated/oversized file patches (fetch them via /diff-file). Default: true
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or merge request not found
Receive
The same shape get_commit_diff returns — 8 fields. Go to it
Show it here
- base_shastring
files[]object11 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
- old_pathobjectoptional
- patchobjectoptional
- patch_truncatedbooleanThe patch exceeds the hard per-file size cap and is omitted. Unlike
collapsed, this file is not expandable in place — a client reads it piece-by-piece viaGET .../file-linesinstead. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- has_morebooleanAnother page follows this one.
- head_shastring
- pageintegerThe page returned, from 0.
statsobject3 fields
- additionsintegerLines added across the whole change.
- deletionsintegerLines deleted across the whole change.
- files_changedintegerEvery file the change touches.
- total_filesintegerHow many files the pages can reach — every changed file, unless
truncated. - truncatedbooleanThe change touches more files than any page can reach.
statsstill counts all of them.
GET/repositories/{repo_id}/comparediff:read
Compare two branches
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
Query
- basestringBase (target) branch. Required.
- headstringHead (source) branch. Required.
- pageintegeroptionalFile-diff page number (0-based). Default: 0
- limitintegeroptionalFiles per page (1-100). Default: 20
- include_patchbooleanoptionalInclude unified diff patch content. Default: false
- collapsebooleanoptionalAuto-collapse generated/oversized file patches (fetch them via /diff-file). Default: true
Refuses
400Missing or invalid base/head branch401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or branch not found
Receive
- aheadint32Commits on head not on base.
- base_shastring
- behindint32Commits on base not on head.
commits[]object13 fields
- author_avatar_urlobjectoptional
- author_emailstring
- author_namestring
- authored_atstring
- commit_shastring
- committed_atstring
- committer_avatar_urlobjectoptional
- committer_emailstring
- committer_namestring
- messageobjectoptional
- repository_iduuid
- short_shastring
- titlestring
- commits_truncatedboolean
truewhen there are more commits than the preview cap returned. diffobject7 fields
- base_shastring
files[]object6 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
GET/repositories/{repo_id}/merge-requests/{mr_id}/stack/diffdiff:read
Cumulative diff of a merge request's downstack
get_stack_diffGET /repositories/{repo_id}/merge-requests/{mr_id}/stack/diffOpen in the API referenceToken scope
diff:read
Send
Path
- repo_iduuidRepository ID
- mr_idint64Anchor merge request ID (top of the rolled-up chain)
Query
- pageintegeroptionalPage number (0-based). Default: 0
- limitintegeroptionalFiles per page (1-100). Default: 20
- include_patchbooleanoptionalInclude unified diff patch content. Default: false
- collapsebooleanoptionalAuto-collapse generated/oversized file patches (fetch them via /diff-file). Default: true
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or merge request not found
Receive
- anchor_mr_idint64
- base_branchstring
diffobject20 fields
- base_shastring
files[]object11 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
- old_pathobjectoptional
- patchobjectoptional
- patch_truncatedbooleanThe patch exceeds the hard per-file size cap and is omitted. Unlike
collapsed, this file is not expandable in place — a client reads it piece-by-piece viaGET .../file-linesinstead. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- has_morebooleanAnother page follows this one.
- head_shastring
- pageintegerThe page returned, from 0.
statsobject3 fields
- additionsintegerLines added across the whole change.
- deletionsintegerLines deleted across the whole change.
- files_changedintegerEvery file the change touches.
- total_filesintegerHow many files the pages can reach — every changed file, unless
truncated. - truncatedbooleanThe change touches more files than any page can reach.
statsstill counts all of them.
- repository_iduuid
GET/repositories/{repo_id}/diffdiff:read
Get a page of the diff between two commits
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
Query
- basestringBase commit SHA — a diff's base_sha. Required.
- headstringHead commit SHA — a diff's head_sha. Required.
- pageintegeroptionalPage number (0-based). Default: 0
- limitintegeroptionalFiles per page (1-100). Default: 20
- include_patchbooleanoptionalInclude unified diff patch content. Default: false
- collapsebooleanoptionalAuto-collapse generated/oversized file patches (fetch them via /diff-file). Default: true
Refuses
400Missing or invalid base/head401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found, or a commit that is not in it
Receive
The same shape get_commit_diff returns — 8 fields. Go to it
Show it here
- base_shastring
files[]object11 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
- old_pathobjectoptional
- patchobjectoptional
- patch_truncatedbooleanThe patch exceeds the hard per-file size cap and is omitted. Unlike
collapsed, this file is not expandable in place — a client reads it piece-by-piece viaGET .../file-linesinstead. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- has_morebooleanAnother page follows this one.
- head_shastring
- pageintegerThe page returned, from 0.
statsobject3 fields
- additionsintegerLines added across the whole change.
- deletionsintegerLines deleted across the whole change.
- files_changedintegerEvery file the change touches.
- total_filesintegerHow many files the pages can reach — every changed file, unless
truncated. - truncatedbooleanThe change touches more files than any page can reach.
statsstill counts all of them.
GET/repositories/{repo_id}/diff-filesdiff:read
List every file a diff between two commits changes, without patches
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
Query
- basestringBase commit SHA — a diff's base_sha. Required.
- headstringHead commit SHA — a diff's head_sha. Required.
Refuses
400Missing or invalid base/head401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found, or a commit that is not in it
Receive
- base_shastring
files[]object7 fields
- additionsinteger
- deletionsinteger
- generatedbooleanLooks generated — a lock file, a minified bundle, a snapshot — judged from the path. A page
read with patches also honours the repository's
.gitattributes. - is_binarybooleanA binary file: it has no line counts and no patch.
- old_pathobjectoptionalThe file's previous path, for a rename or copy. Pass it as
old_pathwhen reading the file's own diff, or a renamed file reads as the whole file added. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- head_shastring
statsobject3 fields
- additionsintegerLines added across the whole change.
- deletionsintegerLines deleted across the whole change.
- files_changedintegerEvery file the change touches.
- truncatedbooleanThe change touches more files than are listed.
statsstill counts all of them.
GET/repositories/{repo_id}/diff-filediff:read
Fetch a single file's diff on demand, force-expanded
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
Query
- basestringBase commit SHA (use the batch diff's diff.base_sha). Required.
- headstringHead commit SHA (use the batch diff's diff.head_sha). Required.
- pathstringFile path within the repository. Required.
- old_pathstringoptionalThe file's previous path, when the diff reports it renamed (its old_path). Without it a renamed file reads as the whole file added.
- contextintegeroptionalUnified context lines (0-100). Default: 3
Refuses
400Missing or invalid base/head/path401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository not found, or file unchanged between the refs
Receive
- base_shastring
fileobject11 fields
- additionsinteger
- collapse_reasonobjectoptional
- collapsedbooleanThe patch was intentionally withheld (generated or oversized) to save
bytes but is fetchable in full on demand via
GET .../diff-file. Only ever set when patches were requested (include_patch=true). - deletionsinteger
- generatedbooleanDetected generated/lock/minified file. Always computed from the path,
independent of
collapsed, so a client can badge or auto-fold it. - is_binaryboolean
- old_pathobjectoptional
- patchobjectoptional
- patch_truncatedbooleanThe patch exceeds the hard per-file size cap and is omitted. Unlike
collapsed, this file is not expandable in place — a client reads it piece-by-piece viaGET .../file-linesinstead. - pathstring
- statusaddedcopieddeletedmodifiedrenamed
- head_shastring
GET/repositories/{repo_id}/file-linesdiff:read
Read a bounded window of a file's lines at a ref
Token scope
diff:read
Send
Path
- repo_iduuidRepository ID
Query
- refstringCommit SHA to read the file at (e.g. the batch diff's diff.head_sha). Required.
- pathstringFile path within the repository. Required.
- startintegerFirst line to return (1-based). Required.
- endintegeroptionalLast line to return (inclusive). Defaults to a 500-line window; clamped so no request exceeds 500 lines.
Refuses
400Missing or invalid ref/path/start401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or file not found
Receive
- endinteger1-based line number of the last returned line (inclusive).
- has_more_afterbooleanA line exists after
end. - has_more_beforebooleanA line exists before
start(i.e.start > 1). - is_binarybooleanThe file is binary;
linesis empty (line windows are text-only). - pathstring
- refstring
- startinteger1-based line number of the first returned line.
Limits
Diffs are bounded so that no single request can be unboundedly large. Every bound below announces itself in the response rather than failing, with one exception, and each one has a way through.
| What | The bound | What you see at it |
|---|---|---|
| Files in one page | 1 to 100, 20 by default | A value outside the range is clamped, not refused |
| Files in one change | 20,000 | truncated: true, and files past the first 20,000 in path order are left out of every page and of the file list. stats still counts every file |
| Patch text for one file | 100 KiB | patch_truncated: true and no patch body — read that file in line windows |
| Changed lines before a file is folded | 500 in the file | collapsed: true with collapse_reason: "large" |
| Changed lines before the rest of a page is folded | 5000 across the page | The same, for every later file whose changes would push past the budget. A small file that still fits is left alone |
| Lines in one window | 500 | A wider request is clamped; has_more_after tells you to ask again |
| File size a line window can read | 1 MiB | 400 BAD_REQUEST naming the file's size — the one bound that refuses rather than trims |
| Context lines around a hunk | 0 to 100, 3 by default | Clamped |
| Commits previewed in a comparison | 100 | commits_truncated: true; ahead and total_commits still count every one of them |
| Merge requests rolled into one stack diff | 20 below the anchor | The walk stops there and the diff covers what it reached |
One more bound is not a number on this page, because it depends on your plan: the bytes you pull back. The batch reads — a commit diff, a merge request diff, a comparison, a stack diff and a page read by its commit pair — and the file list are metered against a per-hour and per-day outbound-data budget for you and for your namespace, and answer 429 TOO_MANY_REQUESTS once one is exhausted. Expanding a single file and reading a line window are not metered, which is the practical argument for reading stats first and expanding only what you open.
Every other number above is the same on every plan — none of them is a paid ceiling. Reading a diff creates nothing, so none of these reads counts against the quotas that cap how much you can create. For a file above the 1 MiB ceiling, fetch it with git from a clone; the ceiling applies to every file the API serves, and it is the same one a blob read enforces.
Troubleshooting
400 BAD_REQUESTmentioning repo_id
- Cause
- The repository in the URL is not a well-formed id — usually a
group/repopath pasted where an id belongs - What to do
- Use the repository's id. You can read it from any repository response, or look it up by path
400 BAD_REQUESTon a commit diff, mentioning commit_ref
- Cause
- You passed a branch or tag name; a commit diff takes 7 to 40 hex characters
- What to do
- Resolve the branch to a commit first — its head SHA is on the branch listing — then pass that
400 BAD_REQUEST— Missing required query parameter 'base' (or 'head')
- Cause
- A comparison needs both branches and has no default for either
- What to do
- Send both.
baseis what you would merge into,headis where the work is
400 BAD_REQUEST— branch_name: … on a comparison
- Cause
- The value is not a usable branch name — a space, a
.., a~, a^, a:or a leading- - What to do
- Pass the name as the branch listing spells it. If you meant a commit rather than a branch, a comparison cannot take one
400 BAD_REQUEST— commit_hash: must be exactly 40 characters
- Cause
base,headorrefwas a branch name or an abbreviated SHA. Those three take a full 40-character SHA, so the message names the type rather than the parameter- What to do
- Copy the SHA pair straight out of the batch diff you are expanding, rather than retyping a short SHA from a commit page
400 BAD_REQUEST— Missing or invalid required query parameter 'start'
- Cause
startwas left out, or was not a whole number- What to do
- Pass the line number the patch showed you, or
start=1for the top of the file. Line numbers are 1-based, and a0is read as1
400 BAD_REQUEST— File too large
- Cause
- The file is over the 1 MiB ceiling for a line window
- What to do
- Fetch that file with
gitfrom a clone. The ceiling is the same for every file read over the API
400 BAD_REQUESTon a merge request or stack diff, mentioning mr_id
- Cause
- The merge request in the URL is not a number — usually a title slug, or the
!copied along with the digits - What to do
- Pass the digits alone. They are the last segment of the merge request's URL, and they are per repository
401 UNAUTHORIZEDon a repository you can see in a browser
- Cause
- No credential was sent, or the one sent has expired. Only public repositories answer anonymously
- What to do
- Sign in, or send a valid token. If a script broke overnight, the token expired rather than the endpoint changing
403 INSUFFICIENT_SCOPE
- Cause
- Your token is valid but was minted without
diff:read. The body repeats the scope it wanted inrequired_scope - What to do
- Mint a new token carrying
diff:read. Scopes are fixed when a token is created and cannot be added afterwards
403 FORBIDDEN— Access token is not authorized for this resource
- Cause
- The token is bound to a different repository or group subtree
- What to do
- Use a token bound to this repository, or one with no resource binding
404 NOT_FOUNDon a repository you are sure exists
- Cause
- It is private and you are not a member. A private repository answers
404rather than403so that its existence stays private - What to do
- Check your membership first, then the id. A wrong id gives the same answer, on purpose
404 NOT_FOUNDon a commit diff
- Cause
- The commit is not in this repository — never pushed, pushed to a fork, or the repository id is wrong
- What to do
- Push the commit, or check you are asking the repository that has it
404 NOT_FOUNDon a comparison
- Cause
- One of the two branches does not exist — deleted, misspelled, or a tag rather than a branch
- What to do
- List the repository's branches and check both names. A comparison takes branches only
404 NOT_FOUNDon a merge request diff or a stack diff
- Cause
- The number belongs to no merge request in this repository
- What to do
- Check the number against the merge request's URL — numbers are per repository, so one from another repository will not resolve
404 NOT_FOUNDfrom a single-file diff, for a file you can see in the repository
- Cause
- That path is unchanged between the two SHAs. The single-file read only serves files the diff actually touched
- What to do
- Copy the path exactly as the diff spelled it, including its new name after a rename. To read a file the change did not touch, use a line window instead
404 NOT_FOUNDfrom a line window
- Cause
- The path does not exist at that commit — asking at the head for a file the change deleted, or at the base for one it added
- What to do
- Read a deleted file at
base_shaand an added file athead_sha
429 TOO_MANY_REQUESTSnaming an outbound-data limit
- Cause
- You have pulled more bytes this hour or this day than the budget allows. The message says which budget tripped, yours or your namespace's
- What to do
- Wait for the window to roll over, or pull less: read stats without patches and expand only the files you actually open. An anonymous caller gets the smallest budget there is, so sign in
304 Not Modified and an empty body
- Cause
- You sent
If-None-Matchand nothing changed - What to do
- Use the copy you already have. Drop the header to force a full body
You asked for patches and some files came back without one
- Cause
- Those files were folded, or their patch is past the per-file ceiling
- What to do
- Check
collapsedandcollapse_reasonand expand those files on demand; apatch_truncatedfile is read in line windows. Sendcollapse=falseto stop folding altogether
Fewer files came back than total_files reports
- Cause
- You are holding one page.
statsdescribes the whole change,filesdescribes the page - What to do
- Turn the page, or raise
limit— 100 files is the maximum in one request
files is empty on a change you know has files
- Cause
- You asked for a page past the end. Paging past the last page returns an empty list rather than an error
- What to do
- Start at
page=0and followhas_more, or dividetotal_filesby yourlimitto find the last page
truncated: true, and files you expected are absent
- Cause
- The change touches more than 20,000 files
- What to do
- Nothing recovers the tail of a single diff. Compare a narrower pair of points, or read the change commit by commit
A renamed file's single-file diff shows the whole file as added
- Cause
- The read was given the new path only, so it never saw the old one
- What to do
- Pass the diff's
old_pathfor that file asold_path
404 NOT_FOUNDfrom a page read by its commit pair, the file list or a single-file diff
- Cause
- One of the two SHAs names no commit in this repository — mistyped, copied from another repository, or reclaimed after a force-push
- What to do
- Take the pair from a diff you have just read in this repository
ahead is greater than zero but the diff is empty
- Cause
- The commits cancel out — a change and its revert, or work already applied to the base by another route
- What to do
- Nothing is wrong and there is nothing to review. The commit list still shows what happened
The comparison is empty and you expected the other branch's work
- Cause
baseandheadare the wrong way round- What to do
- Swap them.
behindis the count of whatbasehas thatheaddoes not
merge_base_sha is absent from a comparison
- Cause
- The two branches share no common ancestor at all
- What to do
- Expect a straight comparison against the base tip. This is normal for a branch imported from elsewhere
A rename shows up as one deletion and one addition
- Cause
- The file changed too much alongside the rename to be recognised as one. In a very large change, a file whose name and content both changed is not matched at all — a moved file that kept its name, or its content, still is
- What to do
- Read the pair together. Committing a rename separately from its edits keeps it detectable next time
A binary file reports 0 additions and 0 deletions
- Cause
- It is binary; line counts have no meaning and
is_binaryistrue. A binary file is never folded either - What to do
- Download the file itself at either commit if you need to inspect it
A line window came back with lines empty
- Cause
- Either the file is binary — line windows are text-only, and
is_binaryistrue— orstartis past the last line, in which caseendcomes back one belowstart - What to do
- Download a binary file rather than windowing it. For a text file, walk back down:
has_more_beforetells you a line exists abovestart
The diff changed between two page requests
- Cause
- Someone pushed to the branch while you were paging through the merge request or the branches, which follow the branch
- What to do
- Re-read page 0, then read every later page, the file list and every expansion by the
base_shaandhead_shait returns — those reads cannot move
A stack diff shows far more than the merge request's own change
- Cause
- That is what it is for — it rolls up every merge request from the stack base up to the anchor
- What to do
- Use the merge request's own diff for just its layer, and the stack diff for what merging the chain would apply
Examples
Reading a merge request the way a reviewer does, one file at a time, without pulling the whole change at once:
# 1. The shape of the change: counts only, no patch bodies.gitmoap diffs get-mr-diff --repo-id <your-repo-id> --mr-id 42 # 2. The first page of files, with patches. --include-patch takes a value, not a bare flag.gitmoap diffs get-mr-diff --repo-id <your-repo-id> --mr-id 42 --include-patch true --limit 20 # 3. Every file the change touches, in one response. The two SHAs come from the response above.gitmoap diffs get-diff-files --repo-id <your-repo-id> --base <base_sha> --head <head_sha> # 4. The second page of patches, read by the pair so it cannot move under you.gitmoap diffs get-diff --repo-id <your-repo-id> --base <base_sha> --head <head_sha> --page 1 --limit 20 --include-patch true # 5. One folded file, in full.gitmoap diffs get-diff-file --repo-id <your-repo-id> --base <base_sha> --head <head_sha> --path Cargo.lock # 6. More of the file around a hunk that starts at line 120.gitmoap diffs get-file-lines --repo-id <your-repo-id> --ref <head_sha> --path src/main.rs --start 60 --end 200<your-repo-id> is the repository's id; <base_sha> and <head_sha> are the two values step 2 returned. Steps 3 to 6 stay consistent with step 2 no matter what lands on the branch in between, because they name commits rather than branches.
FAQ
Why does a merge request diff not show a commit that is definitely on the target branch?
Because it is measured from the point the branches diverged, not from the target's tip. Work that landed on the target after you branched is not part of what your merge request introduces. Your merge check reports how far behind you are separately.
Can I compare a tag, or two arbitrary commits?
A comparison takes branch names on both sides. For two arbitrary commits, read the diff by its commit pair: the page read and the file list take two full SHAs, as do the single-file read and the line window. Unlike a comparison, that diff is two-dot — it starts at the first commit exactly, not at a merge base.
Why are the additions and deletions on a folded file still filled in?
Folding is decided from the file's path and its line counts before any patch text is produced. You get the real numbers and the option to fetch the body, which is the point: a lock file's size is worth knowing and its contents almost never are.
Does reading a diff cost anything against my plan?
Reading creates nothing, so no diff read counts against the quotas that cap how much you can create, and the sizes and page bounds in Limits are identical on every plan. The one plan-dependent bound is outbound data: the batch reads and the file list are metered by the bytes they return, and an exhausted budget answers 429. Expanding a single file and reading a line window are not metered at all.
Related
- Merge requestsproposing a change, the merge check, stacks, and landing it
- Merge request discussionleaving comments on the lines a diff shows you
- Branchescreating and deleting the branches a comparison is taken between
- Repositoriesbrowsing files, commits and tags, and finding a repository's id
- Repository rulesthe protections and checks a change has to satisfy before it merges