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


231

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

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

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

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

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


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

11
От одного одержимого, безумного редактора истории другому, спасибо за размещение вопроса! ; D
Марко

10
В защиту @ kch, одна вполне законная причина - это та, в которой я нахожусь: добавление снимка исторической версии, которая никогда не была захвачена в репо.
Старый МакСтофер

4
У меня есть другая законная причина! Добавление пустого коммита перед первым, чтобы иметь возможность перебазировать к первому коммиту и удалить двоичный раздув, добавленный в начальный коммит репозитория (:
pospi

Ответы:


315

Для этого есть 2 шага:

  1. Создать новый пустой коммит
  2. Переписать историю, чтобы начать с этого пустого коммита

newrootДля удобства мы поместим новый пустой коммит во временную ветку .

1. Создайте новый пустой коммит

Есть несколько способов сделать это.

Используя только сантехнику

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

  1. Создайте объект дерева для пустой директории:

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

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Создайте ссылку на него:

    git branch newroot $commit
    

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

Без сантехники

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

git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'

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

git symbolic-ref HEAD refs/heads/newroot

2. Переписать историю, чтобы начать с этого пустого коммита

У вас есть два варианта: перебазирование или переписывание чистой истории.

перебазировка

git rebase --onto newroot --root master

Это имеет преимущество простоты. Однако он также будет обновлять имя и дату коммиттера при каждом последнем коммите в ветви.

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

Переписать историю

Более чистый подход - переписать ветку. В отличие от с git rebase, вам нужно посмотреть, с какого коммита начинается ваша ветка:

git replace <currentroot> --graft newroot
git filter-branch master

Переписывание происходит на втором этапе, очевидно; это первый шаг, который требует объяснения. Это git replaceговорит Git, что всякий раз, когда он видит ссылку на объект, который вы хотите заменить, Git должен вместо этого посмотреть на замену этого объекта.

С --graftвыключателем вы говорите что-то немного другое, чем обычно. Вы говорите, что у вас еще нет объекта замены, но вы хотите заменить <currentroot>объект фиксации точной копией самого себя, за исключением того, что родительский коммит (ы) замены должен быть тем, который вы перечислили (т.е. newrootкоммит) ). Затем git replaceидет вперед и создает этот коммит для вас, а затем объявляет этот коммит в качестве замены вашего исходного коммита.

Теперь, если вы сделаете a git log, вы увидите, что все уже выглядит так, как вы хотите: ветвь начинается с newroot.

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

Вот почему filter-branchшаг необходим. При этом git replaceвы создаете точную копию с откорректированными родительскими коммитами для корневого коммита; git filter-branchзатем повторяет этот процесс для всех следующих коммитов. Вот где история на самом деле переписывается, чтобы вы могли поделиться ею.


1
Эта --onto newrootопция избыточна; Вы можете обойтись без него, потому что аргумент, который вы передаете newroot, совпадает с аргументом выше newroot.
wilhelmtell

7
Почему бы не использовать фарфор вместо сантехнических команд? Я бы заменил git symbolic-ref HEAD refs / heads / newroot на git checkout --orphan newroot
albfan

4
@nenopera: потому что этот ответ был написан раньше, когда у git-checkoutнего был этот переключатель. Я обновил его, чтобы упомянуть этот подход в первую очередь, спасибо за указатель.
Аристотель Пагальцис

1
Если ваш newroot не пустой, используйте git rebase --merge -s recursive -X theirs --onto newroot --root masterдля автоматического разрешения всех конфликтов (см. Этот ответ). @AlexanderKuzin
пользователь

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

30

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

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

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


Это отлично. Было бы хорошо, если бы это могло быть то, что внутренне сделал git rebase -i --root.
Аредридель

Да, я был удивлен, узнав, что это не так.
Энтони Хэтчкинс

Мне пришлось изменить команду rebase на git rebase newroot master, из-за ошибки.
marbel82

@ Антоний-Хэтчкинс спасибо за это. У меня есть существующее репозиторий git и (по разным причинам, которые я не буду здесь описывать) я пытаюсь добавить NI-EMPTY git commit в качестве моего первого коммита. Поэтому я заменил git commit --allow-empty -m 'initial' на git add.; git commit -m "начальный коммит laravel"; мерзавец толчок; И затем этот шаг перебазировки: git rebase --onto newroot --root master терпит неудачу с TON конфликтов слияния. Любой совет? : ((
kp123

@ kp123 попробуй пустой коммит :)
Энтони Хаткинс

12

Мне нравится ответ Аристотеля. Но обнаружил, что для большого репозитория (> 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' означают, что теги будут переписаны так, чтобы указывать на вновь созданные коммиты.


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

По сравнению с другими решениями у вас есть только один незначительный побочный эффект: он меняет хэши, но вся история остается нетронутой. Спасибо!
Владислав Савченко,

5

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

# 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) в дополнение к тегам.


что делает эта последняя строка?
Дидерик С. Нихорстер,

Он просматривает refs/original/и удаляет каждую ссылку. На ссылки, которые он удаляет, уже должна ссылаться какая-то другая ветка, поэтому они на самом деле не уходят, а просто refs/original/удаляются.
ldav1s

Это сработало для меня. Кроме того, я timedatectl set-time '2017-01-01 00:00:00'давал newrootстарую метку времени.
CHRM

4

git rebase --root --onto $emptyrootcommit

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


3
$emptyrootcommitпеременная оболочки, которая, конечно, ничего не расширяет?
Флим

@Flimm: $ emptyrootcommit - это sha1 пустого коммита, который, похоже, уже есть у исходного постера.
Уве Кляйне-Кениг

4

Я думаю, что использование 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

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


3

Я был взволнован и написал «идемпотентную» версию этого замечательного скрипта ... он всегда будет вставлять один и тот же пустой коммит, и если вы запустите его дважды, он не изменит ваши хеши коммитов каждый раз. Итак, вот мой взгляд на 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

3

Чтобы добавить пустой коммит в начале репозитория, если вы забыли создать пустой коммит сразу после «git init»:

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

1
4b825dc ... это хеш пустого дерева: stackoverflow.com/questions/9765453/…
mrks

2

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

# 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

2

Вот мой bashсценарий, основанный на ответе Кента с улучшениями:

  • он проверяет исходную ветвь, а не только 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"

2

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

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

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

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


1
Теперь, когда в rebase есть --root, это самое лучшее решение.
Росс Бертон

1

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

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 может испортиться, если не будут исправлены и другие ветви.


0

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

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).

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


-6

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

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

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

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

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

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


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

-7

Я знаю, что этот пост старый, но эта страница первая, когда 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, добавление «корневого» коммита не должно быть проблемой.

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


5
Вопрос требует вставки первого коммита: из ABC вы хотите ZABC. Простой git rebaseне может сделать это.
Петр Викторин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.