Inserisci un commit prima del commit di root in Git?

Ho chiesto prima su come schiacciare i primi due commit in un repository git.

Anche se le soluzioni sono piuttosto interessanti e non proprio mentali come alcune altre cose in git, sono ancora un po ‘il proverbiale sacco di ferite se hai bisogno di ripetere la procedura molte volte durante lo sviluppo del tuo progetto.

Quindi, preferirei attraversare il dolore una volta sola, e poi essere in grado di usare per sempre il rebase interattivo standard.

Quello che voglio fare, quindi, è avere un commit iniziale vuoto che esiste unicamente allo scopo di essere il primo. Nessun codice, niente di niente. Basta prendere spazio in modo che possa essere la base per rebase.

La mia domanda è quindi, avendo un repository esistente, come faccio ad inserire un nuovo commit vuoto prima del primo e spostando tutti gli altri in avanti?

Risposta a metà del 2017

Creare un nuovo commit completamente vuoto senza effetti collaterali è probabilmente il modo migliore di utilizzare direttamente l’impianto idraulico di Git. Facendolo in questo modo si evita qualsiasi effetto collaterale: non toccare la copia di lavoro o l’indice, nessun ramo temporaneo da ripulire, ecc. Quindi:

  1. Per creare un commit, abbiamo bisogno di un albero di directory per questo, quindi prima ne creiamo uno vuoto:

    tree=`git hash-object -wt tree --stdin < /dev/null` 
  2. Ora possiamo avvolgere un commit attorno ad esso:

     commit=`git commit-tree -m 'root commit' $tree` 
  3. E ora possiamo rebase su quello:

     git rebase --onto $commit --root master 

E questo è tutto. Puoi riorganizzare l'intera faccenda in un unico rivestimento se conosci abbastanza bene il tuo guscio.

(NB: in pratica, ora utilizzerei il filter-branch . Verrà modificato in seguito).


Risposta storica (a cui fanno riferimento altre risposte)

Ecco un'implementazione più pulita della stessa soluzione, in quanto funziona senza la necessità di creare un repository aggiuntivo, utilizzare i telecomandi e correggere una testina distaccata:

 # 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 

Voilà, sei finito su master con la sua storia riscritta per includere un commit radice vuoto.


NB: sulle vecchie versioni di Git che non hanno l' --orphan per il checkout , è necessario che l'impianto idraulico crei un ramo vuoto:

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

Unione delle risposte di Aristotele Pagaltzis e Uwe Kleine-König e commento di 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 

(solo per mettere tutto in un posto)

Mi piace la risposta di Aristotele. Ma ho scoperto che per un repository di grandi dimensioni (> 5000 commit) il ramo di filtro funziona meglio di rebase per diversi motivi 1) è più veloce 2) non richiede l’intervento umano in caso di conflitto di unione. 3) può riscrivere i tag – conservandoli. Nota che il ramo del filtro funziona perché non ci sono dubbi sul contenuto di ciascun commit: è esattamente lo stesso di prima di questo ‘rebase’.

I miei passi sono:

 # 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 /"' --tag-name-filter cat master 

Nota che le opzioni ‘–tag-name-filter cat’ significano che i tag verranno riscritti per puntare ai commit appena creati.

Ho usato con successo pezzi di Aristotele e la risposta di Kent:

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

Inoltre, riscriverà tutti i rami (non solo il master ) oltre ai tag.

git rebase --root --onto $emptyrootcommit

dovrebbe fare il trucco facilmente

Mi sono emozionato e ho scritto una versione “idempotente” di questo simpatico script … inserirà sempre lo stesso commit vuoto, e se lo esegui due volte, non cambia ogni volta gli hash di commit. Quindi, ecco la mia versione di 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 

Vale la complessità extra? forse no, ma userò questo.

DOVREBBE anche permettere di eseguire questa operazione su diverse copie clonate del repository, e finire con gli stessi risultati, quindi sono ancora compatibili … test … sì, funziona, ma serve anche per eliminare e aggiungere il tuo remote di nuovo, ad esempio:

 git remote rm origin git remote add --track master [email protected]:path/to/repo 

Bene, ecco cosa mi è venuto in mente:

 # 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 

Penso che usare git replace e git filter-branch sia una soluzione migliore rispetto all’utilizzo di una git rebase :

  • migliore preformance
  • più facile e meno rischioso (puoi verificare i risultati ad ogni passo e annullare ciò che hai fatto …)
  • funziona bene con più filiali con risultati garantiti

L’idea alla base è di:

  • Crea un nuovo commit vuoto nel passato
  • Sostituisci il vecchio commit di root con un commit esattamente simile, tranne che il nuovo commit di root viene aggiunto come genitore
  • Verifica che tutto sia come previsto ed esegui git filter-branch
  • Ancora una volta, verifica che tutto sia OK e pulisci i file git non più necessari

