Как удалить коммиты из истории git, но в остальном сохранить график точно таким же, включая слияния?

Что у меня есть:

---A----B-----C-----D--------*-----E-------> (master)
                     \      /
                      1----2 (foo)

Что мне нужно:

---A---------------D--------*-----E-------> (master)
                    \      /
                     1----2 (foo)

Некоторое время назад я сделал два коммита, которые хотел бы удалить из репозитория git. Я попробовал множество различных «руководств» по ​​перебазированию, и все они закончились странными историями git, поэтому я создал пример репозитория, и результат оказался не таким, как я ожидал. Может ли кто-нибудь помочь мне понять, что мне не хватает?

У меня есть две ветки, master и foo. Я сделал коммит B с одним файлом, который хотел бы удалить, и коммит C, в котором я изменил этот файл. Среди других коммитов я больше никогда не прикасался к этому файлу.

Идентификаторы фиксации:

A: f0e0796
B: 5ccb371
C: a46df1c
D: 8eb025b
E: b763a46
1: f5b0116
2: 175e01f

Итак, я использую rebase -i f0e0796 и удаляю B 5ccb371 и C a46df1c, правильно? Если я правильно интерпретирую результат, это то, что gitk показывает мне для моего репо, хотя git branches по-прежнему перечисляет вторую ветвь.

---A-----1----2---E------> (master)

Кто-нибудь может сказать мне, что здесь произошло?

Редактировать. Вот как воссоздать репозиторий из первого графика:

git init foo
cd foo

touch A
git add A
git commit -m "add A"

touch B
git add B
git commit -m "add B"

echo "modify" > B
git add B
git commit -m "modify B"

touch C
git add C
git commit -m "add C"

git checkout -b foo

touch 1
git add 1
git commit -m "add 1"

touch 2
git add 2
git commit -m "add 2"

git switch master
git merge foo --no-ff

touch E
git add E
git commit -m "add E"

person Daniel Stephens    schedule 11.04.2020    source источник
comment
Извините, если я правильно понял. Вы пытаетесь отменить два коммита от своего мастера и сохранить такое же состояние в D, как все происходит в D из B и C? Или вы хотите навсегда отменить изменения, чтобы в D не было изменений, сделанных в B и C?   -  person VPaul    schedule 11.04.2020
comment
Позднее предположение верно. D не должен содержать изменений из B и C. Я знаю, что здесь может помочь bfg, но я не хочу полностью стирать этот файл из истории, так как он действительно существовал несколько лет назад, но был удален. Он просто был случайно повторно представлен в этих двух коммитах.   -  person Daniel Stephens    schedule 11.04.2020
comment
Я предполагаю, что опубликованный ответ должен помочь решить проблему, поскольку нет необходимости удалять историю из git для коммитов. Rebase не используется в этих обстоятельствах. Я использовал его, чтобы довести свою ветку до того же уровня мастера. Иногда слияние решает мою проблему, и мне никогда не приходится использовать перебазирование.   -  person VPaul    schedule 11.04.2020
comment
Спасибо за продолжение!   -  person Daniel Stephens    schedule 11.04.2020
comment
@VPaul Rebase наверняка используется в таких обстоятельствах! Перебазирование, независимо от того, делаете ли вы это с помощью git rebase или вручную (мой ответ показывает и то, и другое), является способом переписать, а не изменить историю .   -  person Inigo    schedule 24.04.2020


Ответы (5)


В то время как то, что я предлагаю, даст вам чистую, линейную историю; это то, что rebase должен делать по существу. Однако я надеюсь, что это даст вам возможность удалить B и B' из истории коммитов. Вот объяснение:

Repo recreation output:
---A----B-----B'-----C--------D-------> (master)
                      \      /
                       1----2 (foo)

git log --graph --all --oneline --decorate #initial view the git commit graph
* dfa0f63 (HEAD -> master) add E
*   843612e Merge branch 'foo'
|\  
| * 3fd261f (foo) add 2
| * ed338bb add 1
|/  
* bf79650 add C
* ff94039 modify B
* 583110a add B
* cd8f6cd add A

