Skip to content
GitmoapdocsBeta
Guide· How do I…?

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 readYou pass
A commit's changesa commit SHA, 7 to 40 hex characters — an abbreviated SHA is fine, a branch name is not
A merge request's changesthe merge request's number, the one in its URL
A comparisontwo branch names — a tag or a raw SHA is refused
A stackthe number of the merge request at the top of the chain
A page of a diff you already have the commit pair fortwo full 40-character SHAs
Every file a change touchestwo full 40-character SHAs
One file's patchtwo full 40-character SHAs and a file path
A window of a file's linesone 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

ReadStarts atEnds at
A commitits first parent — or, for the very first commit in a repository, the empty treethe commit itself
A merge requestthe merge base of its target and source branchesthe source branch tip
A comparisonthe merge base of base and headthe head branch tip
A stackthe merge base of the stack's bottom target branch and the top merge request's sourcethat source branch tip
A page or the file list, read by a commit pairthe base you passthe 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 starts

That 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.

FlagWhat happenedHow you get the content
collapsed: trueThe 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 patchesFetch that one file's diff on demand — it comes back force-expanded
patch_truncated: trueThe patch is past the hard per-file ceiling. Nothing expands it in placeRead the file in line windows, a chunk at a time
generated: trueThe file looks machine-written. Reported always, independent of whether it was foldedNothing 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.

Terminalbash
gitmoap diffs get-commit-diff --repo-id <id> --commit-ref <id>
curlbash
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.

Terminalbash
gitmoap diffs get-mr-diff --repo-id <id> --mr-id <id>
curlbash
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.

Terminalbash
gitmoap diffs compare-refs --repo-id <id> --base <base> --head <head>
curlbash
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.

Terminalbash
gitmoap diffs get-stack-diff --repo-id <id> --mr-id <id>
curlbash
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.

Terminalbash
gitmoap diffs get-diff-file \
--repo-id <id> \
--base <base> \
--head <head> \
--path <path>
curlbash
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.

Terminalbash
gitmoap diffs get-file-lines \
--repo-id <id> \
--path <path> \
--ref <ref> \
--start <n>
curlbash
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.

Terminalbash
gitmoap diffs get-diff-files --repo-id <id> --base <base> --head <head>
curlbash
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.

Terminalbash
gitmoap diffs get-diff --repo-id <id> --base <base> --head <head>
curlbash
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

get_commit_diffGET /repositories/{repo_id}/commits/{commit_ref}/diffOpen in the API reference

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 type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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 via GET .../file-lines instead.
    • 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. stats still counts all of them.
GET/repositories/{repo_id}/merge-requests/{mr_id}/diffdiff:read

Get diff for a merge request

get_mr_diffGET /repositories/{repo_id}/merge-requests/{mr_id}/diffOpen in the API reference

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 type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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 via GET .../file-lines instead.
    • 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. stats still counts all of them.
GET/repositories/{repo_id}/comparediff:read

Compare two branches

compare_refsGET /repositories/{repo_id}/compareOpen in the API reference

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 branch
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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_truncatedbooleantrue when 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 reference

Token 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 type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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 via GET .../file-lines instead.
      • 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. stats still counts all of them.
  • repository_iduuid
GET/repositories/{repo_id}/diffdiff:read

Get a page of the diff between two commits

get_diffGET /repositories/{repo_id}/diffOpen in the API reference

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/head
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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 via GET .../file-lines instead.
    • 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. stats still counts all of them.
GET/repositories/{repo_id}/diff-filesdiff:read

List every file a diff between two commits changes, without patches

get_diff_filesGET /repositories/{repo_id}/diff-filesOpen in the API reference

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/head
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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_path when 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. stats still counts all of them.
GET/repositories/{repo_id}/diff-filediff:read

Fetch a single file's diff on demand, force-expanded

get_diff_fileGET /repositories/{repo_id}/diff-fileOpen in the API reference

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/path
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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 via GET .../file-lines instead.
    • pathstring
    • statusaddedcopieddeletedmodifiedrenamed
  • head_shastring
GET/repositories/{repo_id}/file-linesdiff:read

Read a bounded window of a file's lines at a ref

get_file_linesGET /repositories/{repo_id}/file-linesOpen in the API reference

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/start
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository 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; lines is 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.

WhatThe boundWhat you see at it
Files in one page1 to 100, 20 by defaultA value outside the range is clamped, not refused
Files in one change20,000truncated: 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 file100 KiBpatch_truncated: true and no patch body — read that file in line windows
Changed lines before a file is folded500 in the filecollapsed: true with collapse_reason: "large"
Changed lines before the rest of a page is folded5000 across the pageThe same, for every later file whose changes would push past the budget. A small file that still fits is left alone
Lines in one window500A wider request is clamped; has_more_after tells you to ask again
File size a line window can read1 MiB400 BAD_REQUEST naming the file's size — the one bound that refuses rather than trims
Context lines around a hunk0 to 100, 3 by defaultClamped
Commits previewed in a comparison100commits_truncated: true; ahead and total_commits still count every one of them
Merge requests rolled into one stack diff20 below the anchorThe 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/repo path 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. base is what you would merge into, head is 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, head or ref was 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
start was left out, or was not a whole number
What to do
Pass the line number the patch showed you, or start=1 for the top of the file. Line numbers are 1-based, and a 0 is read as 1
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 git from 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 in required_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 404 rather than 403 so 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_sha and an added file at head_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-Match and 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 collapsed and collapse_reason and expand those files on demand; a patch_truncated file is read in line windows. Send collapse=false to stop folding altogether
Fewer files came back than total_files reports
Cause
You are holding one page. stats describes the whole change, files describes 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=0 and follow has_more, or divide total_files by your limit to 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_path for that file as old_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
base and head are the wrong way round
What to do
Swap them. behind is the count of what base has that head does 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_binary is true. 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_binary is true — or start is past the last line, in which case end comes back one below start
What to do
Download a binary file rather than windowing it. For a text file, walk back down: has_more_before tells you a line exists above start
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_sha and head_sha it 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:

shellscript
# 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.

  • 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