Перейти к содержимому
GitRiverGitRiver
EN
Навигация

Руководство пользователя

Руководство пользователя: репозитории, ветки, запросы на слияние, задачи, реестры пакетов и образов, знание команды

Начало работы

Регистрация

  1. Откройте GitRiver в браузере
  2. Нажмите «Зарегистрироваться»
  3. Заполните: имя пользователя, адрес почты, пароль
  4. Готово — вы можете создавать репозитории

Имя пользователя может содержать латинские буквы, цифры, дефис и подчёркивание, длиной от 2 до 39 символов. Часть имён занята страницами самого приложения (settings, explore, search, admin и подобные) и недоступна.

Пароль — не короче 12 символов (считаются символы, а не байты: короткая кириллическая запись длинной не станет). Ограничений по составу нет: длинная фраза из обычных слов надёжна и хорошо запоминается. То же правило действует при смене пароля и при сбросе его администратором.

Смена имени пользователя

Настройки / Смена имени пользователя. Введите новое имя и подтвердите операцию, напечатав текущее.

Меняются адреса всех ваших репозиториев: git remote, ссылки на задачи и запросы на слияние, адреса реестров пакетов и опубликованных сайтов.

Прежнее имя освобождается, но пока его никто не занял — продолжает вести на вас. Всё, что было настроено на старое имя, продолжает работать: git clone, fetch, push, Git LFS, реестры пакетов, сайты Pages. Обновлять git remote не обязательно, но желательно — как только освободившееся имя займёт кто-то другой, старые ссылки уведут к нему.

Вернуть себе прежнее имя можно, пока его никто не занял.

Путь группы меняется так же — на странице группы, если вы её владелец. Отображаемое название группы к пути отношения не имеет и меняется отдельно.

SSH-ключи

Для работы через SSH (клонирование и отправка):

  1. Настройки / SSH-ключи → «Добавить SSH-ключ»
  2. Вставьте публичный ключ (~/.ssh/id_ed25519.pub)
  3. Клонирование: git clone ssh://git@git.example.com/owner/repo.git

Подпись коммитов (GPG и SSH)

Поддерживаются оба формата подписи git: GPG (gpg.format=openpgp) и SSH (gpg.format=ssh).

  • GPG: публичный ключ добавляется вызовом API POST /api/v1/user/gpg_keys с полем armored_public_key — ключом в текстовом виде (вывод gpg --armor --export <идентификатор ключа>).
  • SSH: отдельный ключ не нужен — добавленные в Настройки / SSH-ключи ключи одновременно служат доверенными подписантами. Настройте git: git config gpg.format ssh и git config user.signingkey ~/.ssh/id_ed25519.pub.

Подписанные коммиты получают отметку в списке коммитов, на странице коммита, на странице сравнения и в запросе на слияние. Отметка бывает трёх видов:

  • Подписано — подпись верна, ключ привязан к учётной записи, и почта автора коммита совпадает с почтой её владельца. Подтверждает только почта учётной записи: почту из UID GPG-ключа владелец вписывает сам, и никакой проверки владения ею нет. Поэтому в user.email репозитория указывайте ту же почту, что и в учётной записи GitRiver.
  • Не подтверждено — подпись есть, но подписант не подтверждён: ключ не зарегистрирован ни за кем либо почта автора коммита не принадлежит владельцу ключа. Причина указана в подсказке.
  • Подпись неверна — подпись не сходится с содержимым коммита, просрочена или сделана отозванным ключом.

Неподписанный коммит отметки не получает.

Если для ветки включено правило обязательной подписи (см. «Защита веток»), коммиты без доверенной подписи (GPG или SSH) при отправке и слиянии отклоняются.

Токены доступа

Для API и автоматизации:

  1. Настройки / Токены доступа → «Создать токен доступа»
  2. Используйте как пароль для HTTPS или в заголовке Authorization: Bearer <token>

Репозитории

Создание

  • В интерфейсе: кнопка «Новый репозиторий»
  • API: POST /api/v1/repos

Параметры: имя, описание, видимость (публичный/приватный), инициализация (README, .gitignore, лицензия).

Имя репозитория. Буквы (любого алфавита, в том числе кириллица), цифры, дефис, подчёркивание и точка; длиной до 255 символов и не больше 251 байта в кодировке UTF-8. Буква кириллицы занимает два байта, поэтому русское имя длиннее 125 букв не примется. Имена ., .., начинающиеся с .. и оканчивающиеся на .git заняты служебно.

Форма интерфейса и API проверяют имя одним и тем же правилом: то, что отклонит форма, отклонит и вызов API, и наоборот.

Путь группы проверяется тем же порядком: буквы, цифры, дефис и подчёркивание, до 255 символов, точка не допускается. Часть путей занята страницами приложения — см. «Регистрация».

Клонирование

# HTTPS
git clone https://git.example.com/owner/repo.git

# SSH
git clone ssh://git@git.example.com/owner/repo.git

Просмотр файлов

  • Дерево файлов: переключение веток/тегов, навигация по каталогам
  • Просмотр файла: подсветка синтаксиса, авторство строк (blame), исходный текст (raw)
  • Коммиты: история, изменения, состояния проверок CI

Редактирование в браузере

Любой файл можно отредактировать прямо в интерфейсе:

  1. Откройте файл
  2. Нажмите «Редактировать»
  3. Внесите изменения
  4. Укажите сообщение коммита
  5. Коммит в текущую ветку или создание новой

Архивы

Скачивание исходников: GET /api/v1/repos/{owner}/{name}/archive/{ref} (tar.gz/zip)

Перенос с ГитХаба, ГитЛаба и Gitea

  • В интерфейсе: страница «Импорт репозитория»
  • API: POST /api/v1/import

Нужен токен доступа к API источника; для установок на своих серверах дополнительно указывается адрес установки. Что переносится, выбирается галочками: задачи, метки, вехи, запросы на слияние, релизы, вики.

Запросы на слияние переносятся вместе с историей рецензий — с исходным номером, состоянием (открыт, закрыт, слит), обеими ветками, метками, назначенными, датами закрытия и слияния, общими и построчными комментариями (включая ответы в ветке обсуждения) и вердиктами рецензий. Для ГитЛаба вердиктов как таковых нет — переносятся одобрения.

