Как передать программно сгенерированный список файлов в `git filter-branch`?

Я отделяю часть репо git для создания нового репо и пытаюсь использовать git filter-branch для сохранения истории файлов, которые перемещаются в новый проект. Я знаю о --subdirectory-filter, но это нехорошее решение, потому что файлы, которые я вынимаю, не отображают чисто в один подкаталог. Лучший вариант, который я нашел до сих пор, --index-filter, используется следующим образом:

git filter-branch -f --index-filter 'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- <list of files>' --prune-empty -f

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

tmp=$(cat ~/to_keep.txt) && git filter-branch -f --index-filter 'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- '$tmp --prune-empty -f

К сожалению, это приводит к

fatal: bad flag '--prune-empty' used after filename

Даже просто эхо файлы, похоже, вызывают проблемы:

tmp=$(echo a.txt b.txt) && git filter-branch -f --index-filter 'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- '$tmp --prune-empty -f
fatal: ambiguous argument 'b.txt': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Я также попытался связать строки ранее:

tmp1=$(echo a.txt b.txt) && tmp2='git read-tree --empty && git reset -q "${GIT_COMMIT}" -- ' && tmp3=${tmp2}${tmp1} && git filter-branch -f --index-filter $tmp3 --prune-empty -f
fatal: ambiguous argument 'read-tree': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Я предполагаю, что это просто конкатенация не происходит, как я ожидаю в оболочке. Кто-нибудь знает, как я могу сделать эту работу? Было бы здорово, если бы вы могли объяснить, что означают эти ошибки. Спасибо.

Ответ 1

Каждый аргумент для различных ...-filter должен быть одной строкой. Эта строка сохраняется как переменная оболочки:

    --index-filter)
            filter_index="$OPTARG"
            ;;

В соответствующей точке ветвь фильтра script (найденная в подкаталоге git-core, например, /usr/libexec/git-core или /usr/local/libexec/git-core) делает следующее:

    eval "$filter_index" < /dev/null ||
            die "index filter failed: $filter_index"

(за исключением фильтра фиксации, который запускается с /bin/sh -c "$filter_commit" ...).

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

Самый простой способ сделать это - начать с вашей исходной команды:

git filter-branch -f --index-filter \
    'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- <list of files>' \
    --prune-empty -f

(который работает, когда у вас есть статический список) и измените его, чтобы извлечь динамический список из ~/to_keep.txt. Я разбил оригинал на три строки частично для показа, но также потому, что теперь мы можем сосредоточиться только на средней линии.

[Изменить для исправления проблемы новой строки, отмеченной в комментарии. Пусть сделайте псевдоним или функцию оболочки, xc, которая переводит символы новой строки в пробелы]

xc() {
    tr '\n' ' '
}

"git read-tree --empty && git reset -q \"\${GIT_COMMIT}\" -- $(xc < ~/to_keep.txt)" \

или

'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- '"$(xc < ~/to_keep.txt)" \

или, как вы пытались (но с одним изменением):

'git read-tree --empty && git reset -q "${GIT_COMMIT}" -- '"$tmp" \

(задав tmp=$(xc < ~/to_keep.txt)).

Обратите внимание, что ни одна из этих правильных вещей, если какое-либо из имен файлов содержит пробел. Например, предположим, что файл имеет имя a file (со встроенным пустым). eval будет разбивать аргументы в пробелах, а команда git reset получит имена a и file как два отдельных аргумента.

Пока у вас нет таких имен файлов, вам не нужно беспокоиться об этом.

Еще одна потенциальная проблема заключается в том, что этот список файлов очень длинный. Вы можете столкнуться с ограничениями ядра на количество аргументов, которые могут быть отправлены в один файл. Вы должны использовать xargs для решения этой проблемы (и, в этом случае, с некоторой работой и использованием -0, для обработки белого пространства в именах файлов).