Masukkan komit sebelum root komit di Git?


231

Saya telah bertanya sebelumnya tentang bagaimana menghapus dua commit pertama dalam repositori git.

Walaupun solusinya agak menarik dan tidak terlalu membingungkan seperti beberapa hal lain di git, solusi tersebut masih sedikit seperti pepatah yang menyakitkan jika Anda perlu mengulangi prosedur ini berkali-kali selama pengembangan proyek Anda.

Jadi, saya lebih suka melewati rasa sakit hanya sekali, dan kemudian dapat selamanya menggunakan rebase interaktif standar.

Jadi, yang ingin saya lakukan adalah memiliki komit awal kosong yang hanya ada untuk tujuan yang pertama. Tidak ada kode, tidak apa-apa. Hanya mengambil ruang sehingga bisa menjadi basis untuk rebase.

Pertanyaan saya kemudian adalah, apakah memiliki repositori yang sudah ada, bagaimana cara memasukkan komit baru yang kosong sebelum yang pertama, dan menggeser orang lain ke depan?


3
;) Saya kira itu menjamin jawaban. Saya semacam mengeksplorasi banyak cara orang bisa menjadi gila dengan mengedit sejarah secara obsesif. Jangan khawatir, bukan repositori bersama.
kch

11
Dari satu editor obsesif, sejarah yang gila ke yang lain, terima kasih telah mengirim pertanyaan! ; D
Marco

10
Dalam pembelaan @ kch, satu alasan sah yang sempurna adalah alasan yang saya temukan: Menambahkan cuplikan versi historis yang tidak pernah ditangkap dalam repo.
Old McStopher