Ход переноса и его итог видны в настройках репозитория, раздел «Отчёт об импорте»: счётчики по этапам и перечень потерь. Потери называются явно, а не выясняются потом:

  • автор с источника не найден среди пользователей GitRiver — им становится запустивший импорт (сопоставление идёт по совпадению имени пользователя);
  • назначенный или рецензент не найден — не переносится;
  • запрос пришёл из форка: репозиторий-источник в GitRiver не заводится, поэтому сравнение изменений по такому запросу недоступно;
  • построчное замечание, которое источник уже не может привязать к строке (устаревшее сравнение), переносится путём к файлу без номера строки;
  • снятая рецензия сводится к «прокомментировано».

Повторный запуск переноса в тот же репозиторий не создаёт дублей: запрос с уже занятым номером пропускается и попадает в отчёт.

Зеркалирование

Репозиторий можно держать зеркалом внешнего: «Настройки → Зеркало» либо POST /api/v1/repos/{owner}/{name}/mirror. Направление — pull (тянуть изменения к себе) или push (отдавать свои наружу).

Интервал синхронизации (interval_minutes, от 5 минут до недели) — расписание: сервер сам обновляет зеркало, как только с прошлой синхронизации прошло столько времени. Нулевой интервал означает «только вручную».

Кнопка «Синхронизировать» (POST .../mirror/sync) запускает то же самое немедленно, не дожидаясь срока. Если синхронизация этого зеркала уже идёт, запрос отвечает 409 — двух git fetch в один репозиторий не бывает.

Итог последнего прохода виден там же: время успешной синхронизации и текст последней ошибки, если проход не удался. Неудача не ускоряет следующую попытку: она произойдёт через тот же интервал, а не сразу.

Зеркалом наполняется тот же репозиторий, в который идёт git push, поэтому и предел «размер одного репозитория» действует одинаково: репозиторий за пределом не синхронизируется, а причина попадает в «последнюю ошибку» зеркала (см. квоты).


Ветки и теги

Ветки

  • Список: вкладка “Ветки” в репозитории
  • Создание: кнопка «Создать ветку» (от любой ветки, тега или коммита)
  • Удаление: значок удаления (защищённые ветки нельзя удалить)

Защита веток

Настройка в Настройки / Защита веток:

  • Запрет отправки и принудительной отправки
  • Обязательные проверки CI
  • Обязательное рецензирование кода
  • Обязательная подпись коммитов (GPG/SSH)
  • Обязательно решённые ветки обсуждения — по умолчанию правило выключено: включение меняет условия слияния для уже открытых запросов
  • Только доверенный исполнитель CI — по умолчанию правило выключено
  • CODEOWNERS (Pro)

Только доверенный исполнитель CI

Исход задания сообщает серверу тот, кто его выполнял, и проверить это сервер не может: исполнитель, у которого увели токен или подменили образ, отвечает success по заданию, которого не выполнял. С обязательными проверками CI это означает путь мимо них.

Правило сужает круг тех, чьему слову защита верит:

Исполнитель Кто его заводит Засчитывается
Встроенный исполнитель администратор сервера да
Исполнитель уровня сервера администратор сервера да
Исполнитель группы сопровождающий группы нет
Исполнитель репозитория администратор CI репозитория нет

Исполнитель группы и репозитория заводит тот же, кто правит рабочие процессы и настройки проекта, — его слово о собственных проверках защиту не подкрепляет. Задание, исполнитель которого серверу неизвестен (например, выполненное на предыдущей версии сервера), доверенным тоже не считается.

Правило смотрит на задания конвейера GitRiver. Состояние коммита, выставленное через API (POST /repos/{owner}/{name}/statuses/{sha} — так отчитываются внешние системы сборки), оно не проверяет: кто его поставил, знает только владелец репозитория, выдавший токен. Если проверки приходят снаружи, доверие к ним задаётся тем, кому выдан токен с правом записи, а не этим правилом.

Правило по умолчанию выключено: включённым оно остановило бы слияния везде, где проверки закрывает исполнитель проекта. Включив его, убедитесь, что задания, от которых зависит слияние, берёт встроенный исполнитель или исполнитель уровня сервера, — иначе запрос на слияние покажет «проверки закрыл исполнитель проекта» и назовёт задачи-причины.

Теги

  • Создание и удаление через интерфейс или git
  • Запускают CI (on: push: tags: ['v*'])

Запросы на слияние

Создание

  1. Отправьте ветку с изменениями (git push)
  2. В интерфейсе: кнопка «Новый запрос»
  3. Укажите целевую ветку, заголовок и описание
  4. Назначьте рецензентов, метки, веху

Рецензирование

  • Комментарии к отдельным строкам кода
  • Одобрить / Запросить изменения / Комментарий — виды рецензии
  • Черновой запрос — работа в процессе (слияние закрыто)

Ветки обсуждения

Замечание и ответы на него образуют ветку обсуждения. Ветка двухуровневая: отвечают на замечание, а не на другой ответ.

  • Ответ оставляется кнопкой «Ответить» — и в обсуждении, и прямо в сравнении изменений. Ответ висит на той же строке, что и замечание: своя привязка ему не задаётся, и вся ветка помечается устаревшей вместе, когда в ветку приходят новые коммиты.
  • Пометка решённой закрывает разобранное замечание: ветка сворачивается в одну строку, разворачивается по щелчку. Пометить и снять пометку вправе автор замечания, автор запроса на слияние и участник с правом записи.
  • Счётчик нерешённых показан над обсуждением и рядом со сводкой изменений.

Слияние

Условия слияния (настраиваются в защите веток):

  • Все обязательные проверки прошли
  • Набрано необходимое число одобрений
  • Нет конфликтов
  • Если включено правило «требовать решённые ветки обсуждения» — не осталось нерешённых веток. Их число видно в чек-листе защиты рядом с кнопкой слияния.

Задачи

Задачи публичного репозитория читаются без входа: список, карточка задачи, обсуждение, метки и вехи открыты так же, как код и описание репозитория. Чтобы создать задачу, оставить комментарий или изменить метки, нужно войти. В приватном репозитории задачи видны только тем, у кого есть доступ к самому репозиторию.

Создание

  1. Вкладка «Задачи» в репозитории
  2. «Новая задача»
  3. Заполните заголовок, описание (Markdown)
  4. Назначьте: ответственного, метки, веху

Шаблоны

Репозиторий может содержать шаблоны задач в .gitriver/issue_templates/.

Метки

Создание и управление: Настройки / Метки

Вехи

Группировка задач по этапам/релизам:

  • Полоса выполнения по закрытым задачам
  • Срок

Разбор изменений помощником

