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


Итак, что дано:


  • Есть большой репозиторий, содержащий множество папок. Каждая папка – это отдельный проект.

Что необходимо сделать:


  • Одну из папок перенести в отдельный репозиторий с сохранением ее истории коммитов.

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


Я использовал стандартный гитовый filter-branch. За основу я взял следующие статьи:



В этом посте я хочу немного адаптировать процесс для лучшего восприятия.


Предположим для примера, что наш репозиторий называется movement-example, а та единственная папка, которую мы хотим перенести в отдельный репозиторий – folder-to-move. Тогда шаги, которые необходимо выполнить для подготовки переноса, выглядят следующим образом:


  1. git clone git@github.com:<user_or_organization>/movement-example.git
    Лучше сделать новый клон репозитория, даже если он у вас уже скачан. А еще лучше делать клон из локального репозитория: git clone <path-to-movement-example> – это гораздо быстрее (спасибо ZyXI за подсказку).
  2. cd movement-example
  3. git remote rm origin
    Это именно тот момент, ради которого мы делали новый клон – теперь мы не боимся поломать оригинальный репозиторий.
  4. git filter-branch --subdirectory-filter folder-to-move -- --all
    После выполнения этого шага в вашем локальном репозитории останется только контент папки folder-to-move, причем самой папки больше нет – все содержащиеся в ней файлы теперь лежат в текущей директории (в movement_example).
  5. mkdir folder-to-move
    mv * folder-to-move
    Это опциональный шаг – если вы хотите иметь все файлы внутри той же папки, что и раньше, а не в корне нового репозитория.
  6. git add .
  7. git commit -m “Preparing to extract folder”

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


  1. git clone git@github.com:<user_or_organization>/new-repo.git
    Клонируем себе новый репозиторий, если, конечно, все еще не сделали этого.
  2. cd new-repo
  3. git remote add old-repo-branch <path-to-movement-example-folder>
    Добавляем новый remote. Если ваши папки new-repo и movement-example лежат на одном уровне в файловой системе, то path-to-movement-example-folder выглядел бы как ../movement-example
  4. git pull old-repo-branch master
    После выполнения этого шага цель будет практически достигнута – у вас уже будет весь контент folder-to-move в локальном репозитории new-repo, останется только сделать push. Но сначала нужно сделать кое-что еще.
  5. git remote rm old-repo-branch
    Вам же больше не нужен второй remote, верно?
  6. git push origin master

Готово! Теперь в вашем новом репозитории есть только интересующая вас папка вместе со всей историей коммитов. Например, сразу после вышеописанных шагов я увидел следующее в своем новом репозитории:


810 коммитов!


Кстати, вы увидите только 1 бранч – master. Процедура переносит только один бранч за раз. Если вы хотите перенести dev, то вам нужно просто сделать git checkout dev и git pull origin dev после второго шага на обоих этапах.


Если вам нужно перенести все 50 (60? 100?) бранчей, то данное решение не будет удачным из-за слишком большого количества рутинной работы. Но я считаю, что для переноса достаточно лишь сохранить master и dev бранчи, потому что все feature branches уже должны быть в dev, а новые бранчи вы будете ветвить уже в новом репозитории.


UPDATE

Спасибо fstep за подсказку. Можно просто воспользоваться другой гитовой утилитой – subtree. Для этого нужно всего ничего:


  1. git clone <path-to-movement-example>
  2. git remote rm origin
    Обратите внимание – эти 2 шага опциональны, вы можете выполнять следующие шаги напрямую из вашего "боевого" локального репозитория. Иметь клон с удаленным origin – мое личное предпочтение, чтобы даже не иметь шанса как-то сломать оригинальный репозиторий.
  3. git subtree split --prefix folder-to-move master
    Или любой другой бранч, кроме master. Эта команда будет долго вычислять значения, но в итоге вернет вам что-то вроде 253f8a5edd9a4dbbb1d72e5837243e93c92ebfcd
  4. git push git@github.com:<user_or_organization>/new-repo.git 253f8a5edd9a4dbbb1d72e5837243e93c92ebfcd:master --force
    Здесь мы пушим с флагом --force, потому что new-repo наверняка будет иметь как минимум readme файл, который вы (вероятно) не захотите пуллить.

