Inserire un commit prima del commit root in Git?


231

Ho chiesto prima di come eliminare i primi due commit in un repository git.

Mentre le soluzioni sono piuttosto interessanti e non sono così strabilianti come alcune altre cose in git, sono ancora un po 'la proverbiale borsa del male se devi ripetere la procedura più volte lungo lo sviluppo del tuo progetto.

Quindi, preferirei provare dolore solo una volta, e quindi essere in grado di usare per sempre il rebase interattivo standard.

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

La mia domanda quindi è, avendo un repository esistente, come posso fare per inserire un nuovo commit vuoto prima del primo e spostare tutti gli altri in avanti?


3
;) Immagino che meriti comunque una risposta. Sto esplorando i molti modi in cui uno può impazzire modificando ossessivamente la storia. Non ti preoccupare, non un repository condiviso.
kch

11
Da un editor di storia ossessivo e folle a un altro, grazie per aver pubblicato la domanda! ; D
Marco

10
A difesa di @ kch, una ragione perfettamente legittima è quella in cui mi trovo: aggiungere un'istantanea di una versione storica che non è mai stata catturata nel repository.
Old McStopher

4
Ho un'altra ragione legittima! Aggiunta di un commit vuoto prima del primo per poter rifare al primo commit e rimuovere il bloat binario aggiunto nel commit iniziale di un repository (:
pospi

Risposte:


314

Ci sono 2 passaggi per raggiungere questo obiettivo:

  1. Crea un nuovo commit vuoto
  2. Riscrivi la cronologia per iniziare da questo commit vuoto

Metteremo il nuovo commit vuoto su un ramo temporaneo newrootper comodità.

1. Creare un nuovo commit vuoto

Esistono diversi modi per farlo.

Usando solo l'impianto idraulico

L'approccio più pulito è utilizzare l'idraulico di Git per creare direttamente un commit, che evita di toccare la copia di lavoro o l'indice o quale ramo è stato estratto, ecc.

  1. Creare un oggetto ad albero per una directory vuota:

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

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Crea un riferimento ad esso:

    git branch newroot $commit
    

Ovviamente puoi riorganizzare l'intera procedura in una sola riga se conosci bene il tuo guscio.

Senza impianto idraulico

Con i normali comandi di porcellana, non è possibile creare un commit vuoto senza estrarre il newrootramo e aggiornare ripetutamente l'indice e la copia di lavoro, senza una buona ragione. Ma alcuni potrebbero trovare questo più facile da capire:

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

Nota che nelle versioni molto vecchie di Git che non hanno il --orphanpassaggio a checkout, devi sostituire la prima riga con questa:

git symbolic-ref HEAD refs/heads/newroot

2. Riscrivi la cronologia per iniziare da questo commit vuoto

Hai due opzioni qui: rebasing o una riscrittura della cronologia pulita.

ribasamento

git rebase --onto newroot --root master

Questo ha la virtù della semplicità. Tuttavia, aggiornerà anche il nome e la data del committer su ogni ultimo commit sul ramo.

Inoltre, con alcune case history limite, potrebbe persino fallire a causa della fusione di conflitti, nonostante il fatto che si stia rifacendo su un commit che non contiene nulla.

Riscrittura della storia

L'approccio più pulito è quello di riscrivere il ramo. A differenza di git rebase, dovrai cercare da quale commit inizia la tua filiale:

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

La riscrittura avviene nel secondo passaggio, ovviamente; è il primo passo che necessita di spiegazioni. Quello che git replacefa è dire a Git che ogni volta che vede un riferimento a un oggetto che si desidera sostituire, Git dovrebbe invece guardare alla sostituzione di quell'oggetto.

Con l' --graftinterruttore, stai dicendo qualcosa di leggermente diverso dal normale. Stai dicendo che non hai ancora un oggetto sostitutivo, ma vuoi sostituire l' <currentroot>oggetto di commit con una copia esatta di se stesso, tranne per il fatto che i commit principali della sostituzione dovrebbero essere quelli che hai elencato (cioè il newrootcommit ). Quindi git replaceprocede e crea questo commit per te, quindi dichiara quel commit come sostituto del commit originale.

Ora se fai un git log, vedrai che le cose sembrano già come le vuoi tu: il ramo inizia da newroot.

Tuttavia, tieni presente che in git replace realtà non modifica la cronologia , né si propaga dal tuo repository. Aggiunge semplicemente un reindirizzamento locale al repository da un oggetto a un altro. Ciò significa che nessun altro vede l'effetto di questa sostituzione - solo tu.

Ecco perché il filter-branchpassaggio è necessario. Con git replacete crei una copia esatta con i commit parent corretti per il commit root; git filter-branchquindi ripete questo processo anche per tutti i seguenti commit. È qui che la storia viene effettivamente riscritta in modo da poterla condividere.