Ecco uno script per i primi due passaggi:

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

Potresti eseguire questo script senza rischi (anche se fare un backup prima di eseguire azioni che non hai mai fatto prima è una buona idea;)), e se il risultato non è quello atteso, elimina semplicemente i file creati nella cartella .git/refs/replace e riprova;)

Una volta verificato che lo stato del repository è quello che ti aspetti, esegui il seguente comando per aggiornare la cronologia di tutti i rami :

 git filter-branch -- --all 

Ora, devi vedere 2 storie, quella vecchia e quella nuova (vedi la guida su filter-branch per maggiori informazioni). È ansible confrontare il 2 e ricontrollare se tutto è OK. Se sei soddisfatto, elimina i file non più necessari:

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

Puoi tornare al tuo ramo master ed eliminare il ramo temporaneo:

 git checkout master git branch -D new-root 

Ora, tutto dovrebbe essere fatto;)

Ecco il mio script bash basato sulla risposta di Kent con miglioramenti:

  • controlla il ramo originale, non solo il master , una volta terminato;
  • Ho cercato di evitare il ramo temporaneo, ma git checkout --orphan funziona solo con un ramo, non con lo stato staccato, quindi è stato estratto abbastanza a lungo da rendere il nuovo root commit e quindi cancellato;
  • usa l’hash del nuovo commit di root durante il filter-branch (Kent ha lasciato un segnaposto lì per la sostituzione manuale);
  • l’operazione di filter-branch riscrive solo i rami locali, non i remoti
  • i metadati dell’autore e del committer sono standardizzati in modo che il commit di root sia identico tra i repository.

 #!/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='[email protected]' 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" 

Per cambiare il commit radice:

Innanzitutto, crea il commit che desideri come prima.

Secondo, cambia l’ordine dei commit usando:

git rebase -i –root

Un editor apparirà con i commit fino al commit di root, come:

scegli il 1234 vecchio messaggio radice

scegli 0294 Un commit nel mezzo

scegli il 5678 commit che vuoi mettere alla radice

È quindi ansible inserire prima il commit desiderato, posizionandolo nella prima riga. Nell’esempio:

scegli il 5678 commit che vuoi mettere alla radice

scegli il 1234 vecchio messaggio radice

scegli 0294 Un commit nel mezzo

Uscire dall’editor l’ordine di commit sarà cambiato.

PS: per cambiare l’editor utilizzato da git, esegui:

git config –global core.editor name_of_the_editor_program_you_want_to_use

Ecco un semplice one-liner che può essere usato per aggiungere un commit vuoto all’inizio di un repository, se hai dimenticato di creare un commit vuoto immediatamente dopo “git init”:

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

In seguito rispondo ad Aristotele e agli altri Pagaltzis usando però comandi più semplici

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

Nota che il tuo repository non dovrebbe contenere modifiche locali in attesa di essere eseguito.
Nota git checkout --orphan funzionerà con le nuove versioni di git, immagino.
Nota il più delle volte lo git status fornisce suggerimenti utili.

Avvia un nuovo repository.

Imposta la data indietro alla data di inizio desiderata.

Fai tutto nel modo in cui vorresti averlo fatto, regolando il tempo del sistema in modo da riflettere quando avresti desiderato che tu l’avessi fatto in quel modo. Estrarre i file dal repository esistente in base alle esigenze per evitare un sacco di digitazione inutile.

Quando arrivi oggi, scambia i repository e hai finito.

Se sei solo pazzo (stabilito) ma ragionevolmente intelligente (probabilmente, perché devi avere una certa quantità di intelligenza per inventare idee pazze come questa) dovrai scrivere la sceneggiatura.

Ciò renderà anche più bello quando decidi che vuoi che il passato sia accaduto in un altro modo da una settimana a partire da ora.

So che questo post è vecchio, ma questa pagina è la prima in cui googling “inserendo commit git”.

Perché rendere le cose semplici complicate?

Hai ABC e vuoi ABZC.

  1. git rebase -i trunk (o qualsiasi cosa prima di B)
  2. cambia il pick da modificare sulla riga B.
  3. apporta le tue modifiche: git add ..
  4. git commit ( git commit --amend che modificherà B e non creerà Z)

[Puoi fare il maggior numero di git commit che vuoi qui per inserire più commit. Naturalmente, potresti avere problemi con il passaggio 5, ma la risoluzione del conflitto di fusione con git è un’abilità che dovresti avere. Altrimenti, pratica!]

  1. git rebase --continue

Semplice, non è vero?

Se capisci git rebase , aggiungere un commit ‘root’ non dovrebbe essere un problema.

Divertiti con git!