Если администратор настроил помощника (редакция Pro и место Pro), на странице запроса на слияние участник с правом записи может нажать «Разобрать изменения»: модель прочитает изменения и оставит замечания комментарием с пометкой «Сгенерировано моделью». Разбор — мнение модели, а не проверка: читать его надо так же, как чужую догадку. Подробно — ai-assistant.md.

CI/CD

Подробная документация: ci-workflows.md

Быстрый старт

Создайте .gitriver/workflows/ci.yml:

name: CI
on:
  push:
    branches: [main]

jobs:
  test:
    image: node:22
    steps:
      - run: npm ci && npm test

Просмотр

  • Вкладка CI/CD в репозитории — список прогонов конвейера
  • Щелчок по прогону — состав заданий, схема DAG
  • Щелчок по заданию — журнал в реальном времени

Разбор упавшего задания

Если администратор настроил помощника на основе языковой модели (редакция Pro и место Pro), у упавшей задачи появляется кнопка «Разобрать сбой»: модель читает хвост журнала и отвечает, в чём причина и что исправить. Ответ помечен как машинный — это разбор от модели, а не от человека, и проверять его надо так же, как чужую догадку.

Разбор доступен участникам с правом записи в репозиторий: обращение расходует месячный предел владельца репозитория. Если поставщик модели облачный, рядом с кнопкой сказано, на какой адрес уходит содержимое журнала. Подробно — ai-assistant.md.

Артефакты

Скачивание артефактов задания: в интерфейсе или GET /api/v1/repos/{owner}/{name}/pipelines/{id}/artifacts/{job_name}

Значки сборки

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

Реестр контейнеров

У каждого репозитория свой реестр образов Docker:

# Вход
docker login git.example.com -u username -p token

# Сборка и отправка
docker build -t git.example.com/owner/repo/image:tag .
docker push git.example.com/owner/repo/image:tag

# В CI (автоматически):
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

Правила хранения тегов

Правила заводятся в интерфейсе: Настройки репозитория → Правила реестра образов. Раздел виден администратору репозитория — тому же, кому сервер разрешает менять правила. Там же они видны списком и удаляются с подтверждением. То же самое доступно через API — примеры ниже.

Правило хранения само удаляет теги, которые перестали быть нужны. У правила четыре поля:

Поле Что задаёт
Маска образа (image_pattern) к каким образам репозитория правило применяется; * — ко всем
Исключающая маска тега (exclude_tag_pattern) какие теги правило НЕ трогает; пустая — не исключает никого
Хранить последних (keep_last) сколько самых свежих тегов образа не удалять; 0 — без ограничения по количеству
Удалять старше, суток (max_age_days) с какого возраста тег считается ненужным; 0 — без ограничения по возрасту

Тег удаляется, только если подошёл под ОБА ограничения сразу: он и за пределами keep_last, и старше max_age_days. Поэтому «хранить последние 10» работает страховкой: тег из десятки свежих переживёт любой срок. Ноль означает «ограничения нет», а два ноля — правило не удаляет ничего.

Исключающая маска нужна подвижным тегам — тем, что перезаписываются каждой сборкой: :buildcache, :latest, :nightly. Правило защиты тегов им не годится по смыслу — оно запрещает перезапись, то есть саму сборку. Пример: при маске *buildcache* кеш сборки переживает чистку и продолжает перезаписываться как обычно.

# Хранить последние 10 тегов образа не меньше 30 суток, кеш сборки не трогать
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

В форме числовые поля пустые означают «без ограничения» — там, где API принимает ноль. Ноль в поле «хранить последних» читался бы запретом («хранить ноль тегов»), поэтому и в форме, и в списке правил он назван словом.

Отдельно от правил хранения действует правило защиты тегов: тег под ним нельзя перезаписать, и чистка его тоже не удаляет. Заводится там же, в разделе «Правила реестра образов», полем «Маска тега»:

# Защитить все теги вида v1.2.3 от перезаписи
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -d '{"pattern": "v*"}' \
  https://git.example.com/api/v1/repos/owner/repo/packages/tag-rules

Маска правила — любого из двух — не может быть пустой и не может содержать пробел: имена образов и тегов пробелов не содержат, поэтому такая маска не совпала бы ни с чем, а правило выглядело бы настроенным. Оба случая отклоняются с кодом 400.


Реестр пакетов

Поддерживаемые форматы:

Формат Публикация Установка
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 через свой репозиторий

Управление пакетами: вкладка Пакеты в репозитории — там же готовая команда установки для каждого пакета.

Опубликованная версия неизменяема. Повторная публикация того же номера версии с другим содержимым отклоняется — 409, прежний файл остаётся на месте: поднимите номер версии. Повтор с тем же содержимым проходит (200 вместо 201) — на случай, если связь оборвалась и непонятно, дошёл ли запрос. Второй файл той же версии (колесо рядом с исходниками, sources.jar рядом с jar) публикуется как обычно, а черновые версии Maven (-SNAPSHOT) можно переопубликовывать сколько угодно. Описание и список зависимостей выпущенной версии повтором тоже не меняются — правьте их следующей версией.

Cargo настраивается не флагом, а конфигом: index — это корень sparse-индекса, от которого cargo сам достраивает config.json и файлы крейтов.

# ~/.cargo/config.toml
[registries.gitriver]
index = "sparse+https://<host>/api/v1/packages/<owner>/<repo>/cargo/index/"

# ~/.cargo/credentials.toml — для приватного репозитория (access token)
[registries.gitriver]
token = "<token>"
cargo publish --registry gitriver
cargo add <crate> --registry gitriver

Выдача пакетов через посредника

Репозиторий-реестр умеет работать кеширующим посредником: при промахе в локальном хранилище пакет берётся из внешнего реестра и остаётся в кеше. Настройки клиента при этом не меняются — адрес реестра остаётся прежним, а локальный пакет всегда сильнее внешнего (защита от подмены зависимости).

Настройка: Настройки → Пакеты → Выдача пакетов через посредника. Адрес источника указывается корнем протокола, а не главной страницей реестра:

Тип Адрес источника
npm https://registry.npmjs.org
PyPI https://pypi.org/simple — корень Simple API
Cargo https://index.crates.io — корень sparse-индекса
Maven https://repo1.maven.org/maven2 — корень репозитория
NuGet https://api.nuget.org/v3/index.json — сам service index
Generic корень, к которому дописывается {пакет}/{версия}/{файл}
Docker https://registry-1.docker.io — корень реестра без /v2

