Перейти к содержанию

Работа с монорепозиториями в GitFlic

В данной документации будут рассмотрены возможные способы обеспечения простоты и комфорта при разработке крупных монорепозиториев с использованием системы контроля версий Git и платформы GitFlic. Будут рассмотрены следующие темы:

  1. Sparse-checkout и sparse-index (Разряженная проверка и разряженный индекс)
  2. Partial clone (Частичное клонирование)
  3. Shallow clone (Поверхностное клонирование)

Данные способы будут проверены при работе с репозиторием ядра linux, а также достаточно крупными репозиториями VSСode и Rust.

Характеристики данных репозиториев на момент проведения анализа:

  1. Linux kernel
    Количество коммитов: 1293367
    Количество файлов: 85719
    Размер проекта: 6.7 GB
    Размер репозитория: 5.2 GB

  2. Rust
    Количество коммитов: 261340
    Количество файлов: 48640
    Размер проекта: 1.6 GB
    Размер репозитория: 1.3 GB

  3. VSCode
    Количество коммитов: 123190
    Количество файлов: 7448
    Размер проекта: 1 GB
    Размер репозитория: 0.84 GB

Стоит также понимать, что время, требуемое для выполнения команд и загрузки репозиториев, зависит от пропускной способности канала передачи, а также скорости интернета.

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

Для дальнейшей возможности проведения сравнительного анализа производительности вышеописанных способов прежде всего необходимо замерить стандартные показатели команды git clone.

Таблица 1. Показатели выполнения клонирования репозитория командой git clone.

Репозиторий Время клонирования Размер проекта Размер репозитория Количество файлов
Linux kernel 847 sec 6.7 GB 5.2 GB 85719
Rust 173 sec 1.6 GB 1.3 GB 48640
VSCode 76 sec 1 GB 0.84 GB 7448

Sparse-checkout

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

Например, у нас есть тестовый проект со следующей структурой:

  • Директория клиента, которая содержит зависимости для 3 различных платформ: Android, компьютерных ОС, а также iOS
  • Сервисная часть, которая содержит в себе всю сервисную логику для нескольких независимых микросервисов.
  • Веб-части приложения, в которой хранятся какие-то статические веб странички, используемые JavaScript.

Также стоит отметить, что в корне проекта лежат несколько файлов, это важно учесть.

test_project_structure

Каждый из этих подмодулей может иметь вложенность вплоть до 10 уровня при этом отдельному разработчику, например, необходимо работать только с определённым модулем.

Для того, чтобы создать видимость работы только с ним, необходимо выполнить следующий набор команд, перейдя в папку проекта:

$ cd <target>
$ git sparse-checkout init --cone
$ git sparse-checkout set <dir1> <dir2> ... <dirN>
1. Первая команда приводит вид репозитория к абсолютно пустому проекту, который включает в себя лишь файлы, находящиеся в корне проекта (например, README.md, .gitignore и т.д.).
2. Вторая команда задает директории, которые мы хотим видеть в проекте, причем директории будут добавляться на основании регулярных выражений-флагов, которые похожи на флаги, используемые в .gitignore файле.
В нашем случае разработчику нужна только директория client/android, поэтому набор комангд будет выглядеть так:

