Как заменить текст из файлов в истории git?

Я всегда использовал основанный на интерфейсе git-клиент (smartGit) и поэтому не имел большого опыта работы с git-консолью.

Однако теперь я сталкиваюсь с необходимостью замены строки во всех файлах.txt из истории (то есть, не стирая весь файл, а просто подставляя строку). Я нашел следующую команду:

git filter-branch --tree-filter 'git ls-files -z "*.php" |xargs -0 perl -p -i -e "s#(PASSWORD1|PASSWORD2|PASSWORD3)#xXxXxXxXxXx#g"' -- --all

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

Есть ли лучший способ сделать это, чтобы не повредить мои двоичные файлы?

Благодарю.

РЕДАКТИРОВАТЬ:

Я с чем-то перепутал. Фактический код, вызвавший повреждение двоичных файлов:

$ git filter-branch --tree-filter "find . -type f -exec sed -i -e 's/originalpassword/newpassword/g' {} \;"

Код вверху фактически удалил все файлы с моим паролем, как ни странно.

7 ответов

Решение

Вы можете избежать касания нежелательных файлов, передав -name "pattern" в find,

Это работает для меня:

git filter-branch --tree-filter "find . -name '*.php' -exec sed -i -e \
    's/originalpassword/newpassword/g' {} \;"

Я бы порекомендовал использовать BFG Repo-Cleaner, более простую и быструю альтернативу git-filter-branch специально предназначенный для перезаписи файлов из истории Git.

Вы должны тщательно выполнить следующие шаги здесь: https://rtyley.github.io/bfg-repo-cleaner/ - но основной бит просто так: скачайте JAR BFG (требуется Java 7 или выше) и выполните эту команду:

$ java -jar bfg.jar  --replace-text replacements.txt -fi *.php  my-repo.git

replacements.txt Файл должен содержать все замены, которые вы хотите сделать, в таком формате (одна запись на строку - обратите внимание, что комментарии не должны быть включены):

PASSWORD1 # Replace literal string 'PASSWORD1' with '***REMOVED***' (default)
PASSWORD2==>examplePass         # replace with 'examplePass' instead
PASSWORD3==>                    # replace with the empty string
regex:password=\w+==>password=  # Replace, using a regex
regex:\r(\n)==>$1               # Replace Windows newlines with Unix newlines

Вся ваша история репозитория будет отсканирована, и .php Для файлов (размером менее 1 МБ) будут выполнены замены: любая подходящая строка (которая отсутствует в вашем последнем коммите) будет заменена.

Полное раскрытие: я являюсь автором BFG Repo-Cleaner.

С Git 2.24 (4 квартал 2019 г.) git filter-branch(и BFG) устарела.

Эквивалент был бы, используя newren/git-filter-repo, и его примерный раздел:

cd repo
git filter-repo --path-glob '*.txt' --replace-text expressions.txt

с expressions.txt:

literal:originalpassword==>newpassword

Больше информации на git-filter-repo

/questions/31012680/kak-zamenit-tekst-iz-fajlov-v-istorii-git/55262075#55262075 дает основы, вот еще немного информации.

Установить

Начиная с git 2.5, по крайней мере, он не поставляется с mainline git, поэтому: https://superuser.com/questions/1563034/how-do-you-install-git-filter-repo/1589985#1589985

python3 -m pip install --user git-filter-repo

Советы по использованию

Вот наиболее распространенный подход, который я использую:

git filter-repo --replace-text <(echo 'my_password==>xxxxxxxx') HEAD

где:

  • Подстановка процесса в Bash позволяет нам не создавать файл для простых замен
  • HEAD влияет только на текущую ветку

Изменить только диапазон коммитов

Как изменить только диапазон коммитов с помощью git filter-repo вместо всей истории ветки?

git filter-repo --replace-text <(echo 'my_password==>xxxxxxxx') HEAD

Заменить с помощью Python API

Для более сложных замен вы можете использовать Python API, см.: Как использовать git filter-repo в качестве библиотеки с интерфейсом модуля Python?

Я создал файл в /usr/local/git/findsed.sh со следующим содержимым:

find . -name 'githubDirToSubmodule.sh' -exec sed -i '' -e 's/What I want to remove//g' {} \;

Я запустил команду:

git filter-branch --tree-filter "sh /usr/local/git/findsed.sh"

Объяснение команд

Когда вы запускаете git filter-branch, он проходит каждую ревизию, которую вы когда-либо фиксировали, одну за другой. --tree-filter запускает скрипт findsed.sh для каждой зафиксированной ревизии, сохраняет его и затем переходит к следующей ревизии.

