Вставить коммит перед корневым коммитом в Git?

Я уже спрашивал о том, как раздавить первые два коммита в репозитории git.

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

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

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

Тогда мой вопрос, имея существующее хранилище, как мне вставить новый пустой коммит перед первым и переместить всех остальных вперед?

16 ответов

Решение

Середина 2017 года ответ

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

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

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Теперь мы можем обернуть коммит вокруг него:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. И теперь мы можем сделать это:

    git rebase --onto $commit --root master
    

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

(NB: на практике я бы сейчас использовал filter-branch, Отредактирую это позже.)


Исторический ответ (на который ссылаются другие ответы)

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

# first you need a new empty branch; let's call it `newroot`
git checkout --orphan newroot
git rm -rf .

# then you apply the same steps
git commit --allow-empty -m 'root commit'
git rebase --onto newroot --root master
git branch -d newroot

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


NB.: на старых версиях Git, в которых нет --orphan переключиться на checkoutВам понадобится сантехника для создания пустой ветки:

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

Слияние ответов Аристотеля Пагальциса и Уве Кляйн-Кенига и комментария Ричарда Броноски.

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
# touch .gitignore && git add .gitignore # if necessary
git commit --allow-empty -m 'initial'
git rebase --onto newroot --root master
git branch -d newroot

(просто положить все в одно место)

Мне нравится ответ Аристотеля. Но обнаружил, что для большого репозитория (>5000 коммитов) ветвь фильтра работает лучше, чем ребаз по нескольким причинам: 1) это быстрее, 2) не требует вмешательства человека, когда возникает конфликт слияния. 3) он может переписать теги - сохранив их. Обратите внимание, что фильтр-ветвь работает, потому что нет никаких сомнений относительно содержимого каждого коммита - он точно такой же, как и до этого 'rebase'.

Мои шаги:

# first you need a new empty branch; let's call it `newroot`
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

# then you apply the same steps
git commit --allow-empty -m 'root commit'

# then use filter-branch to rebase everything on newroot
git filter-branch --parent-filter 'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat master

Обратите внимание, что опции '--tag-name-filter cat' означают, что теги будут переписаны так, чтобы они указывали на вновь созданные коммиты.

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

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)

Я думаю, что с помощью git replace а также git filter-branch это лучшее решение, чем использование git rebase:

  • лучшее исполнение
  • проще и менее рискованно (вы можете проверить свой результат на каждом шаге и отменить то, что вы сделали...)
  • хорошо работать с несколькими ветками с гарантированными результатами

За этим стоит идея:

  • Создать новый пустой коммит далеко в прошлом
  • Замените старый корневой коммит на точно такой же, за исключением того, что новый корневой коммит добавляется как родительский.
  • Убедитесь, что все как ожидалось, и запустите git filter-branch
  • Еще раз проверьте, что все в порядке и очистите ненужные файлы git.

Вот скрипт для 2 первых шагов:

#!/bin/bash
root_commit_sha=$(git rev-list --max-parents=0 HEAD)
git checkout --force --orphan new-root
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null
git add -A
GIT_COMMITTER_DATE="2000-01-01T12:00:00" git commit --date==2000-01-01T12:00:00 --allow-empty -m "empty root commit"
new_root_commit_sha=$(git rev-parse HEAD)

echo "The commit '$new_root_commit_sha' will be added before existing root commit '$root_commit_sha'..."

parent="parent $new_root_commit_sha"
replacement_commit=$(
 git cat-file commit $root_commit_sha | sed "s/author/$parent\nauthor/" |
 git hash-object -t commit -w --stdin
) || return 3
git replace "$root_commit_sha" "$replacement_commit"

Вы можете запустить этот сценарий без риска (даже если создание резервной копии перед выполнением действия, которое вы никогда не выполняли ранее, является хорошей идеей;)), а если результат не тот, который ожидается, просто удалите файлы, созданные в папке. .git/refs/replace и попробуй еще раз;)

Убедившись, что состояние хранилища соответствует ожидаемому, выполните следующую команду, чтобы обновить историю всех ветвей:

git filter-branch -- --all

Теперь вы должны увидеть 2 истории, старую и новую (см. Справку на filter-branch для дополнительной информации). Вы можете сравнить 2 и еще раз проверить, все ли в порядке. Если вы удовлетворены, удалите ненужные файлы:

rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace

Вы могли бы вернуться к своему master Разветвите и удалите временную ветку:

git checkout master
git branch -D new-root

Теперь все должно быть сделано;)

Чтобы переключить корневой коммит:

Сначала создайте коммит, который вы хотите первым.

Во-вторых, измените порядок коммитов, используя:

git rebase -i --root

Редактор будет отображаться с коммитами вплоть до корневого коммита, например:

выбрать 1234 старое корневое сообщение

выбрать 0294 коммит в середине

выбрать 5678 коммит вы хотите положить в корень

Затем вы можете поместить нужный коммит первым, поместив его в первую строку. В примере:

выбрать 5678 коммит вы хотите положить в корень

выбрать 1234 старое корневое сообщение

выбрать 0294 коммит в середине

Выйдите из редактора, порядок фиксации будет изменен.

PS: Чтобы изменить редактор, который использует git, запустите:

git config --global core.editor name_of_the_editor_program_you_want_to_use

Я успешно использовал фрагменты ответа Аристотеля и Кента:

# first you need a new empty branch; let's call it `newroot`
git checkout --orphan newroot
git rm -rf .
git commit --allow-empty -m 'root commit'
git filter-branch --parent-filter \
'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat -- --all
# clean up
git checkout master
git branch -D newroot
# make sure your branches are OK first before this...
git for-each-ref --format="%(refname)" refs/original/ | \
xargs -n 1 git update-ref -d

Это также перезапишет все ветви (не только master) в дополнение к тегам.

git rebase --root --onto $emptyrootcommit

должен сделать трюк легко

Я был взволнован и написал "идемпотентную" версию этого замечательного скрипта... он всегда будет вставлять один и тот же пустой коммит, и если вы запустите его дважды, он не изменит ваши хеши коммитов каждый раз. Итак, вот мой взгляд на git-insert-empty-root:

#!/bin/sh -ev
# idempotence achieved!
tmp_branch=__tmp_empty_root
git symbolic-ref HEAD refs/heads/$tmp_branch
git rm --cached -r . || true
git clean -f -d
touch -d '1970-01-01 UTC' .
GIT_COMMITTER_DATE='1970-01-01T00:00:00 +0000' git commit \
  --date='1970-01-01T00:00:00 +0000' --allow-empty -m 'initial'
git rebase --committer-date-is-author-date --onto $tmp_branch --root master
git branch -d $tmp_branch

Стоит ли дополнительная сложность? возможно нет, но я буду использовать это.

Это ДОЛЖНО также позволять выполнять эту операцию с несколькими клонированными копиями репозитория, и в конечном итоге с одинаковыми результатами, поэтому они все еще совместимы... тестирование... да, это работает, но нужно также удалить и добавить свой снова пульты, например:

git remote rm origin
git remote add --track master user@host:path/to/repo

Ну, вот что я придумал:

# Just setting variables on top for clarity.
# Set this to the path to your original repository.
ORIGINAL_REPO=/path/to/original/repository

# Create a new repository…
mkdir fun
cd fun
git init
# …and add an initial empty commit to it
git commit --allow-empty -m "The first evil."

# Add the original repository as a remote
git remote add previous $ORIGINAL_REPO
git fetch previous

# Get the hash for the first commit in the original repository
FIRST=`git log previous/master --pretty=format:%H  --reverse | head -1`
# Cherry-pick it
git cherry-pick $FIRST
# Then rebase the remainder of the original branch on top of the newly 
# cherry-picked, previously first commit, which is happily the second 
# on this branch, right after the empty one.
git rebase --onto master master previous/master

# rebase --onto leaves your head detached, I don't really know why)
# So now you overwrite your master branch with the newly rebased tree.
# You're now kinda done.
git branch -f master
git checkout master
# But do clean up: remove the remote, you don't need it anymore
git remote rm previous

Вот мой bash скрипт на основе ответа Kent с улучшениями:

  • это проверяет оригинальную ветку, а не только masterкогда сделано;
  • Я пытался избежать временной ветки, но git checkout --orphan работает только с ветвью, а не с состоянием отсоединенной головы, поэтому он извлекается достаточно долго, чтобы сделать новый корневой коммит, а затем удаляется;
  • он использует хеш нового корневого коммита во время filter-branch (Кент оставил там заполнитель для ручной замены);
  • filter-branch операция переписывает только локальные ветки, а не пульты тоже
  • метаданные автора и коммиттера стандартизированы, так что корневой коммит одинаков для всех репозиториев.