Что даёт кеш:

  • сборка продолжает работать при недоступном внешнем реестре: метаданные отдаются из кеша даже с истёкшим сроком годности, файлы — как есть;
  • маски allow/deny ограничивают, какие имена вообще уходят наружу (deny сильнее allow);
  • содержимое кеша занимает то же хранилище, что публикация и образы, и считается той же квотой репозитория.

Выход во внешний реестр от имени сервера требует аутентификации даже в публичном репозитории: анонимному запросу отдаётся только локальное содержимое. Для Cargo это отражается в config.json признаком auth-required — cargo будет присылать токен ко всем запросам индекса.

Маски сверяются с именем пакета так, как его пишет клиент: @mycorp/* у npm, mycorp-* у PyPI и Cargo, com.mycorp:* (то есть groupId:artifactId) у Maven, Mycorp.* у NuGet, имя пакета у Generic, library/* у образов.

Maven и NuGet

Путь запроса Maven уходит к источнику как есть. Артефакты неизменяемы и кешируются без срока годности, maven-metadata.xml изменяем: в нём объединены версии реестра и версии внешнего репозитория. Контрольные суммы .md5/.sha1/.sha256/.sha512 отдаются для всех артефактов, в том числе внешних, и совпадают с содержимым, которое отдаётся обычным GET, — даже там, где сам источник их не публикует.

У NuGet клиент знает ровно один адрес реестра — service index, — и все прочие ресурсы, включая регистрации внешних пакетов, получает через посредника: ни один запрос клиента не уходит к nuget.org мимо кеша. Регистрация крупного пакета приходит одним ответом, без разбиения на страницы. Идентификаторы NuGet регистронезависимы, и кеш это учитывает: Newtonsoft.Json и newtonsoft.json — одна запись.

Поиск (npm search, cargo search, NuGet SearchQueryService) остаётся локальным: он показывает то, что опубликовано в этом репозитории, и внешним реестром не дополняется.

Настройка клиентов

Адрес реестра у клиента один и тот же, настроено проксирование или нет: при промахе в локальном хранилище сервер сам сходит к источнику. Поэтому переводить сборку на кеш — это заменить адрес внешнего реестра на адрес GitRiver, и ничего больше.

Ниже <host> — адрес установки, <owner>/<repo> — репозиторий-реестр, в настройках которого задан источник.

# 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 (или pip.ini в Windows)
[global]
index-url = https://<пользователь>:<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>"

Для Cargo при настроенном проксировании обязательно credentials.toml: выход наружу от имени сервера анониму не даётся, и config.json реестра объявляет auth-required — cargo присылает токен ко всем запросам индекса.

{/* maven — ~/.m2/settings.xml */}
<settings>
  <servers>
    <server>
      <id>gitriver</id>
      <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>
      {/* Через кеш идут все зависимости, включая центральный репозиторий. */}
      <mirrorOf>*</mirrorOf>
    </mirror>
  </mirrors>
</settings>
{/* nuget — nuget.config рядом с решением */}
<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="<пользователь>" />
      <add key="ClearTextPassword" value="<token>" />
    </gitriver>
  </packageSourceCredentials>
</configuration>
// composer — composer.json проекта; адрес БЕЗ `/packages.json`,
// composer дописывает его сам
{
  "repositories": [
    { "type": "composer",
      "url": "https://<host>/api/v1/packages/<owner>/<repo>/composer" }
  ]
}
// composer — ~/.composer/auth.json (или auth.json рядом с composer.json)
{ "bearer": { "<host>": "<token>" } }
# docker — образ тянется по имени репозитория-зеркала
docker login <host>
docker pull <host>/<owner>/<repo>/library/nginx:1.25

Токен предъявляется одной из схем: Authorization: Bearer <токен>, Authorization: token <токен>, заголовок PRIVATE-TOKEN или Authorization: Basic, где токен стоит на месте пароля, — так его шлют pip, Maven и NuGet, других схем эти три клиента не умеют. Имя пользователя в паре при этом любое: личность берётся из токена, а не из него.

<clear /> у NuGet и <mirrorOf>*</mirrorOf> у Maven существенны: без них клиент продолжит ходить в nuget.org и Maven Central напрямую, мимо кеша, и в закрытом контуре сборка упадёт на первой же зависимости.

Обслуживание кеша: срок хранения и вытеснение

Кеш лежит в том же хранилище, что опубликованные пакеты и образы, и без обслуживания рос бы без конца. Поэтому кеш обслуживается фоновым проходом (по умолчанию каждые 6 часов):

  • метаданные удаляются, если к пакету не обращались дольше срока хранения (по умолчанию 30 суток). Срок отсчитывается от последнего обращения, а не от срока годности: метаданные с истёкшим сроком годности остаются в кеше и выручают, когда источник недоступен;
  • файлы вытесняются, когда объём кеша превышает предел, — начиная с самых давних по обращению. Пределов два: свой у каждого источника (Настройки → Пакеты → Выдача пакетов через посредника) и общий у установки (Администрирование → Хранилище → Кеш выдачи пакетов через посредника). Ноль означает «без предела».

Оба предела соблюдает тот же фоновый проход, поэтому при выключенном обслуживании они не действуют вовсе — предел, заданный в репозитории, ждёт включённого обслуживания на уровне установки.

Отдельного предела кеша «на репозиторий» нет: кеш расходует ту же квоту пакетного хранилища репозитория, что публикация и образы.

Тем же проходом убирается журнал прогрева: заявки, завершённые больше месяца назад, удаляются, а ждущие не трогаются, сколько бы ни ждали.

Вытесненный файл освобождает место только тогда, когда на его содержимое не осталось других ссылок: кеш делит хранилище с локальными пакетами, образами и объектами LFS, и одинаковое содержимое лежит там один раз.

Кнопка Обслужить сейчас в разделе Администрирование → Хранилище делает ровно тот же проход, что и расписание.

Закрытый контур

Переключатель «только кеш» отключает выход наружу совсем: клиенту отдаётся только то, что уже прогрето, а чего в кеше нет — то 404 с внятной причиной, а не ожидание, пока истечёт срок соединения.

Включается он в двух местах и действует по «или»:

  • у источника — заморозить один реестр, оставив прочие рабочими;
  • у установки (Администрирование → Хранилище) — отключить сеть целиком; новый источник при этом закрыт сразу, о нём не надо помнить.

От выключенного источника закрытый контур отличается тем, что кеш продолжает обслуживать клиента: выключенный источник — это «посредника нет», закрытый контур — «посредник есть, сети нет».

Смена режима доходит до всех реплик в течение пятнадцати секунд; на той реплике, где переключатель щёлкнули, — сразу.