4
Saya punya alasan sah lainnya! Menambahkan komit kosong sebelum komit pertama agar dapat rebase ke komit pertama dan menghapus mengasapi biner ditambahkan dalam komit awal repositori (:
pospi

Jawaban:


314

Ada 2 langkah untuk mencapai ini:

  1. Buat komit kosong baru
  2. Tulis ulang riwayat untuk memulai dari komit kosong ini

Kami akan menempatkan komit kosong yang baru di cabang sementara newrootuntuk kenyamanan.

1. Buat komit kosong baru

Ada beberapa cara Anda bisa melakukan ini.

Hanya menggunakan pipa ledeng

Pendekatan terbersih adalah dengan menggunakan pipa ledeng Git hanya untuk membuat komit secara langsung, yang menghindari menyentuh copy pekerjaan atau indeks atau cabang mana yang diperiksa, dll.

  1. Buat objek pohon untuk direktori kosong:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Bungkus sebuah komit di sekitarnya:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Buat referensi untuk itu:

    git branch newroot $commit
    

Anda tentu saja dapat mengatur ulang seluruh prosedur menjadi satu-liner jika Anda cukup tahu shell Anda.

Tanpa pipa ledeng

Dengan perintah porselen biasa, Anda tidak dapat membuat komit kosong tanpa memeriksa newrootcabang dan memperbarui indeks dan copy pekerjaan berulang kali, tanpa alasan yang bagus. Tetapi beberapa mungkin menemukan ini lebih mudah untuk dipahami:

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

Perhatikan bahwa pada versi Git yang sangat lama yang tidak memiliki --orphansakelar checkout, Anda harus mengganti baris pertama dengan ini:

git symbolic-ref HEAD refs/heads/newroot

2. Tulis ulang riwayat untuk memulai dari komit kosong ini

Anda memiliki dua opsi di sini: rebasing, atau penulisan ulang riwayat bersih.

Rebasing

git rebase --onto newroot --root master

Ini memiliki sifat kesederhanaan. Namun, itu juga akan memperbarui nama dan tanggal committer pada setiap komit terakhir di cabang.

Juga, dengan beberapa riwayat kasus tepi, itu bahkan mungkin gagal karena menggabungkan konflik - terlepas dari kenyataan bahwa Anda rebasing ke komit yang tidak mengandung apa pun.

Menulis ulang sejarah

Pendekatan yang lebih bersih adalah menulis ulang cabang. Berbeda dengan dengan git rebase, Anda harus mencari komit yang dimulai dari cabang Anda:

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

Penulisan ulang terjadi pada langkah kedua, jelas; itu langkah pertama yang perlu penjelasan. Apa yang git replacedilakukan itu memberi tahu Git bahwa setiap kali ia melihat referensi ke objek yang ingin Anda ganti, Git seharusnya melihat penggantian objek itu.

Dengan --graftsakelar, Anda mengatakan sesuatu yang sedikit berbeda dari biasanya. Anda mengatakan belum memiliki objek pengganti, tetapi Anda ingin mengganti <currentroot>objek komit dengan salinan dirinya sendiri kecuali komit induk pengganti haruslah yang terdaftar (yaitu newrootkomit ). Kemudian git replacelanjutkan dan buat komit ini untuk Anda, dan kemudian nyatakan komit itu sebagai pengganti komit asli Anda.

Sekarang jika Anda melakukan git log, Anda akan melihat bahwa segala sesuatu sudah terlihat seperti yang Anda inginkan: cabang dimulai newroot.

Namun, perhatikan bahwa git replace sebenarnya tidak memodifikasi histori - juga tidak menyebar keluar dari repositori Anda. Itu hanya menambahkan redirect lokal ke repositori Anda dari satu objek ke objek lain. Artinya, tidak ada orang lain yang melihat efek dari penggantian ini - hanya Anda.

Itu sebabnya filter-branchlangkah ini perlu. Dengan git replaceAnda membuat salinan yang tepat dengan komitmen induk yang disesuaikan untuk komit root; git filter-branchkemudian ulangi proses ini untuk semua komitmen berikut juga. Di situlah sejarah sebenarnya ditulis ulang sehingga Anda dapat membagikannya.


1
Itu --onto newrootpilihan adalah berlebihan; Anda dapat melakukannya tanpanya karena argumen yang Anda berikan,, newrootsama dengan argumen hulu - newroot.
wilhelmtell

7
Mengapa tidak menggunakan perintah porselen sebagai ganti pipa? Saya akan mengganti git simbolis-ref kepala / ref / headroot dengan checkout git --orphan newroot
albfan

4
@nenopera: karena jawaban ini ditulis sebelum git-checkoutberalih itu. Saya telah memperbaruinya dengan menyebutkan pendekatan itu terlebih dahulu, terima kasih atas penunjuknya.
Aristoteles Pagaltzis

1
Jika akar baru Anda tidak kosong, gunakan git rebase --merge -s recursive -X theirs --onto newroot --root masteruntuk menyelesaikan semua konflik secara otomatis (lihat jawaban ini ). @AlexanderKuzin
pengguna

1
@Geremia Anda hanya dapat mengubah komit terakhir, jadi jika repositori Anda hanya berisi komit root, mungkin berhasil, jika tidak, Anda harus mengubah semua komit lain dalam repo di atas komit root yang diamandemen. Tetapi meskipun begitu, topik tersebut menyiratkan bahwa Anda tidak ingin mengubah komit root, tetapi ingin memasukkan yang lain sebelum root yang ada.
pengguna

30

Gabungan jawaban Aristoteles Pagaltzis dan Uwe Kleine-König dan komentar Richard Bronosky.

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

(hanya untuk meletakkan semuanya di satu tempat)


Ini luar biasa. Akan lebih baik jika ini bisa menjadi apa yang dilakukan git rebase -i --root secara internal.
aredridel

Yap, saya kaget ternyata ternyata tidak.
Antony Hatchkins

Saya harus mengubah perintah rebase git rebase newroot master, karena kesalahan.
marbel82

@ antony-hatchkins terima kasih untuk ini. Saya memiliki repo git yang ada dan (karena berbagai alasan yang tidak akan saya bahas di sini) saya mencoba untuk menambahkan komit git yang TIDAK KOSONG sebagai komit pertama saya. Jadi saya mengganti git commit --allow-empty -m 'initial' dengan git add.; git commit -m "commit laravel awal"; git push; Dan kemudian langkah rebase ini: git rebase --onto newroot - master master gagal dengan TON konflik gabungan. Ada saran? : ((
kp123

@ kp123 coba komit kosong :)
Antony Hatchkins

12

Saya suka jawaban Aristoteles. Tetapi menemukan bahwa untuk repositori besar (> 5000 commit), filter-branch bekerja lebih baik daripada rebase karena beberapa alasan 1) lebih cepat 2) tidak memerlukan intervensi manusia ketika ada konflik gabungan. 3) itu dapat menulis ulang tag - melestarikannya. Perhatikan bahwa cabang-filter berfungsi karena tidak ada pertanyaan tentang konten dari setiap komit - itu persis sama dengan sebelum 'rebase' ini.

Langkah saya adalah:

# 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

Perhatikan bahwa opsi '--tag-name-filter cat' berarti bahwa tag akan ditulis ulang untuk menunjukkan komit yang baru dibuat.


Ini tidak membantu membuat komit kosong yang juga merupakan use case yang menarik.
ceztko