Ну, а если new-repo является совсем свежим и не содержит даже readme файла – другими словами, вообще не содержит файлов, то все еще проще:


git subtree push --prefix folder-to-move git@github.com:<user_or_organization>/new-repo.git master

Поделиться с друзьями
-->

Комментарии (23)


  1. Zapped
    10.04.2017 23:51

    Если вам нужно перенести все 50 (60? 100?) бранчей, то данное решение...

    автоматизируется скриптом :)


  1. fstep
    10.04.2017 23:59
    +3

    А разве команда

    git subtree push --prefix folder-to-move remote-name branch-name 
    
    не сделает то же самое?


    1. tmn4jq
      11.04.2017 00:51

      Попробовал – делает то, что надо, спасибо! Укажу это в посте


      1. fstep
        11.04.2017 00:59

        Часто использую эту команду, чтобы задеплоить сборку веба на продакшн.
        Не уверен, подтягивается ли история коммитов?


        1. tmn4jq
          11.04.2017 01:02

          Да, подтянулась


  1. Grief
    11.04.2017 00:30

    Могу порекомендовать тулзу BFG, она может удалять файлы из истории типа случайно закоммиченных паролей или просто огромных файлов, которые стали не нужны, но занимают место в истории. Она, к сожалению, не умеет выпиливать по правилу "все, кроме", но можно написать простенький скрипт, чтобы из git'а вытащить список всех файлов во всех бранчах, отсортировать, выбрав уникальные, вычеркнуть sed'ом те, которые планируется оставить, а потом скормить результат bfg.


  1. ZyXI
    11.04.2017 00:32

    А зачем в новом репозитории делать git remote add+git remote rm? Git спокойно принимает вместо названия remote значение, на которое это название ссылается.


    К тому же, кажется, вы чего?то накосячили при клонировании или pull’е: в документации filter-branch ясно сказано, что -- --all переписывает все тёги и ветки, вам нужно просто их все склонировать а потом так же все затянуть в новом репозитории, а не заниматься скриптописательством или рутинной работой.


    И clone в первом шаге лучше делать локального репозитория — это быстрее, а git не будет что?то в оригинале менять, хотя и воспользуется по возможности жёсткими ссылками. Ещё при желании можно cp -r локальные клоны делать, хотя лучше не надо — притянет мусорные изменения. Но все ветки и тёги, с которыми вы работали будут на месте точно.


    1. ZyXI
      11.04.2017 00:43

      Хотя я вижу: вы точно накосячили при pull и push: тянете только master, явно. Git даёт затянуть всё сразу:


      1. В new-repo делаете git checkout -b xxx-temporary-branch-xxx — вам нужна ветка, которой нет нигде, чтобы git не вопил, что он не может затянуть что?то в текущую ветку.
      2. git fetch path/to/movement-example '*:*': затягиваете всё, но ничего не переименовывается.
      3. git checkout master: чтобы удалить ветку нужно перейти куда?то ещё, не важно куда
      4. git branch -D xxx-temporary-branch-xxx
      5. git push origin '*:*': отправляете всё, что затянули при fetch.


      1. ZyXI
        11.04.2017 00:48

        Если не ошибаюсь, то с голыми репозиториями (git clone --bare) 1, 3 и 4 не нужны.


        1. ZyXI
          11.04.2017 00:53

          И вообще вы занимаетесь фигнёй: делайте git push 'git@github.com:<user_or_organization>/new-repo.git' '*:*' прямо из movement-example. Новый клон не нужен, создавать remote не нужно, временная ветка не нужна, push’ити всё сразу; потом сделаете себе клон для работы уже с нужными изменениями или сделаете pull с github из имеющегося.


          1. tmn4jq
            11.04.2017 01:01

            С таким же успехом можно просто через github UI сделать то же самое – полная копия. Мне нужна одна конкретная папка


            1. ZyXI
              11.04.2017 01:42

              Какая ещё полная копия?! После filter-branch там уже только нужные коммиты. Или вы думаете, что изменения в ветке master магическим образом изменятся во время pull, из?за чего мой вариант не эквивалентен использованию промежуточного репозитория?


              1. ZyXI
                11.04.2017 01:52

                Хотя одну вещь в моих советах нужно изменить: везде, где одна команда с '*:*' нужно использовать две: 'refs/heads/*:refs/heads/*' и 'refs/tags/*:refs/tags/*'. Иначе получите дополнительно всю оригинальную историю, т.к. filter-branch сохраняет её в refs/original.


      1. tmn4jq
        11.04.2017 00:50

        Да, спасибо – это оптимальнее будет. Я попробую это все и обновлю пост


  1. VolCh
    11.04.2017 09:18

    Серьёзно думаю над обратным процессом. Как-то не стрельнула идея в SOA-приложении выделить каждый сервис в отдельный репозиторий, объединив их через submodule.


    1. KIVagant
      11.04.2017 13:06
      +1

      Попробуйте такой подход: https://habrahabr.ru/post/326132/#comment_10165486

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


    1. zloddey
      11.04.2017 20:02
      +3

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


  1. Alesh
    11.04.2017 12:59

    Спасибо, нужная вещь. Ну и раз уж пошла тема спрошу свое, может подскажет кто.
    Не так давно хотелось сделать в репе submodule, но что бы в него затягивался не весь целевой репозиторий, а одна папка, к примеру include из большого С/С++ проекта, пусть только в read-only, но с сохранением возможности обновления как обычный submodule. Возможно ли такое?


    1. KIVagant
      11.04.2017 13:07

      Гит в такое не играет, смотрите ниже как можно выкрутиться.


  1. KIVagant
    11.04.2017 13:00
    +1

    Git Subsplit

    # This utility uses `git-subsplit`. If you don't have one, run this:
    # pusd /tmp/ && git clone git@github.com:dflydev/git-subsplit.git && cd git-subsplit && ./instal.sh && popd
    #
    # This utility also uses linux "parallel"
    # To install a fresh version, run:
    # Ubuntu:
    # apt-get -y install bzip2 make && (wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash
    #
    # MacOS:
    # brew install parallel
    
    git subsplit init git@github.com:your/big-project.git
    
    modules_array=(
        'your/subfolder/to/split:git@github.com:your/new_microservice.git',
        'your/another/subfolder/to/split:git@github.com:your/other_microservice.git',
    )
    
    printf '%s\n' "${modules_array[@]}" | parallel "git subsplit publish --heads='master staging development' --no-tags"
    
    rm -rf .subsplit/
    


    Делаете other_microservice.git проектом с READ-ONLY доступом и можете спокойно коммитить в основной проект, запуская утилиту в CI-процессе автоматически. В --heads можно подставлять текущую ветку с помощью чего-то вроде

    ```$(git symbolic-ref --short HEAD)```

    Пример аналогичного подхода: https://github.com/laravel/framework как основной код и https://github.com/illuminate/queue как пакет, автоматически вырезаемый из основного (наряду с другими)


  1. dude_sam
    11.04.2017 14:08

    Не раскрыта тема куда девается передвигаемая папка из первоначального репозитория. — Удаляется?


    1. tmn4jq
      11.04.2017 14:19

      Нет, с ней ничего не происходит – остается на месте :)


    1. ZyXI
      11.04.2017 14:19

      А что с ней станет? С клоном же работаете, push в origin не просто не делается — origin вообще удалён на третьем шаге.