Как разбить репозиторий git при сохранении подкаталогов?

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

У меня есть это:

foo/
  .git/
  bar/
  baz/
  qux/

И я хочу разделить его на два совершенно независимых репозитория:

foo/
  .git/
  bar/
  baz/

quux/
  .git/
  qux/  # Note: still a subdirectory

Как это сделать в git?

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

5 ответов

Решение

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

Вот пример со страницы руководства:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD

Это просто удаляет одно имя файла; что вы хотите сделать, это удалить все, кроме данного подкаталога. Если вы хотите быть осторожными, вы можете явно перечислить каждый путь для удаления, но если вы хотите пойти ва-банк, вы можете просто сделать что-то вроде этого:

git filter-branch --index-filter 'git ls-tree -z --name-only --full-tree $GIT_COMMIT | grep -zv "^directory-to-keep$" | xargs -0 git rm --cached -r' -- --all

Я ожидаю, что есть, вероятно, более элегантный способ; если у кого-то есть что-то, пожалуйста, предложите это!

Несколько замечаний по этой команде:

  • ветвь фильтра внутренне устанавливает GIT_COMMIT на текущий коммит SHA1
  • Я бы не ожидал --full-tree быть необходимым, но, очевидно, ветвь фильтра запускает индексный фильтр из .git-rewrite/t каталог вместо верхнего уровня репо.
  • grep, вероятно, излишний, но я не думаю, что это проблема скорости.
  • --all применяет это ко всем ссылкам; Я полагаю, вы действительно этого хотите. (-- отделяет его от параметров ответвления фильтра)
  • -z а также -0 скажите ls-tree, grep и xargs использовать завершение NUL для обработки пробелов в именах файлов.

Отредактируйте намного позже: Томас услужливо предложил способ убрать пустые коммиты, но теперь он устарел. Посмотрите историю изменений, если у вас есть старая версия git, но с современным git все, что вам нужно сделать, это воспользоваться этой опцией:

--prune-empty

Это удалит все коммиты, которые пусты после применения фильтра индекса.

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

#!/bin/bash

# usage:
# git filter-branch --prune-empty --index-filter \
# 'this-script file-with-list-of-files-to-be-kept' -- --all

if [ -z $1 ]; then
    echo "Too few arguments."
    echo "Please specify an absolute path to the file"
    echo "which contains the list of files that should"
    echo "remain in the repository after filtering."
    exit 1
fi

# save a list of files present in the commit
# which is currently being modified.
git ls-tree -r --name-only --full-tree $GIT_COMMIT > files.txt

# delete all files that shouldn't be removed
while read string; do
    grep -v "$string" files.txt > files.txt.temp
    mv -f files.txt.temp files.txt
done < $1

# remove unwanted files (i.e. everything that remained in the list).
# warning: 'git rm' will exit with non-zero status if it gets
# an invalid (non-existent) filename OR if it gets no arguments.
# If something exits with non-zero status, filter-branch will abort.
# That's why we have to check carefully what is passed to git rm.
if [ "$(cat files.txt)" != "" ]; then
    cat files.txt | \
    # enclose filenames in "" in case they contain spaces
    sed -e 's/^/"/g' -e 's/$/"/g' | \
    xargs git rm --cached --quiet
fi

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

Используйте git-filter-repo. Начиная с версии 2.25 это не является частью git. Для этого требуется Python3 (>=3.5) и git 2.22.0

mkdir new_repoA
mkdir new_repoB
git clone originalRepo newRepoA
git clone originalRepo newRepoB

pushd
cd new_repoA
git filter-repo --path foo/bar --path foo/baz

popd
cd new_repoB 
git filter-repo --path foo/qux

Для моего репо, содержащего ~12000 коммитов, git-filter-branch заняло более 24 часов, а git-filter-repo заняло менее минуты.

Это то, что я в конечном итоге сделал, чтобы решить эту проблему, когда у меня было это самостоятельно:

git filter-branch --index-filter \
'git ls-tree --name-only --full-tree $GIT_COMMIT | \
 grep -v "^directory-to-keep$" | \
 sed -e "s/^/\"/g" -e "s/$/\"/g" | \
 xargs git rm --cached -r -f --ignore-unmatch \
' \
--prune-empty -- --all

Решение основано на ответе Джефроми и на подкаталоге Detach (переместить) в отдельный репозиторий Git, а также на множестве комментариев здесь о SO.

Причина, по которой решение Джефроми не сработало для меня, заключалась в том, что в моем репо были файлы и папки, имена которых содержали специальные символы (в основном пробелы). Дополнительно git rm пожаловался на несопоставленные файлы (решается с помощью --ignore-unmatch).

Вы можете оставить фильтр независимым от каталога, который не находится в корне репозитория или не перемещается:

grep --invert-match "^.*directory-to-keep$"

И, наконец, вы можете использовать это, чтобы отфильтровать фиксированное подмножество файлов или каталогов:

egrep --invert-match "^(.*file-or-directory-to-keep-1$|.*file-or-directory-to-keep-2$|…)"

Чтобы очистить потом вы можете использовать эти команды:

$ git reset --hard
$ git show-ref refs/original/* --hash | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

Более чистый метод:

git filter-branch --index-filter '
                git read-tree --empty
                git reset $GIT_COMMIT path/to/dir
        ' \
        -- --all -- path/to/dir

или придерживаться только основных команд, саб в git read-tree --prefix=path/to/dir/ $GIT_COMMIT:path/to/dir для сброса.

Определение path/to/dir в аргументах rev-list обрезка выполняется рано, с таким дешевым фильтром это не имеет большого значения, но в любом случае стоит избегать ненужных усилий.

Если вы хотите выделить только один каталог как отдельный репозиторий git

git-filter-branch имеет --subdirectory-filter вариант, и это намного проще, чем предыдущие решения, просто:

git filter-branch --subdirectory-filter foodir -- --all

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

Я использовал git-filter-repo с участием filename-callback.

stephen@B450-AORUS-M:~/source/linux$ git filter-repo --force --filename-callback '
  if b"it87.c" in filename:
    return filename
  else:
    # Keep the filename and do not rename it
    return None
  '
warning: Tag points to object of unexpected type tree, skipping.
warning: Tag points to object of unexpected type tree, skipping.
Parsed 935794 commitswarning: Omitting tag 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c,
since tags of trees (or tags of tags of trees, etc.) are not supported.
warning: Omitting tag 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c,
since tags of trees (or tags of tags of trees, etc.) are not supported.
Parsed 937142 commits
New history written in 177.03 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at a57e6edb85a3 treewide: Replace GPLv2 boilerplate/reference with SPDX - rule 157
Enumerating objects: 20210, done.
Counting objects: 100% (20210/20210), done.
Delta compression using up to 12 threads
Compressing objects: 100% (17718/17718), done.
Writing objects: 100% (20210/20210), done.
Total 20210 (delta 1841), reused 20038 (delta 1669), pack-reused 0
Completely finished after 179.76 seconds.

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

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

Rewrite 3e80e1395bd4f410b79dc0f17113f5b6b409c7d8 (329/937142) (8 seconds passed, remaining 22779 predicted)

22779 секунд = 6,3275 часов

Другие вопросы по тегам