Пока реплика не прочитала настройку (первые мгновения после перезапуска, если база в этот момент недоступна), выход в сеть закрыт: клиент может получить отказ в выдаче того, чего нет в кеше, и такой отказ проходит сам.

Прогрев

Прогрев наполняет кеш заранее — пока сеть ещё есть. Это и есть подготовка к закрытому контуру: список зависимостей подаётся в Настройки → Пакеты → Выдача пакетов через посредника → Прогрев кеша, по одной записи в строке:

lodash 4.17.21
react 18.2.0
@mycorp/ui

Поля разделяются пробелами: пакет [версия [файл]].

  • без версии прогреваются только метаданные пакета (packument, страница индекса, список версий) — так ставят заявку, когда версию выберет сборка;
  • без имени файла берутся все файлы версии: у PyPI это колёса всех платформ и исходный дистрибутив, у Maven — pom и основной артефакт по объявленной в нём упаковке;
  • имя файла нужно там, где перечислить нечего: у реестра произвольных файлов (generic) метаданных нет вовсе;
  • для образов версия — это тег или дайджест. Прогревается образ целиком: манифесты всех платформ индекса, конфигурации и слои. Нужна одна платформа — ставьте заявку на дайджест её манифеста. Индекс шире 64 платформ отклоняется.

Заявка ставится мгновенно и выполняется в фоне: прогрев длинного списка может занять часы. Ход виден в той же панели; неудавшаяся заявка повторяется трижды, после чего переходит в «Не вышло» с причиной. Повторная подача того же списка ставит выполненные заявки заново — так подтягивают то, что изменилось или было вытеснено.

Прогрев расходует ту же квоту репозитория и подчиняется тем же маскам allow/deny, что и обычная выдача: закрытое маской имя не попадёт в кеш и через заявку.

Отчёты

Карточка источника показывает попадания, промахи, долю попаданий, объём кеша, сэкономленный трафик и самые востребованные пакеты. Сводка по всей установке — в Администрировании → Хранилище, там же число заявок прогрева в очереди и неудавшихся.

Сэкономленный трафик считается по фактически отданному из кеша содержимому: тело метаданных при попадании и подтверждении по ETag, размер файла при выдаче из кеша. Прогрев в эти числа не входит вовсе — ни попаданиями, ни промахами: доля попаданий описывает пользу кеша для сборок.

Число выдач в «самых востребованных» живёт при самом файле кеша: вытесненный файл начинает счёт заново, поэтому доля попаданий (она накопительная) и этот список могут расходиться после обслуживания. Список отвечает на вопрос «что сейчас в кеше и как часто его берут», а не «что брали за всё время».

Каждый ответ посредника помечен заголовком X-GitRiver-Cache: hit — отдано из кеша, miss — ходили к источнику, revalidated — источник подтвердил кеш ответом без тела, stale — отдано протухшее (источник недоступен либо закрытый контур).

Образы Docker/OCI

Выдача образов через посредника настраивается тем же разделом, тип реестра — docker. Имя внешнего образа дописывается к адресу репозитория-зеркала:

docker login gitriver.example.com
# репозиторий-зеркало: myteam/mirror, внешний образ: library/nginx
docker pull gitriver.example.com/myteam/mirror/library/nginx:1.25

Для Docker Hub односегментное имя дополняется префиксом library/ автоматически — так же, как это делает сам docker pull.

Особенности, отличающие образы от пакетов:

  • манифест по тегу перепроверяется у источника по сроку годности (условными запросами), манифест и слои по дайджесту неизменяемы и хранятся без срока годности;
  • содержимое, полученное по дайджесту, сверяется с ним: если источник отдал другое, ответ отклоняется (502) и в кеш не попадает;
  • список манифестов для нескольких платформ отдаётся как есть — платформу выбирает клиент;
  • слои лежат в общем хранилище рядом с локальными образами: одинаковое содержимое хранится один раз и учитывается той же квотой;
  • учётные данные приватного источника задаются парой имя пользователя:пароль (для Docker Hub — учётная запись и токен доступа); они уходят только службе выдачи токенов этого источника;
  • HEAD по слою отвечает только из кеша: скачивать слой целиком ради ответа без тела посредник не станет, docker pull этого и не требует;
  • образы, полученные через посредника, не проверяются сканером автоматически; проверяются образы, публикуемые в репозиторий;
  • прогрев образа втягивает манифесты всех платформ индекса вместе со слоями: платформу заявка не называет, а сборка в закрытом контуре идёт под ту архитектуру, которую там поставили.

Git LFS

Хранение больших файлов:

# Установка
git lfs install

# Отслеживание
git lfs track "*.psd"
git lfs track "*.bin"

# Коммит .gitattributes
git add .gitattributes
git commit -m "Track large files with LFS"

# Работа как обычно
git add large-file.psd
git commit -m "Add design file"
git push

Поиск кода

В репозитории

Вкладка Поиск в репозитории — поиск по содержимому файлов.

Параметры: регулярные выражения, фильтр по пути, выбор ветки.

Глобальный поиск

Навигация / Поиск — поиск по всем доступным репозиториям.

Параметры: q (запрос), case (регистр), regex, path (фильтр пути), ref (ветка).

Режим запроса

По умолчанию (regex=false) q — это подстрока: точка, скобки и прочие метасимволы ищутся буквально, поэтому запрос arr[0] находит именно arr[0], а h.llo не находит hello.

Кнопка .* (параметр regex=true) включает регулярное выражение в синтаксисе POSIX ERE. Незакрытая скобка или другой негодный шаблон — это ошибка запроса (400), а не сбой сервера: интерфейс просит поправить шаблон.

Пределы выдачи

Поиск отдаёт срез результатов, чтобы частый шаблон не выгружал репозиторий целиком:

  • в репозитории — до 100 файлов, до 50 совпадений на файл и до 1000 совпадений суммарно;
  • в глобальном поиске — до 20 файлов, до 10 совпадений на файл и до 100 совпадений с каждого репозитория;
  • строка совпадения обрезается до 512 байт (актуально для минифицированных файлов в одну строку);
  • двоичные файлы в выдачу не попадают.

Если нужный результат не виден — уточните запрос или фильтр пути.


Уведомления

Уведомления по почте

Настройка в Настройки / Уведомления по почте:

  • Уведомления о запросах на слияние, задачах и сборках CI
  • Выбор событий для подписки

Вебхуки

Настройка в Настройки / Вебхуки (на уровне репозитория):

  • адрес (URL) для отправки событий
  • выбор событий (отправка, запрос на слияние, задача, сборка CI и другие)
  • секрет для подписи (HMAC-SHA256)

