Вставити команду перед кореневою коміткою в Git?


231

Я раніше запитав про те, як розбити перші два коміти у сховищі git.

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

Отже, я б краще пережити біль лише один раз, а потім зможу назавжди використовувати стандартну інтерактивну базу даних.

Тоді я хочу зробити порожнє початкове завдання, яке існує виключно для того, щоб бути першим. Ні коду, ні нічого. Просто зайнявши простір, щоб він міг стати базою для відновлення.

Моє питання тоді, маючи наявне сховище, як я можу вставити новий, порожній комікс перед першим і перемістити всіх інших вперед?


3
;) Я думаю, це все одно вимагає відповіді. Я начебто вивчаю багато способів, коли можна звести з розуму, нав’язливо редагуючи історію. Не хвилюйтесь, не спільне сховище.
кч

11
Від одного нав’язливого, божевільного редактора історії до іншого, дякую за повідомлення! ; D
Марко

10
На захист @ kch, одна цілком законна причина - це те, що я опиняюсь: додавання знімка історичної версії, яка ніколи не була захоплена в репо.
Старий Макстофер

4
У мене є ще одна законна причина! Додавання порожнього комітету перед першим, щоб мати можливість
відновити базу даних

Відповіді:


314

Для досягнення цього є два кроки:

  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 йде далі і створює цю комісію для вас, а потім оголошує це зобов’язання як заміну оригіналу.

Тепер, якщо ви зробите це git log, ви побачите, що речі вже виглядають так, як ви їх хочете: гілка починається з newroot.

Однак зауважте, що git replace насправді історія не змінюється - і не поширюється з вашого сховища. Він просто додає локальне переспрямування у ваше сховище з одного об'єкта на інший. Це означає, що ніхто не бачить ефекту від цієї заміни - лише ви.

Ось чому filter-branchкрок необхідний. Коли git replaceви створюєте точну копію з коригуваними батьківськими комітами для кореневої фіксації; git filter-branchпотім повторює цей процес також і для всіх наступних фактів. Саме там історія переписується, щоб ви могли нею поділитися.


1
Цей --onto newrootваріант є зайвим; ви можете обійтися без нього, оскільки аргумент, який ви newrootпередаєте,, такий же, як аргумент висхідного потоку - newroot.
wilhelmtell

7
Чому б не використовувати порцелянові замість сантехнічних команд ?. Я б замінив git символічний-ref HEAD refs / heads / newroot на git checkout --orphan newroot
albfan

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

1
Якщо ваша нова кореня не порожня, використовуйте 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 робив всередині.
aredridel

Так, я здивовано дізнався, що це не так.
Ентоні Хеткінс

Мені довелося змінити команду rebase git rebase newroot masterчерез помилку.
marbel82

@ antony-hatchkins дякую за це. У мене є вже git repo і (з різних причин, з якими я не збираюся зайти сюди), я намагаюся додати NON-EMPTY git-комітет як перший мій вчинок. Тому я замінив git commit --allow-empty -m 'Initial' на git add .; git commit -m "початковий ларавельський комітет"; git push; І тоді цей крок відновлення: git rebase --o newroot - master root не вдається з TON конфліктів злиття. Будь-яка порада? : ((
kp123

@ kp123 спробуйте порожній фікс :)
Антоні Хеткінс

12

Мені подобається відповідь Арістотеля. Але виявлено, що для великого сховища (> 5000 коміттів) філія-фільтр працює краще, ніж повторне оновлення з кількох причин 1) це швидше 2) воно не потребує втручання людини, коли виникає конфлікт злиття. 3) він може переписати теги - зберігаючи їх. Зауважте, що філія фільтрів працює, оскільки не виникає сумнівів щодо вмісту кожного комітету - він точно такий же, як і перед цим "перезавантаженням".

Мої кроки:

# 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

@Flimm: $ emptyrootcommit - це sha1 порожнього комітету, який, здається, вже має оригінальний плакат.
Uwe Kleine-König

4

Я думаю, що використання git replaceі git filter-branchє кращим рішенням, ніж використання git rebase:

  • кращі показники
  • легше і менш ризиковано (ви можете перевірити результат на кожному кроці та скасувати те, що ви зробили ...)
  • добре працювати з декількома галузями із гарантованими результатами

Ідея цього полягає в тому, щоб:

  • Створіть нову порожню комісію далеко в минулому
  • Замініть старий кореневий комірок на такий самий коміт, за винятком того, що новий кореневий комікс додається як батьківський
  • Перевірте, чи все так, як очікувалося, і запустіть git filter-branch
  • Ще раз переконайтеся, що все в порядку, і очистіть не потрібні файли git

Ось сценарій для двох перших кроків:

#!/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

Я був схвильований і написав "idempotent" версію цього приємного сценарію ... він завжди буде вставляти ту саму порожню комісію, і якщо ви будете виконувати її двічі, вона не змінюватиме ваші хеші комісій кожного разу. Отже, ось я взяв на себе 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 / ...
МРК

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
Тепер, коли база даних має -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).

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


-6

Запустіть новий сховище.

Встановіть дату до потрібної дати початку.

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

Дійшовши до сьогодні, поміняйте сховищами і закінчите.

Якщо ви просто божевільний (встановлений), але досить розумний (ймовірно, тому що вам доведеться мати певну кількість розумних, щоб придумати шалені ідеї, як це), ви складете сценарій цього процесу.

Це також зробить це приємніше, коли ви вирішите, що хочете, щоб минуле сталося по-іншому за тиждень.


У мене погані почуття щодо рішення, яке вимагає, щоб ви заплуталися з системною датою, але ви все-таки дали мені ідею, яку я трохи розробив, і, на жаль, вона спрацювала. Отже, дякую.
кч

-7

Я знаю, що ця публікація стара, але ця сторінка є першою, коли Гуглінг "вставляє фік гйт".

Навіщо ускладнювати прості речі?

У вас 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, додавання команди «root» не повинно бути проблемою.

Веселіться з git!


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