$ cd test_project
$ git sparse-checkout init --cone
$ git sparse-checkout set client/android
В таком случае маска добавления директорий в проекте будет выглядеть вот так:
/*
!/*/ 
/client/
!/client/*/
/сlient/android/ 

ВАЖНО: Т.к. данная команда была использована на уже имеющемся полном репозитории, мы имеем именно видимость работы с маленьким репозиторием, хотя на самом деле абсолютно все файлы проекта уже у нас есть, поэтому для получения максимальной выгоды необходимо выполнить клонирование в стиле sparse mode.

Клонирование в стиле sparse mode

Для того, чтобы клонировать пустой репозиторий, не переключаясь на рабочую ветку, необходимо поставить флаг --no-cheсkout, выглядеть код будет следующим образом:

$ git clone --no-checkout https://gitflic.ru/project/user/test_project.git
$ cd test_project
$ git sparse-checkout init --cone
$ git checkout master
В таком случае мы получим код, который будет иметь состояние последнего коммита, при этом не имея никаких подмодулей, только файлы, расположенные в корне проекта.

Как работать только с необходимым модулем без лишних файлов?

Предположим, мы являемся андроид разработчиком. Судя по диаграмме, для нашей полноценной работы нам необходима лишь директория android, которая находится в модуле client. Для того, чтобы не работать с остальными ненужными модулями, мы можем выполнить так называемое разряженное клонирование

$ git clone --no-checkout https://gitflic.ru/user/test_project.git
$ cd test_project
$ git sparse-checkout init cone
$ git sparse-checkout set client/android
$ git checkout master

android_sparse_clone

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

Плюсы и минусы использования sparse-checkout

Основным и самым большим плюсом является то, что при использовании sparse-checkout команда git pull будет обновлять не все файлы, которые есть в проекте, а только те, которые находятся в границах sparse-checkout.

Так, весь Linux репозиторий имеет 91347 файлов, и даже если не во все из них вносились изменения, то при вызове команды git pull git вытянет все файлы и применит изменения к локальной версии. В нашем же случае вместо обновления 91347 файлов необходимо обновить лишь 2849 файлов, что в десятки раз меньше, а значит и выполнится гораздо быстрее.

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

Для получения максимально выгодного результата можно комбинировать sparse-checkout и partial clone.

Совместное использование partial clone и sparse checkout.

Первоначально, для понимания что из себя представляет partial clone, перейдите по ссылке и прочитайте о нем отдельно.

Вкратце - команда partial clone позволяет клонировать не весь репозиторий целиком, а только какую-то его часть в зависимости от параметров, переданных во флаге --filter.

Рассмотрим наш вариант: выполним клонирование репозитория с флагом --filter=blob:none при этом не переключаясь на ветку, на которую указывает HEAD (в нашем случае ветка master), для этого добавим флаг --no-checkout

Выполним все необходимые команды:

$ git clone --filter=blob:none --no-checkout https://gitflic.ru/project/user/test_project.git
Клонирование в «test_projectl»...
remote: Counting objects: 10226773, done
remote: Finding sources: 100% (7443598/7443598)
remote: Getting sizes: 100% (341114/341114)
remote: Compressing objects: 100% (29493/29493)
remote: Total 7443598 (delta 5984837), reused 7443571 (delta 5984826)
Получение объектов: 100% (7443598/7443598), 1.45 ГиБ | 8.95 МиБ/с, готово.
Определение изменений: 100% (5984837/5984837), готово.
$ cd test_project
$ git sparse-checkout init --cone
$ git checkout master 
remote: Counting objects: 15, done
remote: Finding sources: 100% (15/15)
remote: Getting sizes: 100% (15/15)
remote: Compressing objects: 100% (972780/972780)
remote: Total 15 (delta 0), reused 10 (delta 0)
Получение объектов: 100% (15/15), 262.31 КиБ | 9.71 МиБ/с, готово.
Updating files: 100% (15/15), готово.
Уже на «master»
Эта ветка соответствует «origin/master».
$ git sparse-checkout set client/android
remote: Counting objects: 52, done
remote: Finding sources: 100% (52/52)
remote: Getting sizes: 100% (51/51)
remote: Compressing objects: 100% (378195/378195)
remote: Total 52 (delta 1), reused 22 (delta 1)
Получение объектов: 100% (52/52), 116.16 КиБ | 2.42 МиБ/с, готово.
Определение изменений: 100% (1/1), готово.
Updating files: 100% (52/52), готово.
Таким образом, теперь мы имеем проект, в котором есть только файлы, лежащие в корне нашего проекта, а также все файлы, связанные с подмодулем client/android.

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

Таблица 2. Результат выполнения partial clone совместно с sparse-checkout

Репозиторий Время клонирования Размер проекта Размер репозитория
Linux kernel 145 sec 1.8 GB 1.7 GB
Rust 45 sec 526 MB 509 MB
VSCode 16 sec 311 MB 227 MB

Как можно заметить, время клонирования уменьшилось в 6-7 раз по сравнению с клонированием всего репозитория, при этом размер проекта уменьшился практически в 4 раза, поскольку теперь мы имеешь лишь необходимые для работы файлы.

Как запустить такой проект?

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

Что такое partial clone и как работать с --filter?

Как уже описывалось выше, команда partial clone совместно с флагом --filter позволяет копировать лишь часть репозитория.

Для удобства введем символьные обозначения объектов git:

  1. Blob - блобы будут обозначаться квадратами. Представляют из себя содержимое файлов.
  2. Tree - объекты дерева будут обозначаться треугольниками. Представляют и себя директории в проекте.
  3. Commit - коммиты будут обозначаться кругами. Это снимки состояния репозитория в момент времени.

blob_tree_commit

Существует 3 основных способа уменьшить размер клонируемого репозитория: * git clone --filter=blob:none <url> создает так называемый "Клон без блобов" (blobless clone). Такой способ лучше всего подходит для постоянной разработки. * git clone --filter=tree:0 <url> создает так называемый "Клон без деревьев" (treeless clone). Такие проекты отлично подходят для случаев, когда проект будет удален после единоразовой сборки, но при этом необходим доступ к истории изменений (по сути графу коммитов). * git clone --depth=1 <url> Создает поверхностный клон (shallow clone). Разберем каждый способ отдельно. Такой способ подходит только для единоразовой сборки, постоянная разработка в таком случае слишком неудобна и дорога с точки зрения догрузки объектов.

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

Клонирование без блобов (Blobless clone)

Данный способ загружает информацию по всем доступным из корня репозитория коммитам и деревьям БЕЗ блобов, при этом загружает сами блобы "по запросу". Это значит, что первоначально блобы не буду загружены, их загрузка произойдёт при непосредственном обращении к ним при выполнении команды git checkout. Это также включает в себя первый checkout при выполнении команды git clone.

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

Стоит добавить, что такие команды, как git log или git merge-base не провоцируют загрузку блобов и выполнение этих команд будет давать достоверный результат, ведь мы загружаем всю историю коммитов.

Схематично это будет выглядеть следующим образом:

bloblessClone

В последующем, при использовании команд git fetch или git pull с удаленного репозитория будет приходить информация только об изменениях в коммитах или деревьях. Новые блобы будут загружаться ТОЛЬКО при использовании git checkout.

Загрузка блобов будет вызываться в любом случае, когда нужно будет содержимое файлов, поэтому такие команды, как git diff или git blame <path> будут вызывать эту самую загрузку блобов. Однажды загруженные блобы остаются в репозитории, поэтому любая загрузка требует единожды прогрузить содержимое файлов.

Выполним загрузку linux репозитория таким способом:

$ git clone --filter=blob:none https://gitflic.ru/project/user/linux_kernel.git
Клонирование в «linux_kernel»...
remote: Counting objects: 10325225, done
remote: Finding sources: 100% (7517591/7517591)
remote: Getting sizes: 100% (343546/343546)
remote: Compressing objects: 100% (3778/3778)
remote: Total 7517591 (delta 6046838), reused 7517585 (delta 6046836)
Получение объектов: 100% (7517591/7517591), 1.46 ГиБ | 8.43 МиБ/с, готово.
Определение изменений: 100% (6046838/6046838), готово.
remote: Counting objects: 85266, done
remote: Finding sources: 100% (85266/85266)
remote: Getting sizes: 100% (76628/76628)
remote: Compressing objects: 100% (493686/493687)
remote: Total 85266 (delta 8707), reused 68251 (delta 8638)
Получение объектов: 100% (85266/85266), 250.81 МиБ | 5.01 МиБ/с, готово.
Определение изменений: 100% (8707/8707), готово.
Updating files: 100% (85764/85764), готово.
Как можно заметить, исходя из выполнения запроса, после клонирования при переключении на ветку, которую указывает HEAD, сразу происходит догрузка необходимых блобов, поэтому мы имеем несколько "получений объектов". Результаты, получаемые при таком способе клонирования, представлены ниже

Таблица 3. Результат выполнения git clone --filter=blob:none

Репозиторий Время клонирования Размер проекта Размер репозитория
Linux kernel 177 sec 3.5 GB 2 GB
Rust 49 sec 816 MB 542 MB
VSCode 16 sec 360 MB 233 MB

Клонирование без деревьев (Treeless clone)

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

treelessClone

Как и при клонировании без блобов, коммит, на который указывает HEAD, загружается полностью, вместе со всеми деревьями и блобами, но все остальные объекты имеют только данные о самом коммите. Это означает, что клонирование без деревьев может выполниться ЗНАЧИТЕЛЬНО БЫСТРЕЕ, чем клонирование без блобов или полное клонирование.

Аналогично клонированию без блобов, команды git pull и git fetch будут клонировать только информацию об изменениях в коммитах. Однако, работа в таком ключе будет сложнее, поскольку загружать дополнительную информацию для деревьев с блобами тяжелее и дороже, нежели загружать только блобы.

Аналогично, команда git checkout является триггером для загрузки деревьев из удаленного репозитория, причем, запрашивая корневое дерево для коммита, git также запрашивает и все остальные деревья, которые находятся внутри этого корневого дерева, что и составляет всю дороговизну использования. Команды git merge-base и git log используют только информацию о коммитах, поэтому их использование не будет являться триггером для загрузки деревьев. Однако использование команды git log --<path> будет загружать деревья для всех достижимых коммитов, это стоит помнить!

Если в проекте есть сабмодули, то клонирование без деревьев не очень хорошо с ними работает, поскольку вызов git fetch будет провоцировать загрузку всех деревьев для коммитов, поскольку git будет пытаться найти изменения для сабмодулей. Чтобы избежать этого, необходимо прописать следующее в проекте:

$ git config fetch.recurseSubmodules false

Выполним клонирование без деревьев для репозитория linux и остальных репозиториев, а также посмотрим на результаты.

$ git clone --filter=tree:0 https://gitflic.ru/project/user/linux_kernel.git
Клонирование в «linux_kernel»...
remote: Counting objects: 2572793, done
remote: Finding sources: 100% (1293369/1293369)
remote: Total 1293369 (delta 166160), reused 1293367 (delta 166160)
Получение объектов: 100% (1293369/1293369), 660.71 МиБ | 8.69 МиБ/с, готово.
Определение изменений: 100% (166160/166160), готово.
remote: Counting objects: 90910, done
remote: Finding sources: 100% (5644/5644)
remote: Getting sizes: 100% (5635/5635)
remote: Compressing objects: 100% (2664734/2664734)
remote: Total 5644 (delta 11), reused 3239 (delta 9)
Получение объектов: 100% (5644/5644), 2.64 МиБ | 17.89 МиБ/с, готово.
Определение изменений: 100% (11/11), готово.
remote: Counting objects: 85266, done
remote: Finding sources: 100% (85266/85266)
remote: Getting sizes: 100% (76628/76628)
remote: Compressing objects: 100% (493686/493687)
remote: Total 85266 (delta 8707), reused 68251 (delta 8638)
Получение объектов: 100% (85266/85266), 250.81 МиБ | 8.94 МиБ/с, готово.
Определение изменений: 100% (8707/8707), готово.
Updating files: 100% (85764/85764), готово.

Аналогично клонированию без блобов, мы имеем несколько "получений объектов", поскольку выполняем checkout в контексте выполнения команды git clone, что видно, исходя из кода.

Таблица 4. Результат выполнения git clone --filter=tree:0

Репозиторий Время клонирования Размер проекта Размер репозитория
Linux kernel 52 sec 2.5 GB 978 MB
Rust 10 sec 422 MB 149 MB
VSCode 4 sec 199 MB 73 MB

Поверхностное клонирование (Shallow clone)

Partial clone относительно новая вещь для git в сравнении с поверхностным клонированием. Поверхностное клонирование использует флаг --depth=<N> для того, чтобы обрезать историю коммитов. Например, при --depth=1 мы получим проект с одним коммитом. Лучше всего использовать это совместно с флагами --single-branch --branch=<branch>, чтобы гарантировать загрузку данных только одного коммита, который нам нужен здесь и сейчас.

Схема проекта в таком случае выглядит так:

shallowClone

Из-за того, что история коммитов обрезана, команды git merge-base и git log будут давать не тот результат, который вы получите при полном клонировании.

Другая особенность заключается в работе команды git fetch при использовании поверхностного клонирования. При отправке новых коммитов сервер должен предоставить каждое дерево и блоб, которые являются «новыми» для этих коммитов по сравнению с поверхностными коммитами. Это вычисление может быть более дорогостоящим, чем обычная выборка. В зависимости от того, как другие разработчики вносят свой вклад в ваш удаленный репозиторий, операция git fetch в поверхностном клоне может в конечном итоге загрузить почти полную историю коммитов.

Собственно поэтому не рекомендуется использовать поверхностное клонирование кроме случаев, когда репозиторий будет удален после единоразовой сборки.

Выполним поверхностное клонирование репозиториев и посмотрим на результаты:

$ git clone --depth=1 https://gitflic.ru/project/user/linux_kernel.git                                  
Клонирование в «linux_kernel»...
remote: Counting objects: 90911, done
remote: Finding sources: 100% (90911/90911)
remote: Getting sizes: 100% (82263/82263)
remote: Compressing objects: 100% (496274/496289)
remote: Total 90911 (delta 9510), reused 71490 (delta 8647)
Получение объектов: 100% (90911/90911), 253.50 МиБ | 8.59 МиБ/с, готово.
Определение изменений: 100% (9510/9510), готово.
Updating files: 100% (85764/85764), готово.

Таблица 5. Результат выполнения git clone --depth=1

Репозиторий Время клонирования Размер проекта Размер репозитория
Linux kernel 19 sec 1.8 GB 268 MB
Rust 2.7 sec 314 MB 41 MB
VSCode 2 sec 147 MB 21 MB

Итоги

Пробежимся тезисно по разобранным командам:

  • Поверхностное клонирование удаляет историю коммитов, из-за чего ломается git log и git merge-base. Никогда не выполняйте git fetch, если используте его.
  • Клонирование без деревьев содержит только историю коммитов, но загружать новые деревья достаточно дорогостояще. Однако, при таком способе git log и git merge-base работают корректно, но команды по типу git log -- <path> очень медленные и не рекомендуются к использованию при таком клонировании.
  • Клонирование без блобов содержит в себе все достижимые коммиты и деревья, поэтому git загружает только содержимое файлов при необходимости. Это означает, что команды git blame медленнее при их первом вызове. Однако, это отличный способ начать работу с огромным репозиторием с большим количеством крупных старых файлов.
  • Полное копирование работает как ожидается от него. Однако, время загрузки всех данных и место, занимаемое на диске, слишком велико.