Как удалить коммиты из истории git, но при этом сохранить график точно таким же, включая слияния? - PullRequest
3 голосов
/ 11 апреля 2020

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

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

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

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

А, пока go я сделал два коммиты, которые я хотел бы удалить из моего 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"

Ответы [ 5 ]

3 голосов
/ 24 апреля 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. в интерактивном ребазе todo файл:
    • удалите два коммита, которые вы хотели отбросить (или закомментируйте их, или измените pick на drop или d)
    • на новой строке сразу после строки label foo, добавьте следующее:
    exec git branch -f foo head
    
    (см. объяснение ниже)
  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 изменяет ref foo, чтобы он указывал на текущий head. Как видно из последовательности в файле todo, вы говорите ему сделать это сразу после того, как он зафиксировал последний коммит ветки foo («add 2»), который удобно обозначен label foo в todo файл.

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

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

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

Дайте мне знать, если у вас есть какие-либо вопросы.


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

2 голосов
/ 24 апреля 2020

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

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

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

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

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


Is git перебазировать функцию I ищу мою проблему?

Не совсем. Опция --rebase-merges расширяется, и ответ Ini go хорошо работает для вашего конкретного случая c, но см. Предупреждения в его документах : он имеет реальные ограничения и предостережения. Как указывает ответ Ini go, «[t] эти шаги предполагают точное репо, которое вы показываете в своем вопросе», а «git rebase просто автоматизирует серию шагов, которые вы также можете сделать вручную». Причина этого ответа в том, что для одноразовой работы обычно лучше просто сделать это.

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

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

Что вы вы хотите не создавать единую чистую линейную историю, вы хотите что-то другое.

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

* 1048. *

и все готово.

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

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

- это ответ "все, что работает", я бы, наверное, используйте для, поскольку Ini go говорит "точное репо, которое вы показываете в своем вопросе". См. документы git help revisions для синтаксиса поиска сообщений .

2 голосов
/ 12 апреля 2020

Хотя то, что я предлагаю, даст вам чистую, линейную историю; это то, что ребаз должен делать по существу. Однако я надеюсь, что это даст вам возможность удалить 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.

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

1 голос
/ 11 апреля 2020

Чтобы изменить порядок фиксации истории, существует несколько способов.

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

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

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

Несколько замечаний:

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

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

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

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

1 голос
/ 11 апреля 2020

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

     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 истории?

...