Как вы исправляете плохое слияние и воспроизводите свои хорошие коммиты на фиксированное слияние? - PullRequest
391 голосов
/ 21 ноября 2008

Я случайно зафиксировал нежелательный файл (filename.orig при разрешении слияния) в своем хранилище несколько коммитов назад, но я до сих пор не заметил этого. Я хочу полностью удалить файл из истории хранилища.

Можно ли переписать историю изменений так, чтобы filename.orig никогда не добавлялся в хранилище?

Ответы [ 12 ]

292 голосов
/ 21 ноября 2008

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

Хотя filter-branch будет делать то, что вы хотите, это довольно сложная команда, и я бы, вероятно, решил сделать это с git rebase. Это, вероятно, личное предпочтение. filter-branch может сделать это одной, несколько более сложной командой, тогда как решение rebase выполняет эквивалентные логические операции по одному шагу за раз.

Попробуйте следующий рецепт:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(Обратите внимание, что вам на самом деле не нужна временная ветвь, вы можете сделать это с помощью 'detached HEAD', но вам нужно записать идентификатор фиксации, сгенерированный шагом git commit --amend, для предоставления git rebase вместо использования временного имени ветви.)

200 голосов
/ 21 апреля 2014

Введение: у вас есть 5 доступных решений

Оригинальный плакат гласит:

Я случайно отправил нежелательный файл ... в мой репозиторий несколько коммитов назад ... Я хочу полностью удалить файл из истории хранилища.

Это можно переписать историю изменений так, чтобы filename.orig никогда не было добавили в хранилище в первую очередь?

Есть много разных способов полностью удалить историю файла из мерзавец:

  1. Внесение изменений.
  2. Хард ресет (возможно плюс ребаз).
  3. Неинтерактивный ребаз.
  4. Интерактивные перебазирования.
  5. Фильтрация веток.

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

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


Решение 1: внесение поправок в комитеты

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

git rm <file>
git commit --amend --no-edit

Решение 2. Жесткий сброс (возможно, плюс перебаз)

Как и решение № 1, если вы просто хотите избавиться от предыдущего коммита, то вы также есть возможность просто сделать полный сброс к своему родителю:

git reset --hard HEAD^

Эта команда жестко переустановит вашу ветку к предыдущему 1 st parent совершить.

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

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

Решение 3: Неинтерактивная Rebase

Это будет работать, если вы просто хотите полностью удалить коммит из истории:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

Решение 4: Интерактивные ребазы

Это решение позволит вам выполнить те же задачи, что и решения № 2 и # 3, то есть изменить или удалить коммиты дальше в истории, чем ваш немедленно предыдущий коммит, так что какое решение вы выберете, зависит от вас. Интерактивные перебазировки не подходят для перебазирования сотен коммитов, для из соображений производительности, поэтому я бы использовал неинтерактивные перебазировки или ветку фильтра решение (см. ниже) в подобных ситуациях.

Чтобы начать интерактивную перебазировку, используйте следующее:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

Это заставит git перемотать историю коммитов назад к родителю совершить, что вы хотите изменить или удалить. Затем он представит вам список перемотка фиксирует в обратном порядке в любом редакторе git установлен (это Vim по умолчанию):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

Фиксация, которую вы хотите изменить или удалить, будет в верхней части этого списка. Чтобы удалить его, просто удалите его строку в списке. В противном случае замените «выбор» на «edit» в строке 1 st , например:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

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

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

На этом этапе вы можете удалить файл и изменить коммит, а затем продолжить перебазироваться:

git rm <file>
git commit --amend --no-edit
git rebase --continue

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

git diff master@{1}

Решение 5: Фильтрация ветвей

Наконец, это решение лучше, если вы хотите полностью стереть все следы существование файла из истории, и ни одно из других решений не до задание.

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

Это удалит <file> из всех коммитов, начиная с корневого коммита. Если вместо этого вы просто хотите переписать диапазон фиксации HEAD~5..HEAD, тогда вы можете передать это в качестве дополнительного аргумента filter-branch, как указано в этот ответ :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

Опять же, после завершения filter-branch обычно неплохо проверить что нет других неожиданных изменений, если ваша ветка предыдущее состояние перед операцией фильтрации:

git diff master@{1}

Альтернатива фильтра-ответвления: BFG Repo Cleaner

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