Команда find находит определенный файл или набор файлов и выполняет (-exec) редактор sed для этого файла. sed - это команда, которая принимает регулярное выражение после s/ и заменяет его строкой между / и /g (пустым в моем примере). {} является ссылкой на путь к файлам, который был задан командой find. Путь к файлу подается в sed, чтобы sed знал, над чем работать. \; просто завершает команду -exec.

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

Особенности

Я успешно реализовал это на Mac, и, очевидно, sed - это конкретная (более старая?) Версия для Mac. Это имеет значение, так как иногда ведет себя по-разному. Убедитесь, что вы выполнили sed -i '', иначе он добавил "-e" в конец файлов, думая, что это то, что я хотел назвать своими файлами резервных копий. -i '' говорит, что не делайте резервные копии файлов, просто редактируйте файлы на месте, и резервный файл не требуется.

Указание -name 'filename.sh' помогло мне избежать еще одной проблемы, которую я не мог решить. Был другой файл с.sh, и этот файл заканчивался без символа новой строки. По какой-то причине sed добавит в конец символ новой строки, несмотря на то, что s/blah/blah/g не соответствует никому в этом файле. Поэтому вместо того, чтобы выяснить эту проблему, я просто сказал находке игнорировать все остальные файлы.

Дополнительные команды, которые работают

Кроме того, я обнаружил, что эти команды работают в файле findsed.sh (только одна команда за раз, а не несколько, поэтому закомментируйте # остальные):

find . -name '.publishNewZenPackFromGithub.sh.swp' -exec rm -f {} \;
find . -name '*' -exec grep -H PassToRemove {} \;

Наслаждайтесь!

Может быть проблема расширения оболочки. Если ветвь фильтра теряет кавычки "*.php" к тому времени, когда он оценивает команду, он может расширяться до нуля, таким образом git ls-files -z список всех файлов.

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

Поскольку это появляется в Google дляgit replace text in history, а поскольку использование инструментов, отличных от git, иногда приносит больше проблем, чем пользы, вот команда, которая заменит многострочный текст на всем пути от${COMMIT}вперед кHEAD.

Предупреждение: это НЕ для новичков. Оно использует git filter-branch, так что все его предостережения/подводные камни/и т.д. применять. Убедитесь, что вы зафиксировали/создали резервную копию всего, что вам нужно сохранить, чтобы не потерять данные.

С учетом сказанного создайте псевдоним в Bash следующим образом:

      git config --global alias.filter-branch-replace-text '!main() { set -eu && if [ -n "${BASH_VERSION+x}" ]; then set -o pipefail; fi && local pattern patternq replacement replacementq commit && pattern="$1" && shift && replacement="$1" && shift && commit="$1" && shift && local sed_binary_flags="" && if [ msys = "${OSTYPE-}" ]; then sed_binary_flags="-b"; fi && patternq="$(printf "%s" "${pattern}" | sed ${sed_binary_flags} "s/'\''/'\''\\\\'\'''\''/g")." && patternq="'\''${patternq%.}'\''" && replacementq="$(printf "%s" "${replacement}" | sed ${sed_binary_flags} "s/'\''/'\''\\\\'\'''\''/g")." && replacementq="'\''${replacementq%.}'\''" && git filter-branch --tree-filter "for path in $(printf "%s\n" "$@" | sed ${sed_binary_flags} -e "s/'\''/'\''\\\\'\'''\''/g" -e "s/\(.*\)/'\''\1'\''/" | tr "\n" " ")"'\''; do if [ -f "${path}" ]; then perl -0777 -i -s -p -e "s/\\Q\$q\\E/\$s/sgm" -- -q='\''"${patternq}"'\'' -s='\''"${replacementq}"'\'' -- "${path}"; fi || break; done'\'' "${commit}~1..HEAD" --; } && main'

и затем вы можете вызвать его из Bash следующим образом:

      git filter-branch-replace-text \
    $')\r\n{' \
    $') /* EOL */\r\n{' \
    "${COMMIT}" \
    src/*.txt

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

Если вам нужны регулярные выражения, вам нужно удалить\Qи\Eв команде Perl (которая выполняет экранирование) и правильно экранирует строки, необходимые дляs/$q/$s/sgmкомандовать собой.

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

      (f="$(git --no-pager config --get alias.filter-branch-replace-text)" && eval "${f%&&*}" && declare -f "${f%%()*}")