Каналы уведомлений

Настройка в Репозиторий / Настройки / Каналы уведомлений: Telegram, Max, Slack, Discord, Microsoft Teams, Matrix, электронная почта, произвольный вебхук. Канал подписывается на события (сборки, запросы на слияние, задачи) и получает сообщение при каждом из них.

Состояние доставки. В списке каналов у каждого видно, когда в него доставляли последний раз. Если получатель ответил отказом — вместо этого показан текст ответа: по нему канал и чинят (отозванный токен бота, удалённый входящий вебхук, закрытая комната). Отметка снимается сама, как только доставка удалась; кнопка проверки канала тоже обновляет её.

Повторов доставки у каналов нет — в отличие от вебхуков репозитория, где неудачная доставка повторяется пять раз. Если получатель не ответил, сообщение теряется, а канал показывает отказ. Для событий, которые нельзя терять, берите вебхук.

Единственное исключение — канал типа «электронная почта»: письма идут общей почтовой очередью сервера и повторяются ею наравне с прочими письмами. Отметка доставки у такого канала означает, что письма приняты очередью; дальше за них отвечает она, и отказ SMTP виден администратору установки, а не в канале.


Развёртывание

Развёртывание приложений в Kubernetes:

  1. Подключите кластер (администратор)
  2. В репозитории: вкладка RiverCD → создайте приложение
  3. Укажите: Docker-образ, порт, количество реплик, переменные окружения
  4. Автоматическое развёртывание из CI

Pages (статические сайты)

Размещение статических сайтов из репозитория:

  • URL: https://git.example.com/_pages/owner/repo/ Источник ci — публикация после успешного конвейера: сайт берётся из артефакта задачи, имя которой указано в настройках Pages. Задача обязана сохранить артефакт с каталогом public/ в корне — именно его содержимое становится сайтом:
jobs:
  build:
    steps:
      - run: npm run build --outDir public
    artifacts:
      paths:
        - public

Хранятся последние 5 публикаций, более старые удаляются при следующей.


Вики

У каждого репозитория есть вики — набор страниц в разметке Markdown. Правится она через интерфейс (вкладка «Вики» в репозитории) и через API вики (/api/v1/repos/{owner}/{name}/wiki/...); там же читается история правок каждой страницы. Права наследуются от репозитория: читает тот, кто видит репозиторий, правит тот, у кого есть право записи.

Вики работает иначе, чем сам репозиторий, и три её свойства стоит знать заранее.

  • Ветка одна. Страницы лежат в единственной ветке main. Второй ветки у вики не бывает, ветвление к ней неприменимо.
  • Рецензирования правок нет. Запрос на слияние к вики не привязать: правка становится видна сразу.
  • Снаружи по git вики недоступна. Ни по HTTP, ни по SSH: git clone адреса .wiki.git отказом и закончится, отправить в неё изменения тоже нельзя.

Отсюда рабочий порядок: правка вики идёт через интерфейс или API, а не через «склонировать, поправить, прислать запрос на слияние». Тексту, которому нужны ветки и рецензии, место в самом репозитории — там он получает и то, и другое.


Знание

Знание — память команды о коде: что выяснили, почему сделали так и что проверять в следующий раз. Она нужна прежде всего ИИ-агенту, который иначе каждый раз начинает с чистого листа, но человеку видна тем же экраном — вкладка «Знание» в репозитории.

Работает в любой редакции; платно только сведение счётчиков по команде, разбор мёртвой памяти и предел черновиков (см. licensing.md).

Три вещи, которые видно на вкладке

  • Указатель — всё знание репозитория: фрагмент записи, объявленные механизмы всплытия и пометка «не всплывёт» у записи, механизм которой эта версия сервера не умеет. Такая запись оживёт сама после обновления — ничего пересобирать не нужно.
  • Черновики — незрелые записи. Черновик виден ВСЕЙ команде сразу, ещё до слияния ветки: в этом его смысл, иначе находка одной ветки не дошла бы до соседней. Оттуда же пачка отправляется наружу — файлами в ту самую ветку, из которой записана.
  • Мои счётчики — сколько раз запись показали лично вам и сколько раз вы её открыли: «показали двенадцать раз, не открыл ни разу». Это доказательство, что память работает, и первый признак, что она мертва.

Счётчик показа растёт на каждое всплытие записи у агента. Обычная остановка или обновление сервера не теряет ни одного показа; при аварийном падении процесса могут потеряться показы последних нескольких секунд. Прочитанные счётчики всегда учитывают все сохранённые показы.

Разбор мёртвой памяти (Pro)

Сведение по команде показывает КАЖДУЮ запись указателя, в том числе ту, которую не показывали ни разу, — нулевой строкой. Иначе главную болезнь памяти не увидеть: запись, которая никогда не всплывала, в счётчиках показов просто отсутствует. По той же причине «видели» считает тех, кому запись ПОКАЗЫВАЛИ: открывший её по прямой ссылке в это число не входит.

Мёртвая память — записи, которые не работают. Разбор на вкладке «По команде» называет не только их, но и ПРИЧИНУ: ответы на разные болезни разные.

  • Показывают, не открывают — контракт извлечения срабатывает не там, где запись нужна. Самая дорогая болезнь: такая запись тратит внимание агента на каждом шаге. Лечится переписанным условием всплытия либо снятием записи.
  • Всплыть не может — ни одного объявленного механизма эта версия сервера не умеет; отчёт называет, каких именно механизмов не хватает. Лечится обновлением сервера — и запись оживает сама, без пересборки указателя.
  • Не всплывала ни разу — механизм поддержан, но его условие не наступало. Лечится расширением условия либо снятием записи.

Пороги названы прямо в отчёте: запись считается больной, если её показали не меньше трёх раз без единого открытия либо она не всплыла ни разу за неделю. Свои пороги задаются параметрами min_shown и min_age_days.

Как знание попадает в репозиторий

Долговечная запись живёт файлом в .gitriver/knowledge/ (формат — в knowledge-format.md), а счётчики и черновики — на сервере, в git они не попадают.

Наружу знание выходит пачкой и обычным запросом на слияние — своего потока одобрения у него нет. Защита ветки, CODEOWNERS и проверки слияния действуют на него как на любой другой запрос.

Что записывать

Годится то, что сэкономит время в следующий раз: причина неочевидного решения, грабли окружения, порядок действий, который не выводится из кода. Не годится пересказ кода — он и так виден, а память, которую не открывают, только мешает искать нужное.


