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

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

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

12 ответов

Решение

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

Хотя 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 команда вместо использования временного имени ветви.)

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

  • Опции команды намного более строгие, чем ветка git-filter, и предназначены только для задач удаления нежелательных данных, например: --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 Repo Cleaner (см. Также этот ответ от самого создателя).

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

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

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

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

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

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

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

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

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

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

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

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

git forget-blob filename.orig

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

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

НОТА

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

Если это последний коммит, который вы хотите очистить, я попытался использовать 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

Это то, что git filter-branch был разработан для.

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

git reset HEAD file/path

Другие вопросы по тегам