#!/bin/bash

# Save the current branch so we can check it out again later
INITIAL_BRANCH=`git symbolic-ref --short HEAD`
TEMP_BRANCH='newroot'

# Create a new temporary branch at a new root, and remove everything from the tree
git checkout --orphan "$TEMP_BRANCH"
git rm -rf .

# Commit this empty state with generic metadata that will not change - this should result in the same commit hash every time
export GIT_AUTHOR_NAME='nobody'
export GIT_AUTHOR_EMAIL='nobody@example.org'
export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
git commit --allow-empty -m 'empty root'
NEWROOT=`git rev-parse HEAD`

# Check out the commit we just made and delete the temporary branch
git checkout --detach "$NEWROOT"
git branch -D "$TEMP_BRANCH"

# Rewrite all the local branches to insert the new root commit, delete the 
# original/* branches left behind, and check out the rewritten initial branch
git filter-branch --parent-filter "sed \"s/^\$/-p $NEWROOT/\"" --tag-name-filter cat -- --branches
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
git checkout "$INITIAL_BRANCH"

Объедините ответ Айви и комментарий mrks к нему с ответом VonC отсюда :

      git rebase --root --onto $(git commit-tree -m 'root_commit' $(git hash-object -t tree /dev/null))

Это охватывает ситуацию ближайшего будущего: когда в будущих версиях Git не будет

sha1:4b825dc642cb6eb9a060e54bf8d69288fbee4904

для его пустого узла дерева вообще, но

sha256:6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321

только вместо этого. Эта команда должна работать даже после перехода SHA-2 так же, как и сегодня.

Сочетание новейшего и лучшего. Никаких побочных эффектов, никаких конфликтов, сохранение тегов.

git log --reverse

tree=`git hash-object -wt tree --stdin < /dev/null`
commit=`git commit-tree -m 'Initialize empty repository' $tree`
echo $commit # copy below, interpolation didn't work for me

git filter-branch --parent-filter 'sed "s/^\$/-p <commit>/"' --tag-name-filter cat master

git log --reverse

Обратите внимание, что на GitHub вы потеряете данные запуска CI, и PR может испортиться, если другие ветки также не будут исправлены.

Следующий ответ Аристотель Пагальцис и другие, но с использованием более простых команд

zsh% git checkout --orphan empty     
Switched to a new branch 'empty'
zsh% git rm --cached -r .
zsh% git clean -fdx
zsh% git commit --allow-empty -m 'initial empty commit'
[empty (root-commit) 64ea894] initial empty commit
zsh% git checkout master
Switched to branch 'master'
zsh% git rebase empty
First, rewinding head to replay your work on top of it...
zsh% git branch -d empty 
Deleted branch empty (was 64ea894).

Обратите внимание, что в вашем репо не должно быть локальных изменений, ожидающих подтверждения.
Заметка git checkout --orphan будет работать на новых версиях git, наверное.
Обратите внимание, большую часть времени git status дает полезные советы

Начать новый репозиторий.

Установите дату обратно на желаемую дату начала.

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

Когда вы доберетесь до сегодняшнего дня, поменяйте местами репозитории, и все готово.

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

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

Я знаю, что этот пост старый, но эта страница первая, когда Googling "вставляет коммит git".

Зачем делать простые вещи сложными?

У вас есть ABC, и вы хотите ABZC.

  1. git rebase -i trunk (или что-нибудь до B)
  2. изменить выбор для редактирования на линии B
  3. внесите свои изменения: git add ..
  4. git commit (git commit --amend который будет редактировать B, а не создавать Z)

[Вы можете сделать как можно больше git commit как вы хотите здесь, чтобы вставить больше коммитов. Конечно, у вас могут возникнуть проблемы с шагом 5, но разрешение конфликта слияния с помощью git - это навык, который вы должны иметь. Если нет, практикуйтесь!]

  1. git rebase --continue

Просто, не правда ли?

Если ты понимаешь git rebaseдобавление "корневого" коммита не должно быть проблемой.

Веселитесь с мерзавцем!

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