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

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

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

foo/
  .git/
  bar/
  baz/
  qux/

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

foo/
  .git/
  bar/
  baz/

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

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

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

Ответ 1

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

Вот пример из справочной страницы:

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 применяет это ко всем refs; Я полагаю, вы действительно этого хотите. (-- отделяет его от опций фильтрации)
  • -z и -0 сообщить ls-tree, grep и xargs, чтобы использовать завершение NUL для обработки пробелов в именах файлов.

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

--prune-empty

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

Ответ 2

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

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

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

Причина, по которой решение Jefromis не помогло мне, заключалось в том, что у меня были файлы и папки в моем репо, чьи имена содержали специальные символы (в основном пробелы). Кроме того, 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

Ответ 3

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

#!/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 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

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

Ответ 4

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

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

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

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