Dibandingkan dengan solusi lain, Anda hanya memiliki satu efek samping yang tidak signifikan: ia mengubah hash, tetapi seluruh sejarah tetap tidak tersentuh. Terima kasih!
Vladyslav Savchenko

5

Saya menggunakan potongan jawaban Aristoteles dan Kent dengan sukses:

# 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

Ini juga akan menulis ulang semua cabang (bukan hanya master) di samping tag.


apa yang dilakukan baris terakhir ini?
Diederick C. Niehorster

Ia mencari refs/original/dan menghapus setiap referensi. Wasit yang dihapus seharusnya sudah direferensikan oleh beberapa cabang lain, sehingga mereka tidak benar-benar pergi, hanya refs/original/akan dihapus.
ldav1s

Ini berhasil untuk saya. Selain itu saya biasa timedatectl set-time '2017-01-01 00:00:00'memberi newrootcap waktu lama.
chrm

4

git rebase --root --onto $emptyrootcommit

harus melakukan trik dengan mudah


3
$emptyrootcommitadalah variabel shell yang mengembang ke apa-apa, tentunya?
Flimm

@ Flimm: $ blankrootcommit adalah sha1 dari commit kosong yang sudah dimiliki oleh poster asli.
Uwe Kleine-König

4

Saya pikir menggunakan git replacedan git filter-branchmerupakan solusi yang lebih baik daripada menggunakan git rebase:

  • kinerja yang lebih baik
  • lebih mudah dan lebih tidak berisiko (Anda dapat memverifikasi hasil di setiap langkah dan membatalkan apa yang Anda lakukan ...)
  • bekerja dengan baik dengan beberapa cabang dengan hasil yang terjamin

Ide di baliknya adalah untuk:

  • Buat komit kosong baru jauh di masa lalu
  • Ganti komit root lama dengan komit persis sama kecuali bahwa komit root baru ditambahkan sebagai orangtua
  • Verifikasi bahwa semua sudah seperti yang diharapkan dan berjalan git filter-branch
  • Sekali lagi, verifikasi bahwa semuanya OK dan bersihkan file git yang tidak diperlukan lagi

Berikut ini skrip untuk 2 langkah pertama:

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

Anda dapat menjalankan skrip ini tanpa risiko (bahkan jika melakukan pencadangan sebelum melakukan tindakan yang tidak pernah Anda lakukan sebelumnya adalah ide yang baik;)), dan jika hasilnya tidak sesuai harapan, hapus saja file yang dibuat dalam folder .git/refs/replacedan coba lagi; )

Setelah Anda memverifikasi bahwa keadaan repositori adalah yang Anda harapkan, jalankan perintah berikut untuk memperbarui sejarah semua cabang :

git filter-branch -- --all

Sekarang, Anda harus melihat 2 sejarah, yang lama dan yang baru (lihat bantuan filter-branchuntuk informasi lebih lanjut). Anda dapat membandingkan 2 dan memeriksa lagi apakah semuanya OK. Jika Anda puas, hapus file yang tidak diperlukan lagi:

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

Anda bisa kembali ke mastercabang Anda dan menghapus cabang sementara:

git checkout master
git branch -D new-root

Sekarang, semua harus dilakukan;)


3

Saya menjadi bersemangat dan menulis versi 'idempoten' dari skrip yang bagus ini ... itu akan selalu memasukkan komit kosong yang sama, dan jika Anda menjalankannya dua kali, itu tidak mengubah hash komit Anda setiap kali. Jadi, inilah pendapat saya tentang 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

Apakah sebanding dengan kompleksitas ekstra? mungkin tidak, tapi saya akan menggunakan yang ini.

HARUS ini juga memungkinkan untuk melakukan operasi ini pada beberapa salinan repo kloning, dan berakhir dengan hasil yang sama, sehingga mereka masih kompatibel ... menguji ... ya itu berhasil, berfungsi, tetapi perlu juga menghapus dan menambahkan Anda remote lagi, misalnya:

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

3

Untuk menambahkan komit kosong di awal repositori, jika Anda lupa membuat komit kosong segera setelah "git init":

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

1
4b825dc ... adalah hash dari pohon kosong: stackoverflow.com/questions/9765453/…
mrks

2

Nah, inilah yang saya pikirkan:

# 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

