Как вы исправляете плохое слияние и воспроизводите свои хорошие коммиты на фиксированное слияние?

407

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

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

Грант Лимберг
источник

Ответы:

297

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

Хотя 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

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

CB Bailey
источник
6
Разве не git rebase -iбудет быстрее и все так же легко? $ git rebase -i <sh1-of-merge> Пометить правильный как «edit» $ git rm somefile.orig $ git commit --amend $ git rebase --continue Однако по какой-то причине у меня все еще есть этот файл где-то последний раз, когда я сделал это. Вероятно, чего-то не хватает.
Wernight
12
git rebase -iЭто очень полезно, особенно когда вам нужно выполнить несколько операций rebase-y, но правильно описать правильно, когда вы на самом деле не указываете на чье-то плечо и видите, что они делают с их редактором. Я использую vim, но не все были бы довольны: «ggjcesquash <Esc> jddjp: wq» и такими инструкциями, как «Переместить верхнюю строку после текущей второй строки и изменить первое слово в строке четыре на« редактировать », теперь сохраните и бросить курить быстро кажется сложнее, чем реальные шаги. Вы обычно заканчиваете с некоторыми --amendи --continueдействиями, а также.
CB Bailey
3
Я сделал это, но новый коммит был применен поверх исправленного с тем же сообщением. По-видимому, git осуществил трехстороннее слияние между старым, без изменений коммитом, содержащим нежелательный файл, и фиксированным коммитом из другой ветки, и поэтому он создал новый коммит поверх старого, чтобы повторно применить файл.
6
@UncleCJ: Ваш файл был добавлен в коммит слияния? Это важно. Этот рецепт разработан, чтобы справиться с плохим коммитом слияния. Это не сработает, если ваш нежелательный файл был добавлен в обычный коммит в истории.
CB Bailey
1
Я поражен тем, как я мог сделать все это, используя smartgit и вообще без терминала! Спасибо за рецепт!
Cregox
209

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

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

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

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

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

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

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

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


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

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

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

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

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

git reset --hard HEAD^

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

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

# 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)

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

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 , альтернативы git-filter-branch на основе JVM, как правило, в 10-50 раз быстрее для этих сценариев использования и с совершенно другими характеристиками:

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

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

  • Эти опции команды гораздо более ограничительные , чем ГИТ-фильтр ветвь, и посвящен только к задачам удаления нежелательного данных- например --strip-blobs-bigger-than 1M.

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

  1. Pro Git § 6.4 Инструменты Git - История переписывания .
  2. git-filter-branch (1) Руководство пользователя .
  3. git-commit (1) Страница руководства .
  4. git-reset (1) .
  5. git-rebase (1) .
  6. Очиститель репо BFG (см. Также этот ответ от самого создателя ).
Сообщество
источник
Вызывает ли filter-branchпересчет хешей? Если команда работает с репо, где большой файл должен быть отфильтрован, как они это делают, чтобы у всех было одинаковое состояние репо?
YakovL
@YakovL. Все пересчитывает хэши. На самом деле коммиты неизменны. Он создает совершенно новую историю и перемещает указатель вашей ветви на нее. Единственный способ убедиться, что у всех одна и та же история - это полная перезагрузка.
Безумный физик
118

Если вы ничего не сделали с тех пор, просто 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: путь к файлу должен использовать косую черту

Schwern
источник
3
Спасибо! git filter-branch работал для меня, где пример rebase, приведенный в качестве ответа, не помог Вытащил, потом успешно нажал, но файл все еще был рядом. Попытался повторить шаги перебазировки, а затем все пошло не так с конфликтами слияния. Я использовал немного другую команду filter-branch, однако, «Улучшенный метод», приведенный здесь: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- фильтр 'git update-index - удалить имя файла' <введение-ревизия-sha1> .. HEAD
atomicules
1
Я не уверен, какой из них является улучшенным методом. Git официальной документации, git-filter-branchкажется, дать первый.
Wernight
5
Проверьте zyxware.com/articles/4027/… Я считаю, что это наиболее полное и прямолинейное решение, которое включает в себяfilter-branch
leontalbot
2
@atomicules, если вы попытаетесь перенести локальное хранилище на удаленное, git сначала будет настаивать на извлечении из удаленного хранилища, поскольку в нем есть изменения, которых у вас нет локально. Вы можете использовать флаг --force для отправки на удаленный компьютер - он полностью удалит файлы. Но будьте осторожны, убедитесь, что вы не будете принудительно перезаписывать что-либо, кроме файлов.
sol0mka
1
Не забывайте использовать, "а не 'при использовании Windows, иначе вы получите бесполезную фразу «неверная редакция».
cz
49

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

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

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

Редакция Neon, к сожалению, была отклонена во время обзора.
Смотрите пост 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

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

Даррен
источник
1
Это сработало для меня до того, как "Это все еще не сработало для меня?" комментарий
Шади
Отличный ответ, но предложите добавить --prune-emptyв команду filter-branch.
ideasman42
27

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

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

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

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

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

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

Роберто Тайли
источник
4
Это отличный инструмент: одна команда, она производит очень четкий вывод и предоставляет файл журнала, который соответствует каждому старому коммиту новому . Я не люблю устанавливать Java, но это того стоит.
mikemaccana
Это единственное, что сработало для меня, но это потому, что я неправильно работал с git filter-branch. :-)
Кевин Лабранш
14
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
paulalexandru
источник
1
В то время как все ответы, похоже, находятся на пути к ветвям фильтров, в этом разделе показано, как очистить ВСЕ ветви в вашей истории.
Кэмерон Лоуэлл Палмер
4

Просто чтобы добавить это к решению Чарльза Бэйли, я просто использовал 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
Сверрир Сигмундарсон
источник
4

Простейший способ, который я нашел, был предложен 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указывают на это.

НОТА

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

Лепе
источник
3

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

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

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

git forget-blob filename.orig

nachoparker
источник
1

Если это последний коммит, который вы хотите очистить, я попытался использовать 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
clarkttfu
источник
git reflog expire --expire=now --all; git gc --prune=nowэто очень плохая вещь. Если у вас нет свободного места на диске, позвольте git garbage собирать эти коммиты через несколько недель
avmohan
Спасибо что подметил это. Мой репозиторий был представлен с большим количеством бинарных файлов, и репозиторий полностью копируется каждую ночь. Так что я просто хотел, чтобы все вышло из этого;)
clarkttfu
-1

Вы также можете использовать:

git reset HEAD file/path

Paolo Granada Lim
источник
3
Если файл был добавлен в коммит, то он даже не удаляет файл из индекса, он просто сбрасывает индекс в версию файла HEAD.
CB Bailey