Сжать первые два коммита в Git?

С git rebase --interactive <commit> его помощью вы можете объединить любое количество коммитов в один.

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

Есть ли способы этого добиться?


Умеренно связанные:

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

Если интересно: git: как вставить коммит первым, сдвинув все остальные?

Ответов (9)

Решение

Обновление от июля 2012 г. ( git 1.7.12+ )

Теперь вы можете перебазировать все коммиты до root и выбрать вторую фиксацию, Y которая будет объединена с первой X .

git rebase -i --root master

pick sha1 X
squash sha1 Y
pick sha1 Z
git rebase [-i] --root $tip

Эту команду теперь можно использовать для перезаписи всей истории, ведущей от " $tip" до корневого коммита.

См. Commit df5df20c1308f936ea542c86df1e9c6974168472 на GitHub от Криса Уэбба ( arachsys) .


Оригинальный ответ (февраль 2009 г.)

Я полагаю, вы найдете разные рецепты для этого в вопросе SO " Как мне объединить первые два коммита репозитория git? "

Чарльз Бейли дал там наиболее подробный ответ , напомнив нам, что фиксация - это полное дерево (а не просто отличия от предыдущих состояний).
И здесь старая фиксация («начальная фиксация») и новая фиксация (результат сжатия) не будут иметь общего предка.
Это означает, что вы не можете " commit --amend " первоначальную фиксацию в новую, а затем переустановить на новую исходную фиксацию истории предыдущей первоначальной фиксации (много конфликтов)

(Это последнее предложение уже неверно git rebase -i --root <aBranch> )

Скорее (с A исходной «начальной фиксацией», а B последующую фиксацию нужно было сжать с исходной):

  1. Вернитесь к последней фиксации, которую мы хотим сформировать начальную фиксацию (отсоедините HEAD):

    git checkout <sha1_for_B>
    
  2. Сбросьте указатель ветки на начальную фиксацию, но оставив индекс и рабочее дерево нетронутыми:

    git reset --soft <sha1_for_A>
    
  3. Измените исходное дерево, используя дерево из "B":

    git commit --amend
    
  4. Временно отметьте эту новую начальную фиксацию (или вы можете вспомнить новую фиксацию sha1 вручную):

    git tag tmp
    
  5. Вернитесь к исходной ветке (в этом примере возьмем master):

    git checkout master
    
  6. Воспроизвести все коммиты после B в новом начальном коммите:

    git rebase --onto tmp <sha1_for_B>
    
  7. Удалите временный тег:

    git tag -d tmp
    

Таким образом, " rebase --onto " не приводит к конфликтам во время слияния, так как он перемещает историю, созданную после последней фиксации ( B ), которая должна быть сжата в исходную (которая была A ), чтобы tmp (представляя сжатую новую первоначальную фиксацию): тривиальная перемотка вперед только сливается.

Это работает для " A-B ", но также и для " A-...-...-...-B " (таким образом можно втиснуть любое количество коммитов в исходный)

Это превратит вторую фиксацию в первую:

A-B-C-... -> AB-C-...

git filter-branch --commit-filter '
    if [ "$GIT_COMMIT" = <sha1ofA> ];
    then
        skip_commit "[email protected]";
    else
        git commit-tree "[email protected]";
    fi
' HEAD

Сообщение о фиксации для AB будет взято из B (хотя я бы предпочел из A).

Имеет тот же эффект, что и ответ Уве Кляйне-Кенига, но работает и для не начального A.

Как бы то ни было, я избегаю этой проблемы, всегда создавая первую фиксацию «без операции», в которой единственное, что находится в репозитории, - это пустой .gitignore:

https://github.com/DarwinAwardWinner/git-custom-commands/blob/master/bin/git-myinit

Таким образом, нет причин возиться с первой фиксацией.

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

git rebase HEAD^^ -i

Есть более простой способ сделать это. Предположим, вы на master ветке

Создайте новую осиротевшую ветку, которая удалит всю историю коммитов:

$ git checkout --orphan new_branch

Добавьте свое первоначальное сообщение о фиксации:

$ git commit -a

Избавьтесь от старой неслитой основной ветки:

$ git branch -D master

Переименуйте текущую ветку new_branch в master :

$ git branch -m master

Если вы просто хотите объединить все коммиты в одну начальную фиксацию, просто сбросьте репозиторий и измените первую фиксацию:

git reset hash-of-first-commit
git add -A
git commit --amend

Git reset оставит рабочее дерево нетронутым, поэтому все по-прежнему там. Так что просто добавьте файлы с помощью команд git add и внесите эти изменения в первую фиксацию. Однако по сравнению с rebase -i вы потеряете возможность объединять комментарии git.

Сжатие первой и второй фиксации приведет к перезаписи первой фиксации. Если у вас есть более одной ветки, основанной на первом коммите, вы должны отрезать эту ветку.

Рассмотрим следующий пример:

a---b---HEAD
 \
  \
   '---d

Сжатие a и b в новую фиксацию «ab» приведет к появлению двух разных деревьев, что в большинстве случаев нежелательно, поскольку git-merge и git-rebase больше не будут работать в двух ветвях.

ab---HEAD

a---d

Если вы действительно этого хотите, это можно сделать. Посмотрите на git-filter-branch мощный (и опасный) инструмент для переписывания истории.

Я переработал скрипт VonC, чтобы он делал все автоматически и ни о чем не просил. Вы даете ему два коммита SHA1, и он сжимает все между ними в один коммит с именем «сжатая история»:

#!/bin/sh
# Go back to the last commit that we want
# to form the initial commit (detach HEAD)
git checkout $2

# reset the branch pointer to the initial commit (= $1),
# but leaving the index and working tree intact.
git reset --soft $1

# amend the initial tree using the tree from $2
git commit --amend -m "squashed history"

# remember the new commit sha1
TARGET=`git rev-list HEAD --max-count=1`

# go back to the original branch (assume master for this example)
git checkout master

# Replay all the commits after $2 onto the new initial commit
git rebase --onto $TARGET $2

Для этого вы можете использовать git filter-branch. например

git filter-branch --parent-filter \
'if test $GIT_COMMIT != <sha1ofB>; then cat; fi'

Это приводит к тому, что AB-C отбрасывает журнал фиксации A.