git-filter-branch позволяет вам делать сложные переписанные сценарии оболочки вашей истории Git, но вам, вероятно, не нужна эта гибкость, если вы просто удаляете ненужные данные , например, большие файлы или пароли. Для этих операций вы можете рассмотреть BFG Repo-Cleaner , основанный на JVM альтернатива git-filter-branch, как минимум, в 10-50 раз быстрее для эти варианты использования, и с совершенно разными характеристиками:

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

  • По умолчанию BFG использует все преимущества многоядерных машин, параллельно очищая деревья файлов коммитов. git-filter-branch очищает фиксирует последовательно (то есть однопоточным образом), хотя равно можно написать фильтры, которые включают в себя свой собственный параллелизм, в скрипты, выполняемые против каждого коммита.

  • Опции команды очень более ограничивающий, чем ветка git-filter, и посвященный только задачи удаления нежелательных данных, например: --strip-blobs-bigger-than 1M.

Дополнительные ресурсы

  1. Pro Git & sect; 6.4 Инструменты Git - История переписывания .
  2. git-filter-branch (1) Страница руководства .
  3. git-commit (1) Страница руководства .
  4. git-reset (1) Страница руководства .
  5. git-rebase (1) Страница руководства .
  6. Очиститель репо BFG (см. Также этот ответ от самого создателя ).
118 голосов
/ 14 марта 2009

Если вы ничего не совершали с тех пор, просто git rm файл и git commit --amend.

Если у вас есть

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

будет проходить каждое изменение с merge-point до HEAD, удалять filename.orig и перезаписывать изменения. Использование --ignore-unmatch означает, что команда не завершится ошибкой, если по какой-то причине имя файла.orig отсутствует в изменении. Это рекомендуемый способ из раздела Примеры на странице руководства git-filter-branch .

Примечание для пользователей Windows: путь к файлу должен использовать косую черту

47 голосов
/ 04 февраля 2010

Это лучший способ:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Только сначала сделайте резервную копию файлов.

EDIT

Редактирование Неон , к сожалению, было отклонено во время обзора.
Смотрите пост Neons ниже, он может содержать полезную информацию!


например. удалить все файлы *.gz, случайно переданные в репозиторий git:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

Это все еще не работает для меня? (Я в настоящее время на git версии 1.7.6.1)

$ du -sh .git ==> e.g. 100M

Не знаю почему, так как у меня была только ОДНА мастер ветка. В любом случае, я наконец-то действительно очистил свое git-репо, запустив новый пустой и пустой git-репозиторий, например,

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(да!)

Затем я клонировал его в новый каталог и переместил его в папку .git. например, * * одна тысяча тридцать две

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(да! Наконец-то вычистили!)

Убедившись, что все в порядке, вы можете удалить каталоги ../large_dot_git и ../tmpdir (возможно, через пару недель или месяц, на всякий случай ...)

26 голосов
/ 31 марта 2013

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

Чтобы сделать его максимально простым, я бы рекомендовал использовать BFG Repo-Cleaner , более простую и быструю альтернативу git-filter-branch, специально разработанную для удаления файлов из истории Git. Одним из способов облегчения вашей жизни здесь является то, что он фактически обрабатывает все ссылок по умолчанию (все теги, ветви и т. Д.), Но он также 10 - 50x быстрее.

Вы должны внимательно выполнить следующие шаги: http://rtyley.github.com/bfg-repo-cleaner/#usage - но основной бит такой: скачайте BFG jar (требуется Java 6 или выше) и выполните эту команду:

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

Вся ваша история репозитория будет отсканирована, и любой файл с именем filename.orig (которого нет в вашем последнем коммите ) будет удален. Это значительно проще, чем использовать git-filter-branch, чтобы сделать то же самое!

Полное раскрытие: я являюсь автором репо-уборщика BFG.

13 голосов
/ 10 июня 2016
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all
4 голосов
/ 17 мая 2016

Самый простой способ, который я нашел, был предложен leontalbot (в качестве комментария), который представляет собой сообщение , опубликованное Anoopjohn . Я думаю, что это стоит своего места в качестве ответа:

(я преобразовал его в скрипт bash)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

Все кредиты идут на Annopjohn и leontalbot за указание на это.

Примечание

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

4 голосов
/ 16 октября 2013

Просто чтобы добавить это к решению Чарльза Бэйли, я просто использовал git rebase -i, чтобы удалить ненужные файлы из предыдущего коммита, и это сработало как чудо. Шаги:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue
3 голосов
/ 30 января 2017

Определенно, git filter-branch - это путь.

К сожалению, этого будет недостаточно для полного удаления filename.orig из вашего репо, так как на него все еще могут ссылаться теги, записи reflog, удаленные устройства и т.

Я также рекомендую удалить все эти ссылки, а затем вызвать сборщик мусора. Вы можете использовать скрипт git forget-blob с этого веб-сайта, чтобы сделать все это за один шаг.

git forget-blob filename.orig

1 голос
/ 29 марта 2018

Если это последний коммит, который вы хотите очистить, я попытался использовать git версии 2.14.3 (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git
...