Внешние зависимости в гите: submodule или subtree?

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

    У моего проекта несколько зависимостей. Бóльшая часть зависимостей живет в гитовых репозиториях. Сам проект тоже живёт в гите.

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

    Раньше я просто копировал зависимости в папку проекта, и добавлял к каждой файл VERSION.TXT с её версией. Но, если нужно работать с текущей версией стороннего кода, это неудобно. Да и копировать файлы руками когда есть гит как-то глупо. Хочется найти более современное решение.

    Самая разрекламированная и модная фича гита для работы со сторонними репозиториями — git submodules («подмодули»). Естественно, в первую очередь я стал смотреть на неё.

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

    Однако, как только я попытался перенести свой опыт на более серьёзный рабочий процесс, выяснилось, что не всё так просто.

    Вот с чем мы столкнулись:
    • Каждый человек, работающий с основным репозиторием должен иметь доступ к репозиторию, из которого взят подмодуль.
    • В общем случае невозможно получить целостную рабочую копию одной командой. Теперь после git checkout нужно делать git submodule update --init.
    • То же относится и некоторым другим командам гита. Например, git archive игнорирует подмодули — больше нельзя одной командой запаковать весь проект в архив.
    • Из проекта верхнего уровня не видно изменений внутри подмодулей и наоборот. Чтобы узнать полного состояние рабочей копии проекта, необходимо запрашивать его для каждого подмодуля и для родительского проекта по отдельности. Без подмодулей достаточно сказать git status в любом месте внутри рабочей копии.
    • После замены корневой директории подмодуля на что-нибудь другое (например другой подмодуль), нужно вручную удалять старую версию во всех рабочих копиях.
    • Команда git submodule не понимает стандартных опций --git-dir и --work-tree. Её можно запускать только из корня рабочей копии. Это затрудняет автоматизацию.
    В общем достаточно неприятно. Каждую из проблем можно так или иначе обойти, но проблем слишком много и постоянно появляются новые.

    От подмодулей пришлось отказаться.

    Но нет худа без добра. Знающие люди подсказали, что в гите с давних пор (ещё до 1.5.2) существует альтернативное решение — subtree merge strategy («поддеревья»).

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

    Пример из документации: добавляем код из ветки master репозитория Bproject (лежит в /path/to/B) в наш проект в поддиректорию dir-B/.

    $ git remote add -f Bproject /path/to/B
    $ git merge -s ours --no-commit Bproject/master
    $ git read-tree --prefix=dir-B/ -u Bproject/master
    $ git commit -m "Merge B project as our subdirectory"

    Нужно обратить внимание на ключ -f у git remote add. Он говорит гиту сразу сделать fetch этого remote-а.

    В дальнейшем, изменения в Bproject подтягиваются командой git pull с явным указанием нужной ветки и стратегии объединения:

    $ git pull -s subtree Bproject master

    Если в рабочей копии не добавлен соответствующий remote-у поддерева, в истории основного репозитория не показывается имя ветки, из которой притянуты изменения.

    Проблема чисто косметическая, и на работу не влияет. Лечится добавлением этого remote-а в рабочую копию:

    $ git remote add -f Bproject /path/to/B

    В дальнейшем, если появляются новые ветки, можно подтягивать изменения в remote:

    $ git fetch Bproject

    Есть ещё пара недостатков:
    1. Как и в случая с обычным объединением веток, в логах коммитов история поддерева перемешивается с историей основного проекта.
    2. Сабмитить изменения в проект поддерева значительно сложнее чем с подмодулями. Но это легко обойти, внося изменения в отдельный клон этого проекта.
    Ни один из перечисленных недостатков не является критическим для нашего рабочего процесса.

    Работать с поддеревьями значительно удобнее чем с подмодулями. Для неё не нужно переучивать пользователей, её проще автоматизировать. Поддеревья проще поддерживать. Рекомендую.

    Кстати, на Гитхабе есть проект, нацеленный на развитие работы с поддеревьями: git-subtree.

    Дополнительное чтение:
    1. Pro Git: Submodules
    2. Pro Git: Subtree Merging
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 14
    • +1
      Спасибо, автор
    • +1
      Всем спасибо за карму, перенёс в тематический блог. :-)
      • –1
        0_0 Wow!
        • 0
          Большое спасибо! Про поддеревья не знал, а очень надо было ;)
        • 0
          Как-то вы всё усложняете.

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

          Если хотите скажем обновить все репозитарии начиная с корневого каталога одной командой, создайте alias gitpull='git pull && cd vendor/plugins/prawnto; git pull'

          Преимущества понятны?
          • –1
            Автор так жалко времени, потраченного на возню с git submodules и с subtree merge strategy, что он не готов принять лежащее на поверхности решение и минусует ;-(
            • +1
              Если я правильно понимаю ситуацию, предложенное Вами решение в действительности мало отличается от использования подмодулей. Кроме того, что подмодули на самом деле попытка «легитимизации» такого подхода.

              Список претензий к подмодулям есть в статье. Ваш подход хоть одну из них снимает?
              • –2
                Мой подход претензии эти игноритует. Поскольку я умею автоматизировать свою работу, то я предпочитаю простой и понятный способ использования git, а когда вижу длинный набор команд, то сокращаю его путём встроенных в ОС средств.

                Скажем, чтобы решить одну из ваших проблем, когда «из проекта верхнего уровня не видно изменений внутри подмодулей и наоборот», задать alias вида

                alias gitstatus='find. -type d -name '.git'|while read i; do cd "$i"/..;git status; done'

                для запуска из корня проекта.
                • 0
                  Ну, я ведь не просто так претензии эти написал. Для меня они существенны, и вариант с альясами по ряду причин неприемлем. Если для Вас он проще — так это же хорошо! Я же никого не заставляю пользоваться поддеревьями. Каждый выбирает то, что ему лучше подходит.
          • 0
            Очень интересное решение. Спасибо!
          • +2
            Сабмитить изменения в проект поддерева значительно сложнее чем с подмодулями. Но это легко обойти, внося изменения в отдельный клон этого проекта.
            Кстати, на Гитхабе есть проект, нацеленный на развитие работы с поддеревьями: git-subtree.

            А тем временем на ГитХабе появился форк, позволяющий делать push поддерева :-) github.com/tickzoom/git-subtree

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