1
Tale --onto newrootopzione è ridondante; puoi farne a meno perché l'argomento che lo passi newroot, è lo stesso dell'argomento upstream - newroot.
Wilhelmtell,

7
Perché non usare la porcellana invece dei comandi idraulici ?. Sostituirei git symbolic-ref HEAD refs / heads / newroot con git checkout --orphan newroot
albfan

4
@nenopera: perché questa risposta è stata scritta prima ha git-checkoutavuto quel passaggio. L'ho aggiornato per menzionare prima quell'approccio, grazie per il puntatore.
Aristotele Pagaltzis,

1
Se il tuo newroot non è vuoto, usa git rebase --merge -s recursive -X theirs --onto newroot --root masterper risolvere automaticamente tutti i conflitti (vedi questa risposta). @AlexanderKuzin
utente

1
@Geremia Puoi modificare solo l'ultimo commit, quindi se il tuo repository contiene solo il commit root, potrebbe funzionare, altrimenti dovrai rifare tutti gli altri commit nel repository in cima al commit root modificato comunque. Ma anche in questo caso, l'argomento implica che non si desidera modificare il commit della radice, ma si desidera invece inserirne un altro prima della radice esistente.
utente

30

Unisci le risposte di Aristotele Pagaltzis e Uwe Kleine-König e il 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 unico posto)


Questo è eccellente Sarebbe bello se questo potesse essere ciò che un git rebase -i --root ha fatto internamente.
Aredridel,

Sì, sono stato sorpreso di scoprire che non lo è.
Antony Hatchkins,

Ho dovuto cambiare il comando rebase in git rebase newroot master, a causa di un errore.
marbel82,

@ antony-hatchkins grazie per questo. Ho un repository git esistente e (per vari motivi in ​​cui non entrerò qui) sto cercando di aggiungere un commit git NON EMPTY come primo commit. Quindi ho sostituito git commit --allow-empty -m 'iniziale' con git add.; git commit -m "iniziale laravel commit"; git push; E quindi questo passaggio di rebase: git rebase --onto newroot --root master non riesce con una tonnellata di conflitti di unione. Qualche consiglio? : ((
kp123

@ kp123 prova un commit vuoto :)
Antony Hatchkins il

12

Mi piace la risposta di Aristotele. Ma ho scoperto che per un grande repository (> 5000 commit) il filtro-ramo funziona meglio del rebase per diversi motivi 1) è più veloce 2) non richiede l'intervento umano quando c'è un conflitto di unione. 3) può riscrivere i tag conservandoli. Nota che il filtro-ramo 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 <sha of newroot>/"' --tag-name-filter cat master

Notare che le opzioni '--tag-name-filter cat' indicano che i tag verranno riscritti per puntare ai commit appena creati.


Questo non aiuta a creare commit non vuoti che è anche un caso d'uso interessante.
ceztko,

Rispetto ad altre soluzioni, la tua ha un solo effetto collaterale insignificante: cambia gli hash, ma l'intera storia rimane intatta. Grazie!
Vladyslav Savchenko,

5

Ho usato con successo pezzi della risposta di Aristotele e 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 <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

Ciò riscriverà anche tutti i rami (non solo master) oltre ai tag.


cosa fa quest'ultima riga?
Diederick C. Niehorster,

Cerca refs/original/ed elimina ogni riferimento. I riferimenti che elimina dovrebbero già essere referenziati da qualche altro ramo, quindi non vanno davvero via, ma refs/original/vengono rimossi.
ldav1,

Questo ha funzionato per me. Inoltre, timedatectl set-time '2017-01-01 00:00:00'davo newrootun vecchio timestamp.
chrm

4

git rebase --root --onto $emptyrootcommit

dovrebbe fare il trucco facilmente


3
$emptyrootcommitè una variabile di shell che si espande nel nulla, sicuramente?
Flimm,

@Flimm: $ emptyrootcommit è lo sha1 di un commit vuoto che il poster originale sembra già avere.
Uwe Kleine-König,

4

Penso che usare git replaceed git filter-branchsia una soluzione migliore rispetto all'utilizzo di un git rebase:

  • prestazioni migliori
  • più facile e meno rischioso (potresti verificare il tuo risultato ad ogni passaggio e annullare ciò che hai fatto ...)
  • funziona bene con più filiali con risultati garantiti

L'idea alla base è di:

  • Crea un nuovo commit vuoto lontano nel passato
  • Sostituisci il vecchio commit root con un commit esattamente simile tranne per il fatto che il nuovo commit 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 2 primi passi:

#!/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 fare azioni che non hai mai fatto prima è una buona idea;)), e se il risultato non è quello previsto, elimina semplicemente i file creati nella cartella .git/refs/replacee riprova; )

Dopo aver verificato che lo stato del repository è quello previsto, eseguire il comando seguente per aggiornare la cronologia di tutti i rami :

git filter-branch -- --all

