Navigation
User Guide
User guide: repositories, branches, merge requests, issues, package and image registries, team knowledge
Getting Started
Registration
- Open GitRiver in your browser
- Click “Register”
- Fill in: username, email address, password
- Done — you can now create repositories
Username may contain Latin letters, digits, hyphens, and underscores, from 2
to 39 characters long. Some names are reserved by the application’s own
pages (settings, explore, search, admin, and similar) and are
unavailable.
Password must be at least 12 characters (characters are counted, not bytes: a short Cyrillic word won’t become long that way). There are no composition requirements: a long phrase of ordinary words is strong and easy to remember. The same rule applies when changing your password and when an administrator resets it.
Changing Your Username
Settings / Change Username. Enter the new name and confirm the operation by typing your current one.
This changes the addresses of all your repositories: git remote,
links to issues and pull requests, package registry addresses, and
published site addresses.
The old name is freed but keeps pointing to you until someone else takes
it. Everything that was configured under the old name keeps working: git clone, fetch, push, Git LFS, package registries, Pages sites. Updating
git remote isn’t required, but is advisable — as soon as someone else
takes the freed name, the old links will lead to them instead.
You can reclaim your old name as long as no one else has taken it.
A group’s path changes the same way — on the group’s page, if you’re its owner. The group’s display name is unrelated to its path and is changed separately.
SSH Keys
For working over SSH (cloning and pushing):
- Settings / SSH keys → “Add SSH key”
- Paste your public key (
~/.ssh/id_ed25519.pub) - Cloning:
git clone ssh://git@git.example.com/owner/repo.git
Commit Signing (GPG and SSH)
Both git signature formats are supported: GPG (gpg.format=openpgp) and SSH
(gpg.format=ssh).
- GPG: the public key is added with the API call
POST /api/v1/user/gpg_keys, fieldarmored_public_key— the key in text form (the output ofgpg --armor --export <key ID>). - SSH: no separate key is needed — keys already added under Settings /
SSH Keys also serve as trusted signers. Configure git:
git config gpg.format sshandgit config user.signingkey ~/.ssh/id_ed25519.pub.
Signed commits get a badge in the commit list, on the commit page, on the comparison page, and in the pull request. There are three kinds of badge:
- Signed — the signature is valid, the key is bound to an account, and
the commit author’s email matches the email of the key’s owner. Only the
account’s email is confirmed: the email in the GPG key’s UID is entered
by the owner themselves, and nothing verifies that they actually own it.
That’s why
user.emailin your repository should match the email on your GitRiver account. - Unverified — a signature is present, but the signer isn’t confirmed: either the key isn’t registered to anyone, or the commit author’s email doesn’t belong to the key’s owner. The tooltip states the reason.
- Invalid signature — the signature doesn’t match the commit’s content, has expired, or was made with a revoked key.
An unsigned commit gets no badge.
If a branch has the required signatures rule enabled (see “Branch Protection”), commits without a trusted signature (GPG or SSH) are rejected on push and on merge.
Access Tokens
For the API and automation:
- Settings / Access tokens → “Create access token”
- Use it as a password for HTTPS, or in the
Authorization: Bearer <token>header
Repositories
Creation
- In the interface: the “New Repository” button
- API:
POST /api/v1/repos
Parameters: name, description, visibility (public/private), initialization (README, .gitignore, license).
Repository name. Letters (any alphabet, including Cyrillic), digits,
hyphens, underscores, and dots; up to 255 characters and no more than 251
bytes in UTF-8. A Cyrillic letter takes two bytes, so a Russian name longer
than 125 letters won’t be accepted. The names ., .., names starting
with .., and names ending in .git are reserved.
The interface form and the API validate the name with the same rule: whatever the form rejects, the API call rejects too, and vice versa.
Group path is validated the same way: letters, digits, hyphens, and underscores, up to 255 characters; dots aren’t allowed. Some paths are reserved by application pages — see “Registration”.
Cloning
# HTTPS
git clone https://git.example.com/owner/repo.git
# SSH
git clone ssh://git@git.example.com/owner/repo.git
Browsing Files
- File tree: switching branches/tags, navigating directories
- File view: syntax highlighting, line authorship (blame), source text (raw)
- Commits: history, diff, CI check states
Editing in the Browser
Any file can be edited right in the interface:
- Open the file
- Click “Edit”
- Make your changes
- Enter a commit message
- Commit to the current branch or create a new one
Archives
Downloading sources: GET /api/v1/repos/{owner}/{name}/archive/{ref}
(tar.gz/zip)
Migrating from GitHub, GitLab, and Gitea
- In the interface: the “Import Repository” page
- API:
POST /api/v1/import
You’ll need an API access token for the source; for self-hosted installations, the installation’s address must also be given. What gets migrated is chosen with checkboxes: issues, labels, milestones, pull requests, releases, wikis.
Pull requests are migrated together with their review history — original number, state (open, closed, merged), both branches, labels, assignees, close and merge dates, general and line-level comments (including thread replies), and review verdicts. GitLab has no verdicts as such — approvals are migrated instead.
Migration progress and its outcome are visible in the repository’s settings, under “Import Report”: per-stage counters and a list of losses. Losses are stated explicitly, not discovered later:
- an author from the source not found among GitRiver users becomes whoever started the import (matching is done by username);
- an assignee or reviewer that isn’t found is not migrated;
- a pull request that came from a fork: the source repository isn’t created in GitRiver, so diffing such a request isn’t available;
- a line comment the source can no longer attach to a line (a stale diff) is migrated as a path to the file, with no line number;
- a dismissed review is reduced to “commented”.
Running the migration again into the same repository doesn’t create duplicates: a request whose number is already taken is skipped and listed in the report.
Mirroring
A repository can be kept as a mirror of an external one: “Settings →
Mirror”, or POST /api/v1/repos/{owner}/{name}/mirror. Direction is
pull (pull changes in) or push (push your own changes out).
Sync interval (interval_minutes, from 5 minutes to a week) is a
schedule: the server updates the mirror on its own as soon as that much
time has passed since the last sync. An interval of zero means “manual
only”.
The “Sync” button (POST .../mirror/sync) runs the same thing
immediately, without waiting for the schedule. If this mirror is already
syncing, the request gets a 409 response — there’s never two git fetch
runs on the same repository at once.
The result of the last pass is shown in the same place: the time of the last successful sync, and the text of the last error if the pass failed. A failure doesn’t speed up the next attempt: it happens after the same interval, not right away.
A mirror is populated into the same repository that git push targets, so
the “single repository size” limit applies to it the same way: a
repository over the limit doesn’t sync, and the reason ends up in the
mirror’s “last error” (see quotas).
Branches and Tags
Branches
- List: the “Branches” tab in the repository
- Create: the “Create branch” button (from any branch, tag, or commit)
- Delete: the delete icon (protected branches can’t be deleted)
Branch Protection
Configured under Settings / Branch protection:
- Block push and force-push
- Required CI checks
- Required code review
- Required commit signatures (GPG/SSH)
- Required resolved discussion threads — off by default: turning it on changes the merge conditions for pull requests that are already open
- Trusted CI runner only — off by default
- CODEOWNERS (Pro)
Trusted CI Runner Only
A job’s outcome is reported to the server by whoever ran it, and the
server can’t verify that: a runner whose token was stolen, or whose image
was swapped, can report success for a job it never ran. With required CI
checks, that’s a way around them.
The rule narrows down whose word the protection trusts:
| Runner | Who registers it | Counts |
|---|---|---|
| Built-in runner | server administrator | yes |
| Instance-level runner | server administrator | yes |
| Group runner | group maintainer | no |
| Repository runner | repository CI administrator | no |
A group or repository runner is registered by the same person who controls that project’s workflows and settings — their word about their own checks doesn’t reinforce the protection. A job whose runner is unknown to the server (for example, one run on a previous server version) also doesn’t count as trusted.
The rule looks at jobs from GitRiver’s own pipeline. It doesn’t check a
commit status set via the API (POST /repos/{owner}/{name}/statuses/{sha} — how external build systems report
in): only the repository owner who issued the token knows who set it. If
checks come from outside, trust in them is set by who holds a write-scoped
token — not by this rule.
The rule is off by default: enabled, it would block merges everywhere a project runner closes out checks. Before turning it on, make sure the jobs that merging depends on are picked up by the built-in runner or an instance-level runner — otherwise the pull request will show “checks were closed by a project runner” and name the offending jobs.
Tags
- Create and delete via the interface or git
- Start CI (
on: push: tags: ['v*'])
Pull Requests
Creation
- Push a branch with your changes (
git push) - In the interface: the “New Pull Request” button
- Specify the target branch, title, and description
- Assign reviewers, labels, a milestone
Review
- Comments on individual lines of code
- Approve / Request Changes / Comment — review types
- Draft — work in progress (merge is disabled)
Discussion Threads
A remark and its replies form a discussion thread. The thread has two levels: you reply to the remark, not to another reply.
- A reply is left with the “Reply” button — both in the discussion panel and right in the diff. A reply sits on the same line as the remark: it has no attachment of its own, and the whole thread is marked stale together whenever new commits land on the branch.
- Marking as resolved closes a handled remark: the thread collapses to a single line, expandable on click. The remark’s author, the pull request’s author, and anyone with write access may mark it resolved or reopen it.
- An unresolved count is shown above the discussion and next to the changes summary.
Merging
Merge conditions (configured in branch protection):
- All required checks passed
- Required number of approvals met
- No conflicts
- If the “require resolved discussion threads” rule is on — no unresolved threads remain. Their count is shown in the protection checklist next to the merge button.
Issues
Issues in a public repository can be read without signing in: the list, an issue card, its discussion, labels, and milestones are open the same way as the repository’s code and description. Signing in is required to create an issue, leave a comment, or change labels. In a private repository, issues are visible only to those with access to the repository itself.
Creation
- The “Issues” tab in the repository
- “New Issue”
- Fill in the title and description (Markdown)
- Assign: an owner, labels, a milestone
Templates
A repository can contain issue templates under
.gitriver/issue_templates/.
Labels
Create and manage: Settings / Labels
Milestones
Grouping issues by stage/release:
- Progress bar of closed issues
- Due date
Assistant-Powered Change Review
If the administrator has set up the assistant (Pro edition and a Pro seat), a user with write access can click “Review Changes” on the pull request page: the model reads the changes and leaves remarks as a comment marked “Generated by a model.” The review is the model’s opinion, not a check — read it the way you’d read someone else’s guess. Details — ai-assistant.md.
CI/CD
Full documentation: ci-workflows.md
Quick Start
Create .gitriver/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
jobs:
test:
image: node:22
steps:
- run: npm ci && npm test
Viewing
- The CI/CD tab in the repository — list of pipeline runs
- Click a run — job composition, DAG diagram
- Click a job — real-time log
Failed Job Analysis
If the administrator has set up a language-model-based assistant (Pro edition and a Pro seat), a failed job gets an “Analyze Failure” button: the model reads the tail of the log and reports the likely cause and fix. The response is marked as machine-generated — it’s the model’s analysis, not a human’s, and should be checked the same way as someone else’s guess.
The analysis is available to users with write access to the repository: each use counts against the repository owner’s monthly limit. If the model provider is cloud-hosted, the button states which address the log content is sent to. Details — ai-assistant.md.
Artifacts
Downloading job artifacts: in the interface, or GET /api/v1/repos/{owner}/{name}/pipelines/{id}/artifacts/{job_name}
Build Badges


