Skip to content
GitRiverGitRiver
RU
Navigation

User Guide

User guide: repositories, branches, merge requests, issues, package and image registries, team knowledge

Getting Started

Registration

  1. Open GitRiver in your browser
  2. Click “Register”
  3. Fill in: username, email address, password
  4. 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):

  1. Settings / SSH keys → “Add SSH key”
  2. Paste your public key (~/.ssh/id_ed25519.pub)
  3. 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, field armored_public_key — the key in text form (the output of gpg --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 ssh and git 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.email in 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:

  1. Settings / Access tokens → “Create access token”
  2. 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:

  1. Open the file
  2. Click “Edit”
  3. Make your changes
  4. Enter a commit message
  5. 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

  1. Push a branch with your changes (git push)
  2. In the interface: the “New Pull Request” button
  3. Specify the target branch, title, and description
  4. 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

  1. The “Issues” tab in the repository
  2. “New Issue”
  3. Fill in the title and description (Markdown)
  4. 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

![CI](https://git.example.com/owner/repo/badge.svg)
![CI develop](https://git.example.com/owner/repo/badge.svg?branch=develop)

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/deny masks restrict which names are even allowed out at all (deny wins over allow);
  • 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 pom and 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:password pair (for Docker Hub, an account and access token); they’re sent only to that source’s token-issuing service;
  • a HEAD on a layer is answered only from the cache: the proxy won’t download an entire layer just to answer with an empty body, and docker pull doesn’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

Within a Repository

The Search tab in the repository — search over file contents.

Options: regular expressions, path filter, branch selection.

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:

  1. Connect a cluster (administrator)
  2. In the repository: the RiverCD tab → create an application
  3. Specify: Docker image, port, replica count, environment variables
  4. Automatic deployment from CI

Pages (Static Sites)

Hosting static sites from a repository:

  • URL: https://git.example.com/_pages/owner/repo/ The ci source 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 a public/ 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 main branch. 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 clone on a .wiki.git address 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:

  1. Settings / Two-factor authentication
  2. “Enable 2FA”
  3. Scan the QR code in an app (Google Authenticator, Authy)
  4. Enter the confirmation code
  5. 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.