Ora, devi vedere 2 storie, quella vecchia e quella nuova (vedi aiuto su filter-branchper maggiori informazioni). È possibile confrontare i 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 masterramo ed eliminare il ramo temporaneo:

git checkout master
git branch -D new-root

Ora, tutto dovrebbe essere fatto;)


3

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 cambierà gli hash di commit ogni volta. Quindi, ecco la mia opinione su 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 pena la complessità in più? forse no, ma userò questo.

Questo DOVREBBE anche consentire di eseguire questa operazione su diverse copie clonate del repository e finire con gli stessi risultati, quindi sono ancora compatibili ... test ... sì, funziona, ma è necessario anche eliminare e aggiungere il tuo telecomandi di nuovo, ad es .:

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

3

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)

1
4b825dc ... è l'hash del albero vuoto: stackoverflow.com/questions/9765453/...
Mrks

2

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

2

Ecco la mia bashsceneggiatura basata sulla risposta di Kent con miglioramenti:

  • controlla il ramo originale, non solo master, quando fatto;
  • Ho cercato di evitare il ramo temporaneo, ma git checkout --orphanfunziona solo con un ramo, non con uno stato a testa staccata, quindi è stato estratto abbastanza a lungo da rendere il nuovo root commesso e quindi eliminato;
  • utilizza l'hash del nuovo commit root durante il filter-branch (Kent ha lasciato un segnaposto per la sostituzione manuale);
  • il filter-branch operazione riscrive solo le filiali locali, non anche i telecomandi
  • i metadati dell'autore e del committer sono standardizzati in modo che il commit della radice 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='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

Per cambiare il commit root:

Innanzitutto, crea il commit che desideri come primo.

In secondo luogo, cambia l'ordine dei commit usando:

git rebase -i --root

Verrà visualizzato un editor con i commit fino al commit di root, come:

scegli 1234 vecchio messaggio di root

scegli 0294 Un commit nel mezzo

scegli 5678 commit che vuoi mettere alla radice

È quindi possibile inserire prima il commit desiderato, posizionandolo nella prima riga. Nell'esempio:

scegli 5678 commit che vuoi mettere alla radice

scegli 1234 vecchio messaggio di root

scegli 0294 Un commit nel mezzo

Esci dall'editor l'ordine di commit sarà cambiato.

PS: per modificare l'editor utilizzato da git, esegui:

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
Ora che rebase ha --root, questa è di gran lunga la soluzione più accurata.
Ross Burton,

1

Combinando l'ultimo e il più grande. Nessun effetto collaterale, nessun conflitto, mantenimento dei 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

Si noti che su GitHub si perderanno i dati di esecuzione degli elementi della configurazione e le PR potrebbero incasinarsi a meno che non vengano riparati anche altri rami.


0

Seguendo la risposta Aristotele Pagaltzis e altri ma usando 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 memorizzate.
Nota git checkout --orphanfunzionerà con le nuove versioni di git, immagino.
Nota la maggior parte delle volte git statusfornisce suggerimenti utili.


-6

Avvia un nuovo repository.

Riporta la data alla data di inizio desiderata.

Fai tutto come desideri che avessi fatto, regolando l'ora del sistema in modo da riflettere quando avresti voluto farlo in quel modo. Estrarre i file dal repository esistente in base alle esigenze per evitare molte digitazioni inutili.

Quando arrivi a oggi, scambia i repository e il gioco è fatto.

Se sei solo pazzo (stabilito) ma ragionevolmente intelligente (probabilmente, perché devi avere una certa quantità di intelligenza per pensare a idee folli come questa) dovrai scrivere il processo.

Ciò renderà anche più bello quando decidi di volere che il passato sia accaduto in un altro modo tra una settimana.


Ho dei brutti sentimenti riguardo a una soluzione che richiede di perdere tempo con la data del sistema, ma mi hai dato un'idea, che ho sviluppato un po 'e, ahimè, ha funzionato. Quindi grazie.
kch

-7

So che questo post è vecchio ma questa pagina è la prima in cui Google "inserisce commit git".

Perché complicare le cose semplici?

Hai ABC e vuoi ABZC.

  1. git rebase -i trunk (o qualsiasi altra cosa prima di B)
  2. cambia scegli per modificare sulla linea B.
  3. apporta le tue modifiche: git add ..
  4. git commit( git commit --amendche modificherà B e non creerà Z)

[Puoi fare quanti git commitne vuoi qui per inserire più commit. Certo, potresti avere problemi con il passaggio 5, ma risolvere i conflitti di fusione con git è un'abilità che dovresti avere. In caso contrario, esercitati!]

  1. git rebase --continue

Semplice, no?

Se hai capito git rebase, aggiungere un commit 'root' non dovrebbe essere un problema.

Divertiti con Git!


5
La domanda richiede l'inserimento di un primo commit: da ABC vuoi ZABC. Un semplice git rebasenon può farlo.
Petr Viktorin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.