git rebase -i HEAD~5 #here you drop 583110a/add B and ff94039/modify B from
foo branch.

git log --graph --all --oneline --decorate
$ git rebase -i HEAD~5
* 701d9e7 (HEAD -> master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
| * 3fd261f (foo) add 2
| * ed338bb add 1
| * bf79650 add C
| * ff94039 modify B
| * 583110a add B
|/  
* cd8f6cd add A

$ git rebase -i master foo #drop 583110a/add B and ff94039/modify B again

$ git log --graph --all --oneline --decorate #view the git commit graph

* 701d9e7 (HEAD -> foo, master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
* cd8f6cd add A

Наконец, финальный выход может быть не в том порядке, в котором вы ожидали, A--C--1---2---E. Однако вы можете снова изменить порядок в интерактивном режиме. Попробуйте git rebase -i HEAD~n.

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

person Smeet Thakkar    schedule 12.04.2020
comment
@DanielStephens Этот ответ делает все, что вы просите, за исключением сохранения ветки foo как отдельной ветки. Хотя в этом случае это не имеет значения, поскольку master и foo идентичны как до, так и после того, как вы выполнили перебазирование, это не сработало бы, если бы они не были, например. у вас были коммиты между D и E в основной ветке. См. stackoverflow.com/a/61411955/8910547, чтобы узнать, как использовать git rebase для выполнения exactl й что вы хотите. - person Inigo; 24.04.2020

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

Если у вас есть git версии 2.18 или выше, вы можете использовать параметр --rebase-merges*, чтобы сообщить git воссоздать историю слияния, а не линеаризовать ее, как это делается по умолчанию. Перебазированная история будет иметь те же ответвления и слияния. Ниже я покажу вам шаги для достижения того, чего вы хотите, используя --rebase-merges.

Эти шаги предполагают точное репо, которое вы указали в своем вопросе.

  1. git checkout master
  2. git rebase -i --rebase-merges f0e0796
  3. in the interactive rebase todo file:
    • remove the two commits you wanted to drop (or comment them out, or change pick to drop or d)
    • на новой строке сразу после строки label foo добавьте следующее:
    exec git branch -f foo head
    
    (see below for explanation)
  4. сохраните и закройте файл todo и вуаля, git перебазирует коммиты с графиком, который будет выглядеть именно так, как вы хотели.


объяснение файла todo

git rebase просто автоматизирует ряд шагов, которые вы с тем же успехом можете выполнять вручную. Эта последовательность шагов представлена ​​в файле todo. git rebase --interactive позволяет изменить последовательность перед ее выполнением.

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

label onto                  // labels "rebase onto" commit (f0e0796)
                            // this is what you would do in your head
                            // if doing this manually
# Branch foo
reset onto                  // git reset --hard <onto>
drop 5ccb371 add B          // skip this commit
drop a46df1c modify B       // skip this commit
pick 8eb025b add C          // git cherry-pick 8eb025b
label branch-point          // label this commit so we can reset back to it later
pick f5b0116 add 1          // git cherry-pick f5b0116
pick 175e01f add 2          // git cherry-pick 175e01f
label foo                   // label this commit so we can merge it later
                            //   This is just a rebase internal label. 
                            //   It does not affect the `foo` branch ref.
exec git branch -f foo head // point the `foo` branch ref to this commit 

reset branch-point # add C  // git reset --hard <branch-point>
merge -C b763a46 foo # Merge branch 'foo'  // git merge --no-ff foo
                                           // use comment from b763a46

exec git branch -f foo head объяснил

Как я упоминал выше, git rebase работает только с одной веткой. Что делает эта команда exec, так это изменяет ссылку foo, чтобы она указывала на текущий head. Как вы можете видеть в последовательности в файле todo, вы говорите ему сделать это сразу после того, как он зафиксировал последний коммит ветки foo ("добавить 2"), которая в файле todo удобно помечена как label foo.

Если вам больше не нужна ссылка foo (например, это функциональная ветвь и это ее окончательное слияние), вы можете пропустить добавление этой строки в файл todo.

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

git branch -f foo <hash of the rebased commit that should be the new head of `foo`>

Дайте знать, если у вас появятся вопросы.


* Если у вас более старая версия git, вы можете использовать устаревшую опцию --preserve-merges, хотя она не совместима с интерактивным режимом rebase.

person Inigo    schedule 24.04.2020
comment
Хорошая точка зрения. Проголосовал. Я представил --rebase-merges в stackoverflow.com/a/50555740/6309. - person VonC; 24.04.2020
comment
@VonC спасибо и круто. --rebase-merges намного лучше, чем --preserve-merges, учитывая то, что вы можете делать с ним в интерактивном режиме. Ознакомьтесь с моим объяснением скрипта todo, который я только что добавил. Дайте мне знать, если у меня что-то не так, но я просто выполнил шаги вручную, как показано, и все работает так же. - person Inigo; 24.04.2020

Итак, я использую rebase -i f0e0796 и удаляю B 5ccb371 и C a46df1c, верно? Если я правильно интерпретирую результат, это то, что gitk показывает мне для моего репо, хотя git branches по-прежнему перечисляет вторую ветвь.

...A---1---2---E    master

Кто-нибудь может сказать мне, что здесь произошло?

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

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


Является ли git rebase функцией, которую я ищу для своей проблемы?

Не совсем. Параметр --rebase-merges дорабатывается, и ответ Иниго хорошо подходит для вашего конкретного случая, но см. предупреждения в документации: у него есть реальные ограничения и предостережения. Как указывает ответ Иниго, эти шаги предполагают точное репо, которое вы показываете в своем вопросе, а git rebase просто автоматизирует ряд шагов, которые вы также можете выполнить вручную. Причина этого ответа в том, что для одноразовой работы обычно лучше просто сделать это.

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

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

Вам нужно не создать единую, чистую линейную историю, вам нужно что-то другое.

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

git checkout :/A; git cherry-pick :/D :/1 :/2; git branch -f foo
git checkout foo^{/D}; git merge foo; git cherry-pick :/E; git branch -f master

и вы сделали.

Да, вы могли заставить git rebase -ir настроить это для вас, но когда я посмотрел на список выбора, который производит, редактирование в правильных инструкциях не казалось проще или легче, чем описанная выше последовательность. Выяснить, какой именно результат вы хотите, и выяснить, как заставить git rebase -ir сделать это за вас, и просто сделать это.

git rebase -r --onto :/A :/C master
git branch -f foo :/2

- это ответ на все, что я, вероятно, использовал бы, поскольку Иниго говорит точное репо, которое вы показываете в своем вопросе. См. документацию по синтаксису поиска сообщений.

person jthill    schedule 24.04.2020

Чтобы изменить историю коммитов, есть несколько способов.

Проблема с rebase, когда вы хотите изменить всю историю репо, заключается в том, что он перемещает только одну ветку за раз. Кроме того, у него есть проблемы со слиянием, поэтому вы не можете просто перебазировать D и E на A, сохраняя при этом более свежую историю в том виде, в каком она существует сейчас (потому что E — это слияние).

Вы можете обойти все это, но этот метод сложен и подвержен ошибкам. Существуют инструменты, предназначенные для полной перезаписи репо. Возможно, вы захотите взглянуть на filter-repo (инструмент, который заменяет filter-branch) - но похоже, что вы просто пытаетесь удалить конкретный файл из своей истории, что (1) может быть хорошей работой для BFG Repo Cleaner, или ( 2) на самом деле достаточно простая задача с filter-branch

(Если вы хотите изучить BFG, https://rtyley.github.io/bfg-repo-cleaner/ ; если вы хотите изучить filter-repo, https://github.com/newren/git-filter-repo)

Чтобы использовать filter-branch для этой цели

git filter-branch --index-filter 'git rm --cached --ignore-unmatch path/to/file' --prune-empty -- --all

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

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

Несколько заметок:

1 - Если есть пароли, и они когда-либо были переданы на общий пульт, эти пароли скомпрометированы. Вы ничего не можете с этим поделать; смените пароли.

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

3 - В локальном репозитории, где вы сделали ремонт, вам нужно избавиться от журналов ссылок (а также резервных ссылок, которые могли быть созданы, если вы использовали такой инструмент, как filter-branch), а затем запустить gc. Или может быть проще повторно клонировать новый репозиторий, который извлекает только новые версии веток.

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

person Mark Adelsberger    schedule 11.04.2020
comment
Спасибо за разъяснения!! - person Daniel Stephens; 11.04.2020
comment
Кроме того, у него есть проблемы со слиянием -- НЕ ПРАВДА! См. мой ответ. Это на самом деле довольно просто. - person Inigo; 24.04.2020
comment
@Иниго - Неправильно. Я остаюсь при своем заявлении. Как и гораздо более старая опция --preserve-merges, опция --rebase-merges пытается обрабатывать слияния, но у нее это плохо получается, за исключением очень простых случаев (например, стратегия слияния по умолчанию без конфликтов). - person Mark Adelsberger; 25.04.2020
comment
Я использую его ВСЕ время, и конфликты случаются именно так, как они ожидаются. И это редко проблема, потому что у меня включен git rerere. `stackoverflow.com/a/7241744/8910547 - person Inigo; 25.04.2020
comment
@Inigo - мне потребовалась ровно одна попытка создать тестовое репо, в котором оно не работало. В этом случае это молча дало неправильный результат - не было даже конфликта, поэтому rerere не помогло бы. Вы не переубедите меня, что то, что я вижу своими глазами, неправильно. О, и если вы обнаружите, что используете rerere, вы решаете проблемы, которые сами себе создали. - person Mark Adelsberger; 25.04.2020
comment
@MarkAdelsberger не стесняйтесь публиковать этот репозиторий где-нибудь, чтобы я и другие могли понять, что вы имеете в виду. - person Inigo; 25.04.2020
comment
@Иниго - Нет. Ограничения, о которых я говорю, хорошо известны. Я не чувствую необходимости доказывать это вам только потому, что вы решили намекнуть, что я вам что-то должен. - person Mark Adelsberger; 25.04.2020
comment
Ага. так и думал - person Inigo; 25.04.2020

Первое, что нужно понять, это то, что коммиты — это неизменяемые объекты. Когда вы перепишете историю так, как вы предлагаете, вы получите совершенно другой набор коммитов. Родитель является частью неизменного хэша каждой фиксации, среди прочего, которую вы не можете изменить. Если вы сделаете то, что предлагаете, ваша история будет выглядеть так:

     D'-----E'-----> (master)
    /
---A----B-----C-----D--------E-------> (abandoned)
                     \      /
                      1----2 (foo)

Чтобы добиться этого, вы просто переустанавливаете D..E на A и сбрасываете master на E'. Вы можете (но на самом деле не обязаны) перебазировать 1..foo на D'.

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

---A----B-----C-----D--------E-----F-----> (master)
                     \      /
                      1----2 (foo)

Здесь F является результатом git rm that_file. Цель git — хранить историю. Обрезать его только потому, что он выглядит некрасиво, непродуктивно (опять же, мое мнение). Единственный раз, когда я бы порекомендовал первый вариант, это когда рассматриваемый файл содержит конфиденциальную информацию, такую ​​​​как пароли.

Если, с другой стороны, вам нужна очистка файла, вам придется принять более крайние меры. Например: Как удалить файл из истории Git?

person Mad Physicist    schedule 11.04.2020
comment
Большое спасибо за подробный ответ! К сожалению, файл содержит данные, которых не должно быть в репозитории, поэтому я думаю, что мне придется использовать первый вариант! - person Daniel Stephens; 11.04.2020
comment
@DanielStephens Если вы пытаетесь удалить файл из репозитория (например, из-за того, что он содержит конфиденциальные данные), процедур, описанных в этом ответе, может быть недостаточно. - person Mark Adelsberger; 11.04.2020