Як виправити погане злиття та повторно відтворити свої товари на фіксований злиття?


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команду, а не використання тимчасової гілки назва.)


6
Хіба не git rebase -iшвидше і все одно так просто? $ git rebase -i <sh1-of-merge> Позначте правильний як "редагування" $ git rm somefile.orig $ git commit --amend $ git rebase --continue Однак, чомусь я все ще маю цей файл десь останній раз я це зробив. Напевно, чогось не вистачає.
Wernight

12
git rebase -iдуже корисно, особливо коли для виконання декількох операцій з перезавантаженням y, але це правильний біль, щоб точно описати, коли ви насправді не вказуєте на чиєсь плече і можете побачити, що вони роблять зі своїм редактором. Я використовую vim, але не всі будуть задоволені: "ggjcesquash <Esc> jddjp: wq" та вказівками на кшталт "Перемістити верхній рядок до поточного другого рядка та змінити перше слово на четвертому рядку, щоб" редагувати "тепер зберегти та кинути "швидко здається складнішим, ніж реальні кроки. Як правило , ви в кінцевому підсумку з деякими --amendі --continueдій, а також.
CB Bailey

3
Я зробив це, але нова зміна була повторно застосована поверх зміненої, з тим же повідомленням. Очевидно, git зробив тристоронній злиття між старим, незміненим коміксом, що містить непотрібний файл, і фіксованим фіксацією з іншої гілки, і тому він створив новий комітет поверх старого, щоб повторно застосувати файл.

6
@UncleCJ: Ваш файл додано до комісії злиття? Це важливо. Цей рецепт розроблений для того, щоб впоратися з поганим злиттям. Це не спрацює, якщо ваш небажаний файл був доданий у звичайний фіксатор в історії.
CB Bailey

1
Я вражений, як я міг зробити все це за допомогою smartgit і зовсім не терміналу! Дякую за рецепт!
Крегокс

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: Неінтерактивна база даних

Це спрацює, якщо ви просто хочете повністю видалити комісію з історії:

# 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 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-гілки на основі 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 Repo Cleaner (див. Також цю відповідь від самого творця ).

Чи 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засобів означає, що команда не вийде з ладу, якщо з якоїсь причини filename.orig відсутній у зміні. Це рекомендований спосіб із розділу Приклади на сторінці вказівника git-filter-branch .

Примітка для користувачів Windows: Шлях до файлу повинен використовувати косої риски вперед


3
Дякую! git filter-branch працював для мене, де приклад ребазування, наведений як відповідь, не зробив: кроки, здавалося, спрацювали, але потім натискання не вдалося. Зробив потяг, потім вдало штовхнув, але файл все ще був навколо. Спробував переробити кроки відновлення, і тоді все зіпсувалося конфліктами злиття. Я використовував дещо іншу команду фільтр-гілка, "вдосконалений метод", наведений тут: github.com/guides/completely-remove-a-file-from-all-reitions git filter-branch -f --index- filter 'git update-index --remove filename' <introduction-revision-sha1>
..HEAD

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-reitions

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

EDIT

Редагування 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

Не знаю чому, оскільки я мав лише ОДИН головний відділ. У будь-якому випадку, я нарешті очистив своє git repo, по-справжньому очистивши, натиснувши у нове порожнє і оголене сховище 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
Це працювало для мене до того, як "Це все ще не працювало для мене?" коментар
shadi

Чудова відповідь, але пропонуємо додати --prune-emptyдо команди фільтр-гілка.
ideaman42

27

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

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

Ви повинні уважно виконувати тут кроки: http://rtyley.github.com/bfg-repo-cleaner/#usage - але основний біт саме такий: завантажте банку 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 правильно. :-)
Кевін ЛаБранш

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

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

Я рекомендую також видалити всі ці посилання, а потім зателефонувати до сміттєзбірника. Ви можете використовувати git forget-blobсценарій з цього веб-сайту, щоб зробити все це за один крок.

git forget-blob filename.orig


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

git reflog expire --expire=now --all; git gc --prune=nowце дуже погано робити. Якщо у вас не вистачає місця на диску, нехай git сміття збирає ці
комісії

Дякуємо, що вказали на це. Моє репо було подано з великою кількістю великих двійкових файлів, і репостування резервного копіювання робиться повністю щовечора Тож я просто хотів, щоб з неї
вискакували


-1

Ви також можете використовувати:

git reset HEAD file/path


3
Якщо файл було додано до комісії, він навіть не видаляє файл з індексу, він просто скидає індекс у версію HEAD.
CB Bailey
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.