Berikut bashskrip saya berdasarkan jawaban Kent dengan peningkatan:

  • memeriksa cabang asli, bukan hanya master, ketika dilakukan;
  • Saya mencoba menghindari cabang sementara, tetapi git checkout --orphanhanya bekerja dengan cabang, bukan keadaan kepala terpisah, jadi itu diperiksa cukup lama untuk membuat root baru komit dan kemudian dihapus;
  • menggunakan hash dari root commit baru selama filter-branch(Kent meninggalkan pengganti di sana untuk penggantian manual);
  • satu filter-branchoperasi penulisan ulang hanya cabang-cabang lokal, tidak remote terlalu
  • metadata penulis dan pengangkat distandarisasi sehingga root commit identik di seluruh repositori.

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

Untuk mengalihkan komit root:

Pertama, buat komit yang Anda inginkan sebagai yang pertama.

Kedua, alihkan urutan komit menggunakan:

git rebase -i --root

Editor akan muncul dengan komit sampai root melakukan, seperti:

pilih pesan lama 1234 root

pilih 0294 A komit di tengah

pilih 5678 komit yang ingin Anda masukkan ke root

Anda kemudian dapat menempatkan komit yang Anda inginkan terlebih dahulu, dengan menempatkannya di baris pertama. Dalam contoh:

pilih 5678 komit yang ingin Anda masukkan ke root

pilih pesan lama 1234 root

pilih 0294 A komit di tengah

Keluar dari editor, perintah komit akan berubah.

PS: Untuk mengubah penggunaan editor git, jalankan:

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
Sekarang rebase telah --root, ini adalah solusi yang paling rapi.
Ross Burton

1

Menggabungkan yang terbaru dan terhebat. Tidak ada efek samping, tidak ada konflik, menyimpan tag.

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

Perhatikan bahwa pada GitHub Anda akan kehilangan data CI run dan PR mungkin akan kacau kecuali cabang lain diperbaiki juga.


0

Mengikuti jawaban Aristoteles Pagaltzis dan lainnya tetapi menggunakan perintah yang lebih sederhana

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

Perhatikan bahwa repo Anda tidak boleh mengandung modifikasi lokal yang menunggu untuk dilakukan.
Note git checkout --orphanakan bekerja di git versi baru, kurasa.
Catatan sebagian besar waktu git statusmemberikan petunjuk yang bermanfaat.


-6

Mulai repositori baru.

Atur kembali tanggal Anda ke tanggal mulai yang Anda inginkan.

Lakukan segala cara yang Anda inginkan Anda lakukan, sesuaikan waktu sistem untuk mencerminkan kapan Anda berharap Anda melakukannya dengan cara itu. Tarik file dari repositori yang ada sesuai kebutuhan untuk menghindari banyak pengetikan yang tidak perlu.

Ketika Anda sampai hari ini, tukar repositori dan Anda selesai.

Jika Anda hanya gila (mapan) tetapi cukup cerdas (mungkin, karena Anda harus memiliki sejumlah kecerdasan untuk memikirkan ide-ide gila seperti ini), Anda akan membuat skrip prosesnya.

Itu juga akan membuatnya lebih baik ketika Anda memutuskan Anda ingin masa lalu telah terjadi dengan cara lain seminggu dari sekarang.


Saya memiliki perasaan buruk tentang solusi yang mengharuskan Anda untuk bermain-main dengan tanggal sistem, tetapi Anda memang memberi saya ide, yang saya kembangkan sedikit dan, sayangnya, itu berhasil. Jadi terima kasih.
kch

-7

Saya tahu posting ini sudah tua tetapi halaman ini adalah yang pertama ketika Googling "menyisipkan komit git".

Mengapa membuat hal-hal sederhana menjadi rumit?

Anda memiliki ABC dan Anda ingin ABZC.

  1. git rebase -i trunk (atau apapun sebelum B)
  2. ubah pilih untuk diedit pada baris B.
  3. buat perubahan Anda: git add ..
  4. git commit( git commit --amendyang akan mengedit B dan tidak membuat Z)

[Anda dapat menghasilkan sebanyak yang git commitAnda inginkan di sini untuk memasukkan lebih banyak komitmen. Tentu saja, Anda mungkin memiliki masalah dengan langkah 5, tetapi menyelesaikan konflik penggabungan dengan git adalah keterampilan yang harus Anda miliki. Jika tidak, berlatih!]

  1. git rebase --continue

Sederhana bukan?

Jika Anda mengerti git rebase, menambahkan komit 'root' seharusnya tidak menjadi masalah.

Bersenang-senang dengan git!


5
Pertanyaannya meminta memasukkan komit pertama : dari ABC Anda ingin ZABC. Seorang yang lugas git rebasetidak dapat melakukan ini.
Petr Viktorin
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.