Указатель по коду

Вкладка «Символы» отвечает на три вопроса о коде репозитория: где символ, что он зовёт и кто зовёт его. Возможность редакции Pro: общую карту имён сервер строит один раз для всей команды, и она доступна без клона репозитория.

Это не поиск по коду. Поиск (вкладка «Поиск») отвечает «где встречается строка» и одинаково показывает объявление, вызов и слово в комментарии. Указатель отвечает о СВЯЗЯХ — «кого сломает правка этой функции», и такого ответа поиск дать не может.

И не замена среде разработки. Связь вызова определяется ПО ИМЕНИ, без учёта типов: «кто зовёт push» покажет вызовы одноимённых методов разных типов. Для уникальных имён ответ точен, для коротких — шумен, и в среде разработки переход будет точнее. Указатель ценен там, где среды разработки нет: в вебе, при разборе чужого репозитория и у ИИ-агента, который видит репозиторий через API.

Что показывает вкладка

Начните вводить имя — поиск идёт по началу имени. У найденного символа видно вид (функция, метод, структура, трейт), родительский блок и путь со строкой: ссылка ведёт прямо в код. Выберите символ — рядом появятся два списка: что он зовёт (с числом повторов: цикл из полусотни вызовов — одна строка со счётчиком) и кто зовёт его.

Когда ответ помечен оговоркой

Указатель догоняет дерево сам: при чтении он учитывает изменения, пришедшие в репозиторий с прошлого раза. Если досчёт не успел, ответ помечается «указатель догоняет дерево» — и это важное отличие от «ничего не найдено»: тот же вопрос, заданный снова, ответит полнее. Отдельная пометка «указатель неполон» означает, что репозиторий упёрся в предел размера указателя, и часть кода в него не вошла.

Разбираются Rust, Python, JavaScript и TypeScript. Файлы прочих языков в указатель не попадают.

Безопасность и лицензии

Вкладка «Безопасность» собирает находки проверок кода и лицензии зависимостей репозитория. Возможность редакции Pro, доступна учётным записям с местом Pro.

Находки

Находки приходят двумя путями:

  • кнопка «Сканировать» ищет в последнем коммите ветки по умолчанию секреты: ключи, токены, закрытые ключи;
  • отчёт SARIF от любого сканера — Trivy, Semgrep, Gitleaks, CodeQL и других — загружается вызовом POST /api/v1/repos/{owner}/{name}/security/sarif (тело — текст отчёта, не более 10 МБ). Так отчёт отправляют из задания CI сразу после сканера. Вид находки (зависимость, код, секрет) определяется по имени сканера, уровень — по уровню результата в отчёте.

Находки фильтруются по сканеру, уровню и состоянию. Ложную находку можно отклонить, отклонённую — вернуть.

Лицензии зависимостей

Кнопка «Сканировать лицензии» разбирает манифесты зависимостей в последнем коммите ветки по умолчанию: package.json, package-lock.json, Cargo.toml, Cargo.lock, go.mod, go.sum, requirements.txt, pyproject.toml, poetry.lock, Gemfile, pom.xml. Если рядом с манифестом или выше по дереву лежит его lock-файл, зависимости берутся из lock-файла — с точными версиями и по одному разу.

Лицензии пакетов Rust запрашиваются у crates.io — серверу нужен доступ к нему. На одну проверку отводится ограниченное время: если зависимостей много, часть лицензий останется неизвестной до следующей проверки, которая продолжит с того же места.

Политика лицензий

Без политики проверка только перечисляет зависимости и нарушений не ищет. Политика задаётся в карточке «Политика лицензий» на той же вкладке:

  • режим — «запрещённые лицензии» (всё, кроме перечисленных, допустимо) или «только разрешённые лицензии»;
  • лицензии — идентификаторы SPDX через запятую, например GPL-3.0-only, AGPL-3.0-only;
  • уровень нарушения — с каким уровнем нарушение попадёт в находки;
  • запрещать слияние при нарушении.

Политика принадлежит учётной записи, которая её завела, и привязывается к любому репозиторию, где эта запись — администратор. Изменения политики учитывает следующая проверка лицензий.

С запретом слияния запрос, чьи зависимости нарушают политику, не сливается ни напрямую, ни через очередь, а панель запроса называет нарушающие пакеты. Проверяются зависимости того коммита, который будет слит. Пакет, лицензию которого ещё не определила ни одна проверка, нарушением не считается.

SBOM

Кнопка «SBOM» выгружает перечень зависимостей в формате CycloneDX 1.4 (JSON), построенный по тем же манифестам.

Двухфакторная аутентификация

Рекомендуется для всех пользователей:

  1. Настройки / Двухфакторная аутентификация
  2. «Включить 2FA»
  3. Сканируйте QR-код в приложении (Google Authenticator, Authy)
  4. Введите код подтверждения
  5. Сохраните резервные коды — они не показываются повторно

API: контракт ответов

Единые правила для всех вызовов /api/v1 — клиент пишется один раз на весь API.

Операция Ответ
POST, создающий ресурс 201 Created + JSON созданного ресурса
POST — действие (слияние, отмена, проверка, синхронизация) 200 OK + результат действия
PATCH — частичное обновление 200 OK + обновлённый ресурс
PUT — замена или создание по известному URI 200 OK + ресурс либо 204 без тела
DELETE 204 No Content, тела нет
Мутация без полезной нагрузки 204 No Content
Ошибка тот же код состояния + {"error": "<текст>", "code": "<машинный код>"}

Поле error — текст для человека: он переводится и может меняться. Поле code — стабильный машинный код для программ: not_found, ref_not_found, unauthorized, forbidden, conflict, validation, internal, bad_gateway, too_large, license_required, rate_limited, registration_closed. По нему клиент, в частности, отличает отказ по лицензии (license_required — возможность доступна в платной редакции) от отказа по правам (forbidden) — не разбирая текст, а промах ветки при чтении дерева или файла (ref_not_found — такой ветки, тега или коммита в репозитории нет) от промаха пути внутри неё (not_found). Полный список кодов есть в /api/openapi.json (схема Error).

Успешный ответ может нести необязательное поле partial — оговорку, что данные возвращены не полностью: сейчас единственное значение "truncated" означает, что выдача обрезана пределом (например, поиск по коду нашёл больше совпадений, чем ответ вправе унести). Отсутствие поля означает полный ответ.