Container Registry
Every repository has its own Docker image registry:
# Login
docker login git.example.com -u username -p token
# Build and push
docker build -t git.example.com/owner/repo/image:tag .
docker push git.example.com/owner/repo/image:tag
# In CI (automatically):
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker push $CI_REGISTRY/$CI_REPOSITORY_OWNER/$CI_REPOSITORY_NAME:$CI_COMMIT_TAG
Tag Retention Rules
Rules are set up in the interface: Repository Settings → Image Registry Rules. The section is visible to the repository administrator — the same person the server lets change the rules. They’re also listed and deleted with confirmation there. The same is available through the API — examples below.
A retention rule automatically deletes tags that are no longer needed. A rule has four fields:
| Field | What it sets |
|---|---|
Image mask (image_pattern) |
which of the repository’s images the rule applies to; * — all of them |
Tag exclusion mask (exclude_tag_pattern) |
which tags the rule does NOT touch; empty — excludes nothing |
Keep last (keep_last) |
how many of the image’s newest tags to never delete; 0 — no limit on count |
Delete older than, days (max_age_days) |
the age at which a tag counts as unneeded; 0 — no age limit |
A tag is deleted only if it falls outside BOTH limits at once: it’s
both beyond keep_last and older than max_age_days. That’s why “keep
the last 10” works as a safety net: a tag among the ten newest survives
any age limit. Zero means “no limit”, and two zeros mean the rule deletes
nothing.
The exclusion mask is for moving tags — ones overwritten by every build:
:buildcache, :latest, :nightly. The tag protection rule doesn’t
suit them by nature — it forbids overwriting, which is the build itself.
Example: with the mask *buildcache*, the build
cache survives cleanup and keeps getting overwritten as usual.
# Keep the image's last 10 tags for at least 30 days, don't touch the build cache
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"image_pattern": "*", "exclude_tag_pattern": "*buildcache*",
"keep_last": 10, "max_age_days": 30}' \
https://git.example.com/api/v1/repos/owner/repo/packages/retention
In the form, empty numeric fields mean “no limit” wherever the API accepts a zero. A zero in the “keep last” field would read as a prohibition (“keep zero tags”), so both the form and the rule list spell it out in words.
Separate from retention rules, there’s a tag protection rule: a tag under it can’t be overwritten, and cleanup doesn’t delete it either. It’s set up in the same place, under “Image Registry Rules”, with the “Tag mask” field:
# Protect all tags matching v1.2.3 from being overwritten
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"pattern": "v*"}' \
https://git.example.com/api/v1/repos/owner/repo/packages/tag-rules
The mask for either rule can’t be empty and can’t contain a space: image
and tag names never contain spaces, so such a mask would match nothing
while the rule looked configured. Both cases are rejected with 400.
Package Registry
Supported formats:
| Format | Publish | Install |
|---|---|---|
| npm | npm publish --registry |
npm install --registry |
| PyPI | twine upload --repository-url |
pip install --index-url |
| Cargo | cargo publish --registry |
cargo install --registry |
| Maven | mvn deploy |
mvn install |
| NuGet | dotnet nuget push |
dotnet add package |
| Generic | PUT /api/v1/packages/.../generic/... |
GET /api/v1/packages/.../generic/... |
| Composer | POST /api/v1/packages/.../composer/publish?tag=v1.0.0 |
composer require through its own repository |
Package management: the Packages tab in the repository — it also has a ready-made install command for each package.
A published version is immutable. Republishing the same version
number with different content is rejected — 409, the previous file
stays in place: bump the version number instead. Republishing with the
same content succeeds (200 instead of 201) — for when the connection
dropped and it’s unclear whether the request got through. A second file
for the same version (a wheel next to the sources, sources.jar next to
the jar) is published as usual, and Maven snapshot versions
(-SNAPSHOT) can be republished any number of times. A released
version’s description and dependency list also can’t be changed by
republishing — edit them in the next version instead.
Cargo is configured not with a flag but with a config file: index is the
root of the sparse index, from which cargo itself builds config.json and
the crate files.
# ~/.cargo/config.toml
[registries.gitriver]
index = "sparse+https://<host>/api/v1/packages/<owner>/<repo>/cargo/index/"
# ~/.cargo/credentials.toml — for a private repository (access token)
[registries.gitriver]
token = "<token>"
cargo publish --registry gitriver
cargo add <crate> --registry gitriver
Proxying Package Downloads
The registry repository can act as a caching proxy: on a miss in local storage, the package is fetched from an external registry and kept in the cache. Client configuration doesn’t change — the registry address stays the same, and a local package always wins over an external one (protection against dependency substitution).
Setup: Settings → Packages → Proxying Package Downloads. The source address is given as the protocol’s root, not the registry’s homepage:
| Type | Source address |
|---|---|
| npm | https://registry.npmjs.org |
| PyPI | https://pypi.org/simple — root of the Simple API |
| Cargo | https://index.crates.io — root of the sparse index |
| Maven | https://repo1.maven.org/maven2 — repository root |
| NuGet | https://api.nuget.org/v3/index.json — the service index itself |
| Generic | root, to which {package}/{version}/{file} is appended |
| Docker | https://registry-1.docker.io — registry root without /v2 |
What the cache gives you:
- the build keeps working when the external registry is unreachable: metadata is served from the cache even past its expiry, files as they are;
allow/denymasks restrict which names are even allowed out at all (denywins overallow);- the cache’s content shares the same storage as published packages and images, and counts against the same repository quota.
Reaching an external registry on the server’s behalf requires
authentication even in a public repository: an anonymous request only
gets local content. For Cargo this shows up in config.json as the
auth-required flag — cargo will send a token with every index request.
Masks are matched against the package name the way the client writes it:
@mycorp/* for npm, mycorp-* for PyPI and Cargo, com.mycorp:* (i.e.
groupId:artifactId) for Maven, Mycorp.* for NuGet, the package name
for Generic, library/* for images.
Maven and NuGet
A Maven request path goes to the source as-is. Artifacts are immutable
and cached without an expiry; maven-metadata.xml is mutable — it
combines the registry’s versions and the external repository’s versions.
The .md5/.sha1/.sha256/.sha512 checksums are served for every
artifact, external ones included, and match the content served by a plain
GET — even where the source itself doesn’t publish them.
A NuGet client knows exactly one registry address — the service index —
and gets every other resource, including external packages’
registrations, through the proxy: no client request goes to nuget.org
past the cache. A large package’s registration arrives as a single
response, not split into pages. NuGet identifiers are case-insensitive,
and the cache respects that: Newtonsoft.Json and newtonsoft.json are
one and the same entry.
Search (npm search, cargo search, NuGet’s SearchQueryService) stays
local: it shows what’s published in this repository and isn’t
supplemented by the external registry.
Configuring Clients
The client’s registry address is the same whether proxying is configured or not: on a miss in local storage, the server fetches from the source itself. Switching a build over to the cache is simply replacing the external registry’s address with GitRiver’s — nothing more.
Below, <host> is the installation’s address, <owner>/<repo> is the
registry repository whose settings define the source.
# npm — .npmrc
registry=https://<host>/api/v1/packages/<owner>/<repo>/npm/
//<host>/api/v1/packages/<owner>/<repo>/npm/:_authToken=<token>
# pip — ~/.config/pip/pip.conf (or pip.ini on Windows)
[global]
index-url = https://<username>:<token>@<host>/api/v1/packages/<owner>/<repo>/pypi/simple/
# cargo — ~/.cargo/config.toml
[registries.gitriver]
index = "sparse+https://<host>/api/v1/packages/<owner>/<repo>/cargo/index/"
# ~/.cargo/credentials.toml
[registries.gitriver]
token = "<token>"
For Cargo, when proxying is configured, credentials.toml is required:
reaching out on the server’s behalf isn’t given to anonymous requests, and
the registry’s config.json declares auth-required — cargo sends a
token with every index request.
{/* maven — ~/.m2/settings.xml */}
<settings>
<servers>
<server>
<id>gitriver</id>
<username><username></username>
<password><token></password>
</server>
</servers>
<mirrors>
<mirror>
<id>gitriver</id>
<name>GitRiver</name>
<url>https://<host>/api/v1/packages/<owner>/<repo>/maven/</url>
{/* All dependencies go through the cache, including the central repository. */}
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
</settings>
{/* nuget — nuget.config next to the solution */}
<configuration>
<packageSources>
<clear />
<add key="gitriver"
value="https://<host>/api/v1/packages/<owner>/<repo>/nuget/index.json"
protocolVersion="3" />
</packageSources>
<packageSourceCredentials>
<gitriver>
<add key="Username" value="<username>" />
<add key="ClearTextPassword" value="<token>" />
</gitriver>
</packageSourceCredentials>
</configuration>
// composer — the project's composer.json; address WITHOUT `/packages.json`,
// composer appends it itself
{
"repositories": [
{ "type": "composer",
"url": "https://<host>/api/v1/packages/<owner>/<repo>/composer" }
]
}
// composer — ~/.composer/auth.json (or auth.json next to composer.json)
{ "bearer": { "<host>": "<token>" } }
# docker — the image is pulled by the mirror repository's name
docker login <host>
docker pull <host>/<owner>/<repo>/library/nginx:1.25
A token is presented with one of these schemes: Authorization: Bearer <token>, Authorization: token <token>, the PRIVATE-TOKEN header, or Authorization: Basic with the token in the
password’s place — that’s how pip, Maven, and NuGet send it; none of
these three clients know any other scheme. The username in the pair can
be anything: identity comes from the token, not from it.
<clear /> for NuGet and <mirrorOf>*</mirrorOf> for Maven matter:
without them, the client keeps going straight to nuget.org and Maven
Central, bypassing the cache, and in an air-gapped network the build will
fail on the very first dependency.
Cache Maintenance: Retention and Eviction
The cache lives in the same storage as published packages and images, and without maintenance it would grow without end. So the cache is maintained by a background pass (every 6 hours by default):
- metadata is deleted once a package hasn’t been requested for longer than the retention period (30 days by default). The period is counted from the last access, not from the expiry date: metadata past its expiry stays in the cache and helps out when the source is unreachable;
- files are evicted once the cache’s size exceeds its limit — starting with the least recently accessed. There are two limits: one per source (Settings → Packages → Proxying Package Downloads) and one shared across the installation (Administration → Storage → Proxy Cache). Zero means “no limit”.
Both limits are enforced by that same background pass, so with maintenance turned off neither applies at all — a limit set on a repository waits for maintenance to be enabled at the installation level.
There’s no separate per-repository cache limit: the cache draws on the same package storage quota as publishing and images.
The same pass also cleans up the warming log: requests finished more than a month ago are deleted, and pending ones aren’t touched no matter how long they wait.
An evicted file frees space only once nothing else still references its content: the cache shares storage with local packages, images, and LFS objects, and identical content is stored once.
The Run Maintenance Now button under Administration → Storage runs exactly the same pass as the schedule.
Air-Gapped Mode
The “cache only” switch turns off all outbound access: the client only
gets what’s already been warmed, and whatever isn’t in the cache gets a
404 with a clear reason instead of waiting out a connection timeout.
It’s switched on in two places, and applies on an “or” basis:
- per source — freeze one registry while leaving the others working;
- per installation (Administration → Storage) — turn off the network entirely; a new source is then closed from the start, with nothing to remember about it.
Air-gapped mode differs from a disabled source in that the cache keeps serving clients: a disabled source means “there’s no proxy”, while air-gapped mode means “there’s a proxy, but no network”.
A mode change reaches all replicas within fifteen seconds; on the replica where the switch was flipped — immediately.
Until a replica has read the setting (the first moments after a restart, if the database happens to be unreachable then), outbound access is closed: a client may be refused something that isn’t in the cache, and such a refusal clears itself.
Warming
Warming fills the cache ahead of time — while the network is still there. This is exactly the preparation for going air-gapped: a list of dependencies is submitted under Settings → Packages → Proxying Package Downloads → Warm Cache, one entry per line:
lodash 4.17.21
react 18.2.0
@mycorp/ui
Fields are separated by spaces: package [version [file]].
- without a version, only the package’s metadata is warmed (the packument, the index page, the version list) — this is how you queue a request when the build will pick the version later;
- without a file name, all files for the version are fetched — for
PyPI that’s wheels for every platform plus the source distribution, for
Maven it’s the
pomand the main artifact per the packaging it declares; - a file name is needed where there’s nothing to enumerate — the generic file registry has no metadata at all;
- for images, the version is a tag or digest. The whole image is warmed: manifests for every platform in the index, configs, and layers. If you only need one platform, queue its manifest digest instead. An index wider than 64 platforms is rejected.
The request is queued instantly and carried out in the background: warming a long list can take hours. Progress is shown in the same panel; a failed request is retried three times, after which it moves to “Failed” with a reason. Resubmitting the same list re-queues requests that already succeeded — that’s how you refresh what’s changed or been evicted.
Warming draws on the same repository quota and obeys the same
allow/deny masks as regular serving: a name closed off by a mask won’t
get into the cache through a request either.
Reports
A source’s card shows hits, misses, hit rate, cache size, bandwidth saved, and the most requested packages. An installation-wide summary is under Administration → Storage, along with the number of warming requests queued and failed.
Bandwidth saved is counted from content actually served out of the cache: the metadata body on a hit or an ETag revalidation, the file size on a cache serve. Warming isn’t counted in these numbers at all — neither as a hit nor a miss: the hit rate describes the cache’s value to builds.
The count in “most requested” lives with the cache file itself: an evicted file starts its count over, so the hit rate (which is cumulative) and this list can diverge after maintenance runs. The list answers “what’s in the cache right now and how often it’s fetched”, not “what was fetched ever”.
Every proxy response carries the X-GitRiver-Cache header: hit —
served from cache, miss — went to the source, revalidated — the
source confirmed the cache with a bodiless response, stale — served
expired content (source unreachable, or air-gapped mode).
Docker/OCI Images
Proxying images is configured in the same section, with registry type
docker. The external image’s name is appended to the mirror
repository’s address:
docker login gitriver.example.com
# mirror repository: myteam/mirror, external image: library/nginx
docker pull gitriver.example.com/myteam/mirror/library/nginx:1.25
For Docker Hub, a single-segment name automatically gets the library/
prefix — the same way docker pull itself does.
What sets images apart from packages:
- a manifest fetched by tag is revalidated against the source on expiry (via conditional requests); a manifest or layer fetched by digest is immutable and stored with no expiry;
- content fetched by digest is checked against it: if the source returns
something else, the response is rejected (
502) and doesn’t enter the cache; - a manifest list for several platforms is served as-is — the client picks the platform;
- layers live in shared storage alongside local images: identical content is stored once and counted against the same quota;
- credentials for a private source are given as a
username:passwordpair (for Docker Hub, an account and access token); they’re sent only to that source’s token-issuing service; - a
HEADon a layer is answered only from the cache: the proxy won’t download an entire layer just to answer with an empty body, anddocker pulldoesn’t need it to; - images fetched through the proxy are not scanned automatically; images published to the repository are;
- warming an image pulls in manifests for all platforms in the index, together with their layers: a request doesn’t name a platform, and a build in an air-gapped setup runs under whatever architecture was deployed there.
Git LFS
Storing large files:
# Install
git lfs install
# Track
git lfs track "*.psd"
git lfs track "*.bin"
# Commit .gitattributes
git add .gitattributes
git commit -m "Track large files with LFS"
# Work as usual
git add large-file.psd
git commit -m "Add design file"
git push
Code Search
Within a Repository
The Search tab in the repository — search over file contents.
Options: regular expressions, path filter, branch selection.
Global Search
Navigation / Search — search across every repository you have access to.
Options: q (query), case (case sensitivity), regex, path (path
filter), ref (branch).
Query Mode
By default (regex=false), q is a substring: dots, brackets, and other
metacharacters are matched literally, so the query arr[0] finds exactly
arr[0], and h.llo doesn’t match hello.
The .* button (regex=true) turns on a regular expression in POSIX ERE
syntax. An unclosed bracket or another invalid pattern is a query error
(400), not a server failure: the interface asks you to fix the pattern.
Result Limits
Search returns a slice of the results, so a common pattern doesn’t dump an entire repository:
- within a repository — up to 100 files, up to 50 matches per file, and up to 1000 matches total;
- in global search — up to 20 files, up to 10 matches per file, and up to 100 matches from each repository;
- a matching line is truncated to 512 bytes (relevant for single-line minified files);
- binary files never appear in results.
If the result you need isn’t showing up, narrow the query or the path filter.
Notifications
Email Notifications
Configured under Settings / Email Notifications:
- Notifications for pull requests, issues, and CI builds
- Choice of events to subscribe to
Webhooks
Configured under Settings / Webhooks (at the repository level):
- URL to send events to
- Choice of events (push, pull request, issue, CI build, and others)
- Secret for signing (HMAC-SHA256)
Notification Channels
Configured under Repository / Settings / Notification Channels: Telegram, Max, Slack, Discord, Microsoft Teams, Matrix, email, or a custom webhook. A channel subscribes to events (builds, pull requests, issues) and gets a message for each one.
Delivery status. In the channel list, each one shows when it last delivered. If the recipient answered with a failure, the failure text is shown instead — that’s what you use to fix the channel (a revoked bot token, a deleted incoming webhook, a closed room). The mark clears itself as soon as delivery succeeds; the channel’s test button also refreshes it.
Channels don’t retry delivery — unlike repository webhooks, where a failed delivery is retried five times. If the recipient doesn’t answer, the message is lost, and the channel shows the failure. For events that mustn’t be lost, use a webhook.
The one exception is an “email” channel: messages go through the server’s shared mail queue and are retried by it just like any other mail. That channel’s delivery mark means the message was accepted by the queue; from there, the queue is responsible for it, and an SMTP failure is visible to the installation’s administrator, not in the channel.
Deploy
Deploying applications to Kubernetes:
- Connect a cluster (administrator)
- In the repository: the RiverCD tab → create an application
- Specify: Docker image, port, replica count, environment variables
- Automatic deployment from CI
Pages (Static Sites)
Hosting static sites from a repository:
- URL:
https://git.example.com/_pages/owner/repo/Thecisource publishes after a successful pipeline: the site is taken from the artifact of the job named in the Pages settings. The job must save an artifact with apublic/directory at its root — that directory’s contents become the site:
jobs:
build:
steps:
- run: npm run build --outDir public
artifacts:
paths:
- public
The last 5 publications are kept; older ones are deleted on the next one.
Wiki
Every repository has a wiki — a set of pages in Markdown. It’s edited
through the interface (the “Wiki” tab in the repository) and through the
wiki API (/api/v1/repos/{owner}/{name}/wiki/...); the edit history of
each page is read there too. Permissions are inherited from the
repository: whoever can see the repository can read the wiki, whoever has
write access can edit it.
The wiki works differently from the repository itself, and three of its properties are worth knowing in advance.
- There’s only one branch. Pages live on the single
mainbranch. The wiki never has a second branch; branching doesn’t apply to it. - Edits aren’t reviewed. A pull request can’t be attached to the wiki: an edit becomes visible immediately.
- The wiki isn’t reachable over git from outside. Neither over HTTP
nor over SSH:
git cloneon a.wiki.gitaddress simply fails, and pushing to it isn’t possible either.
Hence the working pattern: wiki edits go through the interface or the API, not “clone it, edit it, send a pull request.” Text that needs branches and review belongs in the repository itself — there it gets both.
Knowledge
Knowledge is the team’s memory of the code: what was learned, why something was done a certain way, and what to check next time. It exists mainly for an AI agent, which would otherwise start from a blank page every time, but a person sees it on the same screen — the “Knowledge” tab in the repository.
It works in every edition; only team roll-ups, dead-memory analysis, and the draft limit are paid (see licensing.md).
Three Things the Tab Shows
- The index — all of the repository’s knowledge: an entry’s excerpt, its declared surfacing mechanisms, and a “won’t surface” mark on an entry whose mechanism this server version doesn’t support. Such an entry comes back to life on its own after an upgrade — nothing needs rebuilding.
- Drafts — immature entries. A draft is visible to the whole team right away, even before the branch is merged: that’s the point, otherwise one branch’s finding wouldn’t reach another. From there, a batch is published outward as files, into that very branch it was written from.
- My counters — how many times an entry was surfaced to you personally, and how many times you opened it: “surfaced twelve times, never opened.” This is proof that the memory is working, and the first sign that it’s dead.
The surfaced-count grows on every surfacing of an entry to an agent. A normal server stop or upgrade loses no surfacings; a crashed process may lose those from the last few seconds. Counters you read always include every saved surfacing.
Dead-Memory Analysis (Pro)
The team roll-up shows EVERY entry in the index, including ones that were never surfaced — as a zero row. Otherwise memory’s main ailment would go unseen: an entry that never surfaced is simply absent from surfacing counts. For the same reason, “seen by” counts people the entry was actually SHOWN to: someone who opened it via a direct link isn’t counted.
Dead memory is entries that aren’t working. The “By Team” tab’s analysis names not just those entries but the REASON — different ailments call for different remedies.
- Shown but never opened — the extraction contract fires where the entry isn’t needed. The most expensive ailment: such an entry costs the agent’s attention at every single step. Fixed by rewriting the surfacing condition or by retiring the entry.
- Can’t surface — this server version doesn’t support any of the declared mechanisms; the report names exactly which ones are missing. Fixed by upgrading the server — the entry comes back to life on its own, with no rebuild needed.
- Never surfaced — the mechanism is supported, but its condition never occurred. Fixed by broadening the condition or retiring the entry.
The thresholds are stated right in the report: an entry counts as ailing
if it’s been shown at least three times with no opens, or if it hasn’t
surfaced even once in a week. Custom thresholds are set with the
min_shown and min_age_days parameters.
How Knowledge Gets Into the Repository
A durable entry lives as a file under .gitriver/knowledge/ (format in
knowledge-format.md), while counters and drafts
live on the server and never go into git.
Knowledge goes out as a batch, through an ordinary pull request — it has no approval flow of its own. Branch protection, CODEOWNERS, and merge checks apply to it just like to any other pull request.
What to Write Down
Anything that will save time next time is worth writing: the reason behind a non-obvious decision, an environment gotcha, a sequence of steps that can’t be derived from the code. A retelling of the code isn’t worth it — it’s already visible, and memory that never gets opened just gets in the way of finding what matters.
Code Index
The “Symbols” tab answers three questions about the repository’s code: where a symbol is, what it calls, and who calls it. A Pro-edition feature: the server builds one shared name map for the whole team, and it’s available without cloning the repository.
This isn’t code search. Search (the “Search” tab) answers “where does this string appear” and shows a declaration, a call, and a word in a comment all the same way. The index answers about RELATIONSHIPS — “what will break if I change this function” — and search can’t answer that.
And it’s not a substitute for an IDE. Call relationships are determined
BY NAME, without regard to types: “who calls push” will show calls to
same-named methods on different types. For unique names the answer is
exact; for short ones it’s noisy, and an IDE will jump more precisely.
The index earns its keep where there’s no IDE: on the web, when reading
through someone else’s repository, and for an AI agent that sees the
repository through the API.
What the Tab Shows
Start typing a name — search matches by prefix. A found symbol shows its kind (function, method, struct, trait), its parent block, and its path and line: the link goes straight to the code. Select a symbol and two lists appear next to it: what it calls (with a repeat count: a loop with fifty calls is one line with a counter) and who calls it.
When the Answer Carries a Caveat
The index catches up with the tree on its own: on read, it takes in the changes that reached the repository since last time. If it didn’t finish in time, the answer is marked “index is catching up with the tree” — and that’s an important difference from “nothing found”: the same question, asked again, will answer more fully. A separate “index is incomplete” mark means the repository has hit the index’s size limit, and part of the code didn’t make it in.
Rust, Python, JavaScript, and TypeScript are parsed. Files in other languages don’t make it into the index.
Security and Licenses
The Security tab collects code scanning findings and the licenses of the repository’s dependencies. It is a Pro edition feature, available to accounts with a Pro seat.
Findings
Findings arrive in two ways:
- the Run scan button searches the latest commit of the default branch for secrets: keys, tokens, private keys;
- a SARIF report from any scanner — Trivy, Semgrep, Gitleaks, CodeQL and
others — is uploaded with
POST /api/v1/repos/{owner}/{name}/security/sarif(the body is the report text, up to 10 MB). This is how a CI job sends the report right after the scanner runs. The kind of finding (dependency, code, secret) is determined by the scanner name, the severity by the result level in the report.
Findings can be filtered by scanner, severity and state. A false finding can be dismissed and a dismissed one reopened.
Dependency Licenses
The Scan licenses button parses the dependency manifests in the latest
commit of the default branch: package.json, package-lock.json,
Cargo.toml, Cargo.lock, go.mod, go.sum, requirements.txt,
pyproject.toml, poetry.lock, Gemfile, pom.xml. If a manifest has its
lock file next to it or higher up the tree, dependencies are taken from the
lock file — with exact versions and once each.
Licenses of Rust packages are requested from crates.io, so the server needs access to it. Each scan has a limited time budget: with many dependencies, some licenses stay unknown until the next scan, which continues where this one stopped.
License Policy
Without a policy the scan only lists dependencies and does not look for violations. A policy is set in the License policy card on the same tab:
- mode — “denied licenses” (everything not listed is allowed) or “allowed licenses only”;
- licenses — SPDX identifiers separated by commas, for example
GPL-3.0-only, AGPL-3.0-only; - violation severity — the severity a violation gets among findings;
- block merging on violation.
A policy belongs to the account that created it and can be bound to any repository that account administers. Policy changes apply on the next license scan.
With merge blocking, a pull request whose dependencies violate the policy cannot be merged, directly or through the merge queue, and the pull request panel names the violating packages. The dependencies of the commit that would be merged are checked. A package whose license no scan has determined yet is not treated as a violation.
SBOM
The SBOM button downloads the dependency list in CycloneDX 1.4 (JSON) format, built from the same manifests.
Two-Factor Authentication
Recommended for all users:
- Settings / Two-factor authentication
- “Enable 2FA”
- Scan the QR code in an app (Google Authenticator, Authy)
- Enter the confirmation code
- Save your recovery codes — they aren’t shown again
API: Response Contract
Uniform rules for every /api/v1 call — a client is written once for the
whole API.
| Operation | Response |
|---|---|
POST creating a resource |
201 Created + JSON of the created resource |
POST — an action (merge, cancel, test, sync) |
200 OK + the action’s result |
PATCH — partial update |
200 OK + the updated resource |
PUT — replace or create at a known URI |
200 OK + resource, or 204 with no body |
DELETE |
204 No Content, no body |
| A mutation with no payload | 204 No Content |
| Error | same status code + {"error": "<text>", "code": "<machine code>"} |
The error field is text for a human: it’s translated and may change.
The code field is a stable machine code for programs: not_found,
ref_not_found, unauthorized, forbidden, conflict, validation,
internal, bad_gateway, too_large, license_required, rate_limited,
registration_closed. It’s how a client tells a licensing denial
(license_required — the feature is available in a paid edition) apart
from a permissions denial (forbidden) — without parsing the text — and
a missed ref when reading a tree or file (ref_not_found — no such
branch, tag, or commit exists in the repository) from a missed path
within it (not_found).
The full list of codes is in /api/openapi.json (the Error schema).
A successful response may carry an optional partial field — a caveat
that the data wasn’t returned in full: right now the only value,
"truncated", means the results were cut off by a limit (for example, a
code search found more matches than the response is allowed to carry).
Absence of the field means a complete response.
A partial update (user, issue, pull request, webhook, release, branch
protection rule, and so on) is done with PATCH. The old PUT on the
same paths keeps working as a deprecated alias and is marked deprecated
in /api/openapi.json — move clients over to PATCH.
The exceptions to this contract are calls to external protocols (npm, Cargo, NuGet, Maven, Git LFS, OCI, SCIM): there, the response’s shape is set by the client’s own specification.
What this promise covers, how deprecation is announced, and how long a
deprecated call keeps working — see the /api/v1 compatibility promise.
/api/v1 responses aren’t cached (Cache-Control: no-store, private).
Native package-registry calls are marked private — a package manager’s
local cache (npm, cargo, pip) works as usual.
The Request Body Is Parsed Strictly
A field the call doesn’t recognize gets a 400 with code validation,
not silent discarding. The message names both the fields it didn’t
recognize and the ones the call accepts:
{
"error": "the request body has fields this call does not know: “visibility”. The call accepts: “name”, “description”, “default_branch”, “is_private”, “group_path”",
"code": "validation"
}
This way a request with a mistake in a field name isn’t silently carried
out without that field: a repository-creation request with
"visibility": "public" instead of "is_private": false gets a refusal,
not a private repository and a 201. The same error also covers a duplicated field in an
object and trailing content past the end of the body.
Parse failures arrive in the same envelope: an unreadable body is a 400
with code validation and text naming the field (“the request body lacks
the required field “title””). A body with no
Content-Type: application/json header gets 415, same envelope and
code.
The same envelope covers failures parsing PARAMETERS: an unreadable
identifier in the URL is a 400 with the text “the path parameter “id” is
invalid”, a missing query-string parameter reads “the query string lacks
the required parameter “after””. The
query string itself isn’t parsed strictly, though: an unrecognized
parameter is simply ignored, since proxies and tracking tags append extra
ones along the way.
The rule applies within /api/v1. Calls to external protocols (npm,
Cargo, NuGet, Maven, Git LFS, OCI, SCIM, OIDC), the GitHub- and
GitLab-dialect compatibility layers, and the runner protocol
(/api/v1/runner/**) accept unknown fields silently: the request’s shape
there is set by an external specification, and the runner is updated
separately from the server.
Fields the call recognizes but that not everyone may set (an
administrator flag on a profile, a Pro-seat flag) also get an error —
403 with code forbidden and a list of the fields — rather than success
with the record left unchanged.
Pagination Bounds
Page size (limit, per_page, size, take, count) is clamped to the
1..=100 range; offset (offset, skip, startIndex) is clamped to
0..=100000. A value outside the range isn’t treated as an error: it’s
truncated, and the request returns a normal page. That is, ?limit=0 and
?limit=-1 both return a single record, not an empty list and not a
500.
Some calls document a wider cap: the audit log
(/api/v1/admin/audit-log) and findings delivery — 200 records, the SCIM
lists (/scim/v2/Users, /scim/v2/Groups) — 200, an image’s tag list
(/v2/{image}/tags/list) — 1000 per the OCI spec. Commit history and
wiki-page history have a stricter limit on how far back you can page:
10,000 pages and 10,000 revisions respectively.
For deep navigation through large lists, use the cursor (after,
before, the next_cursor field in the response) rather than growing
the offset.
Total Count for Commit History, Branches and Tags
Commit history and a page of branches or tags are returned as an array, as
before, and the full number of records comes in the X-Total response
header:
GET /api/v1/repos/{owner}/{name}/commits?ref=main&page=2&per_page=20
X-Total: 4213
For history it is the number of commits reachable from the branch; for
branches and tags, the number of records after the query filter. It tells
how many pages there are, and no extra empty page is requested past the last
one. The header is sent when a page is requested; branches and tags without
page parameters return the whole list and carry no header.
Comments and Reviews Are Paged
Lists of issue comments, pull request comments, and pull request reviews return a page, not the whole array:
GET /api/v1/repos/{owner}/{name}/issues/{number}/comments?per_page=20&after=<cursor>
GET /api/v1/repos/{owner}/{name}/pull_requests/{number}/comments?per_page=20&after=<cursor>
GET /api/v1/repos/{owner}/{name}/pull_requests/{number}/reviews?per_page=20&after=<cursor>
The response looks like other cursor-paged lists: {"items": [...], "next_cursor": "..."}. The order is unchanged, oldest to newest;
next_cursor is empty once the discussion has been fully read. For
discussion threads, this means a remark’s root always arrives no later
than its replies.
On the first page, a pull request’s discussion and reviews also
carry total — the full record count; it’s absent from later pages. Issue comments don’t
carry total: the full count is already in the issue itself
(comment_count).
This is a breaking change relative to versions up to and including
1.0.x, where these same three calls returned a top-level array: a client
that read the response as an array needs to switch to the items field
and page through with next_cursor.
Packages and Their Versions Are Paged
A repository’s package list and a single package’s version list return a page, not the whole array:
GET /api/v1/repos/{owner}/{name}/pkg?per_page=20&after=<cursor>
GET /api/v1/repos/{owner}/{name}/pkg/{pkg_type}/{pkg_name}?per_page=20&after=<cursor>
The response looks like other cursor-paged lists: {"items": [...], "next_cursor": "..."}. The package list is ordered by name; versions go
from newest to oldest; next_cursor is empty once the list has been
fully read. The first page also carries total — the full count of
packages or versions; it’s absent from later pages. Each version’s files arrive together
with its page — no separate request per version is needed.
This is a breaking change relative to versions up to and including
1.0.x, where both calls returned a top-level array: a client that read
the response as an array needs to switch to the items field and page
through with next_cursor.
Protocol-native registry listings (PyPI Simple, maven-metadata.xml, the
NuGet index, the Cargo sparse index, npm’s packument, Composer) haven’t
been paged and won’t be: their shape is set by an external specification,
and the client parses the response against it.