Частичное обновление (пользователь, задача, запрос на слияние, вебхук, релиз, правило защиты ветки и т. п.) выполняется методом PATCH. Прежний PUT на тех же путях продолжает работать как устаревший псевдоним и помечен deprecated в /api/openapi.json — переводите клиентов на PATCH.

Исключения из контракта — только вызовы внешних протоколов (npm, Cargo, NuGet, Maven, Git LFS, OCI, SCIM): там форму ответа задаёт спецификация клиента.

Что из перечисленного мы обязуемся не менять, как объявляется устаревание и сколько живёт устаревший вызов — обещание совместимости /api/v1.

Ответы /api/v1 не кешируются (Cache-Control: no-store, private). Нативные вызовы пакетных реестров помечены private — локальный кеш пакетного менеджера (npm, cargo, pip) работает как обычно.

Тело запроса разбирается строго

Поле, которого вызов не знает, — отказ 400 с кодом validation, а не молчаливое отбрасывание. Текст называет и нераспознанные поля, и те, которые вызов принимает:

{
  "error": "в теле запроса поля, которых этот вызов не знает: «visibility». Вызов принимает: «name», «description», «default_branch», «is_private», «group_path»",
  "code": "validation"
}

Так запрос с ошибкой в имени поля не выполняется молча без этого поля: запрос создания репозитория с "visibility": "public" вместо "is_private": false получит отказ, а не приватный репозиторий с ответом 201. Тем же отказом отвечают повтор одного поля в объекте и хвост за концом тела.

Тем же конвертом приходит и отказ разбора: нечитаемое тело — 400 с кодом validation и текстом, называющим поле («в теле запроса нет обязательного поля «title»»). Тело без заголовка Content-Type: application/json — 415, конверт и код те же.

Тем же конвертом приходит отказ разбора ПАРАМЕТРОВ: нечитаемый идентификатор в адресе — 400 с текстом «параметр адреса «id» задан неверно», недостающий параметр строки запроса — «в строке запроса нет обязательного параметра «after»». Состав строки запроса при этом не строгий: незнакомый параметр пропускается, потому что по дороге к адресу их приписывают посредники и метки переходов.

Правило действует в /api/v1. Вызовы внешних протоколов (npm, Cargo, NuGet, Maven, Git LFS, OCI, SCIM, OIDC), слои совместимости с диалектами ГитХаба и ГитЛаба и протокол исполнителя (/api/v1/runner/**) незнакомые поля принимают молча: форму запроса там задаёт внешняя спецификация, а исполнитель обновляется отдельно от сервера.

Поля, которые вызов знает, но задавать их вправе не всякий (признак администратора у профиля, признак места Pro), тоже отвечают отказом — 403 с кодом forbidden и перечнем полей, а не успехом с неизменённой записью.

Границы пагинации

Размер страницы (limit, per_page, size, take, count) приводится к диапазону 1..=100, смещение (offset, skip, startIndex) — к 0..=100000. Значение за границей не считается ошибкой: оно усекается, и запрос отдаёт обычную страницу. То есть ?limit=0 и ?limit=-1 вернут одну запись, а не пустой список и не 500.

Отдельные вызовы документируют более широкий потолок: журнал аудита (/api/v1/admin/audit-log) и выдача находок — 200 записей, списки SCIM (/scim/v2/Users, /scim/v2/Groups) — 200, список тегов образа (/v2/{image}/tags/list) — 1000 по спецификации OCI. У истории коммитов и истории страницы вики перемотка ограничена жёстче: 10 000 страниц и 10 000 правок соответственно.

Для глубокой навигации по большим спискам используйте курсор (after, before, поле next_cursor в ответе), а не рост смещения.

Общее число у истории коммитов, веток и тегов

История коммитов и страница веток или тегов отдаются массивом, как и раньше, а полное число записей приходит в заголовке ответа X-Total:

GET /api/v1/repos/{owner}/{name}/commits?ref=main&page=2&per_page=20
X-Total: 4213

У истории это число коммитов, достижимых из ветки; у веток и тегов — число записей после отбора query. По нему видно, сколько всего страниц, и за последней не запрашивается лишняя пустая. Заголовок приходит, когда запрошена страница; у веток и тегов без параметров страницы список отдаётся целиком и заголовка нет.

Комментарии и обзоры отдаются страницами

Списки комментариев задачи, комментариев запроса на слияние и обзоров запроса на слияние отдают страницу, а не весь массив:

GET /api/v1/repos/{owner}/{name}/issues/{number}/comments?per_page=20&after=<курсор>
GET /api/v1/repos/{owner}/{name}/pull_requests/{number}/comments?per_page=20&after=<курсор>
GET /api/v1/repos/{owner}/{name}/pull_requests/{number}/reviews?per_page=20&after=<курсор>

Ответ — как у остальных курсорных списков: {"items": [...], "next_cursor": "..."}. Порядок прежний, от старых к новым; next_cursor пуст, когда обсуждение дочитано. Для веток обсуждения это значит, что корень замечания приходит не позже своих ответов.

У обсуждения и обзоров запроса на слияние первая страница несёт ещё и total — полное число записей; на последующих страницах его нет. У комментариев задачи total не приходит: полное число уже есть в самой задаче (comment_count).

Это ломающее изменение относительно версий по 1.0.x включительно, где те же три вызова отдавали массив верхнего уровня: клиент, читавший ответ как массив, обязан перейти на поле items и дочитывать страницы по next_cursor.

Пакеты и их версии отдаются страницами

Список пакетов репозитория и версии одного пакета отдают страницу, а не весь массив:

GET /api/v1/repos/{owner}/{name}/pkg?per_page=20&after=<курсор>
GET /api/v1/repos/{owner}/{name}/pkg/{тип}/{имя}?per_page=20&after=<курсор>

Ответ — как у остальных курсорных списков: {"items": [...], "next_cursor": "..."}. Список пакетов идёт по имени, версии — от свежих к старым; next_cursor пуст, когда список дочитан. Первая страница несёт ещё и total — полное число пакетов либо версий; на последующих страницах его нет. Файлы каждой версии приходят вместе с её страницей — отдельного запроса на версию не нужно.

Это ломающее изменение относительно версий по 1.0.x включительно, где оба вызова отдавали массив верхнего уровня: клиент, читавший ответ как массив, обязан перейти на поле items и дочитывать страницы по next_cursor.

Протокольные выдачи реестров (PyPI Simple, maven-metadata.xml, индекс NuGet, sparse-индекс Cargo, packument npm, Composer) страниц не получили и не получат: их форму задаёт внешняя спецификация, и клиент разбирает ответ по ней.