Insérer un commit avant le commit root dans Git?


231

J'ai déjà demandé comment écraser les deux premiers commits dans un référentiel git.

Bien que les solutions soient plutôt intéressantes et pas vraiment aussi déformantes que d'autres choses dans git, elles sont toujours un peu le sac proverbial de la douleur si vous devez répéter la procédure plusieurs fois pendant le développement de votre projet.

Donc, je préfère ne souffrir qu'une seule fois, puis être en mesure d'utiliser à jamais le rebase interactif standard.

Ce que je veux faire, alors, c'est d'avoir un commit initial vide qui existe uniquement dans le but d'être le premier. Pas de code, rien du tout. Il suffit de prendre de l'espace pour qu'il puisse être la base du rebase.

Ma question est alors, ayant un référentiel existant, comment puis-je insérer un nouveau commit vide avant le premier et déplacer tout le monde vers l'avant?


3
;) Je suppose que cela mérite quand même une réponse. J'explore en quelque sorte les nombreuses façons dont on peut devenir fou en modifiant l'histoire de manière obsessionnelle. Ne vous inquiétez pas, pas un référentiel partagé.
kch

11
D'un éditeur d'histoire obsessionnel et fou à un autre, merci d'avoir posté la question! ; D
Marco

10
Dans la défense de @ kch, une raison parfaitement légitime est celle dans laquelle je me trouve: ajouter un instantané d'une version historique qui n'a jamais été capturée dans le dépôt.
Old McStopher

4
J'ai une autre raison légitime! Ajouter un commit vide avant le premier afin de pouvoir rebaser au premier commit et supprimer le ballonnement binaire ajouté dans le commit initial d'un dépôt (:
pospi

Réponses:


315

Il y a 2 étapes pour y parvenir:

  1. Créer un nouveau commit vide
  2. Réécrire l'historique pour commencer à partir de ce commit vide

Nous allons mettre le nouveau commit vide sur une branche temporaire newrootpour plus de commodité.

1. Créez un nouveau commit vide

Il existe plusieurs façons de procéder.

Utiliser uniquement de la plomberie

L'approche la plus propre consiste à utiliser la plomberie de Git pour simplement créer un commit directement, ce qui évite de toucher la copie de travail ou l'index ou la branche qui est extraite, etc.

  1. Créez un objet arborescent pour un répertoire vide:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Enveloppez un commit autour de lui:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Créez-y une référence:

    git branch newroot $commit
    

Vous pouvez bien sûr réorganiser toute la procédure en une seule ligne si vous connaissez bien votre coque.

Sans plomberie

Avec les commandes porcelaine classiques, vous ne pouvez pas créer un commit vide sans vérifier la newrootbranche et mettre à jour l'index et la copie de travail à plusieurs reprises, sans raison valable. Mais certains peuvent trouver cela plus facile à comprendre:

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

Notez que sur les très anciennes versions de Git qui n'ont pas le --orphancommutateur checkout, vous devez remplacer la première ligne par ceci:

git symbolic-ref HEAD refs/heads/newroot

2. Réécrire l'historique pour commencer à partir de ce commit vide

Vous avez deux options ici: le rebasage ou une réécriture de l'historique propre.

Rebasing

git rebase --onto newroot --root master

Cela a la vertu de la simplicité. Cependant, il mettra également à jour le nom et la date du committer à chaque dernier commit sur la branche.

En outre, avec certains historiques de cas de bord, il peut même échouer en raison de conflits de fusion - malgré le fait que vous rebasiez sur une validation qui ne contient rien.

Réécriture de l'histoire

L'approche la plus propre consiste à réécrire la branche. Contrairement à with git rebase, vous devrez rechercher à partir de quel commit votre branche commence:

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

La réécriture se produit dans la deuxième étape, évidemment; c'est la première étape qui a besoin d'explications. Qu'est git replace- ce que c'est, il dit à Git que chaque fois qu'il voit une référence à un objet que vous souhaitez remplacer, Git devrait plutôt regarder le remplacement de cet objet.

Avec le --graftcommutateur, vous lui dites quelque chose de légèrement différent de la normale. Vous dites que vous n'avez pas encore d'objet de remplacement, mais vous souhaitez remplacer l' <currentroot>objet de validation par une copie exacte de lui-même, à l' exception que les validations parentes du remplacement doivent être celles que vous avez répertoriées (c'est-à-dire la newrootvalidation ). Puis git replaceva de l'avant et crée ce commit pour vous, puis déclare ce commit comme remplacement de votre commit d'origine.

Maintenant, si vous faites un git log, vous verrez que les choses ressemblent déjà à ce que vous voulez: la branche démarre newroot.

Cependant, notez que git replace cela ne modifie pas réellement l'historique - ni ne se propage hors de votre référentiel. Il ajoute simplement une redirection locale vers votre référentiel d'un objet à un autre. Cela signifie que personne d'autre ne voit l'effet de ce remplacement - seulement vous.

C'est pourquoi l' filter-branchétape est nécessaire. Avec git replacevous créez une copie exacte avec des validations parent ajustées pour la validation racine; git filter-branchrépète ensuite ce processus pour toutes les validations suivantes également. C'est là que l'histoire est réellement réécrite pour que vous puissiez la partager.


1
Cette --onto newrootoption est redondante; vous pouvez vous en passer car l'argument que vous lui transmettez newrootest le même que l'argument en amont - newroot.
wilhelmtell

7
Pourquoi ne pas utiliser des commandes de plomberie en porcelaine?. Je remplacerais git symbolic-ref HEAD refs / heads / newroot par git checkout --orphan newroot
albfan

4
@nenopera: parce que cette réponse a été écrite avant git-checkoutce changement. Je l'ai mis à jour pour mentionner cette approche en premier, merci pour le pointeur.
Aristote Pagaltzis le

1
Si votre racine nouvelle n'est pas vide, utilisez git rebase --merge -s recursive -X theirs --onto newroot --root masterpour résoudre automatiquement tous les conflits (voir cette réponse). @AlexanderKuzin
utilisateur

1
@Geremia Vous ne pouvez modifier que le dernier commit, donc si votre référentiel ne contient que le commit racine, cela peut fonctionner, sinon vous devrez de toute façon rebaser tous les autres commits dans le référentiel au-dessus du commit racine modifié. Mais même dans ce cas, la rubrique implique que vous ne souhaitez pas modifier la validation racine, mais que vous souhaitez en insérer une autre avant la racine existante.
utilisateur

30

Fusion des réponses d'Aristote Pagaltzis et Uwe Kleine-König et du commentaire de 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

(juste pour tout mettre au même endroit)


C'est excellent. Ce serait bien si cela pouvait être ce qu'un git rebase -i --root faisait en interne.
aredridel

Oui, j'ai été surpris de découvrir que ce n'est pas le cas.
Antony Hatchkins

J'ai dû changer la commande rebase en git rebase newroot masterraison d'une erreur.
marbel82

@ antony-hatchkins merci pour cela. J'ai un dépôt git existant et (pour diverses raisons que je n'entrerai pas ici), j'essaye d'ajouter un commit git NON-VIDE comme mon premier commit. J'ai donc remplacé git commit --allow-empty -m 'initial' par git add.; git commit -m "initial laravel commit"; git push; Et puis cette étape de rebase: git rebase --onto newroot --root master échoue avec une tonne de conflits de fusion. Aucun conseil? : ((
kp123

@ kp123 essayez un commit vide :)
Antony Hatchkins

12

J'aime la réponse d'Aristote. Mais j'ai constaté que pour un grand référentiel (> 5000 validations), la branche de filtre fonctionne mieux que rebaser pour plusieurs raisons 1) elle est plus rapide 2) elle ne nécessite pas d'intervention humaine en cas de conflit de fusion. 3) il peut réécrire les balises - en les préservant. Notez que filter-branch fonctionne car il n'y a pas de question sur le contenu de chaque commit - c'est exactement la même chose qu'avant ce 'rebase'.

Mes étapes sont:

# 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

Notez que les options '--tag-name-filter cat' signifient que les balises seront réécrites pour pointer vers les commits nouvellement créés.


Cela n'aide pas à créer des validations non vides, ce qui est également un cas d'utilisation intéressant.
ceztko

En comparaison avec d'autres solutions, la vôtre n'a qu'un effet secondaire insignifiant: elle change de hachage, mais toute l'histoire reste intacte. Je vous remercie!
Vladyslav Savchenko

5

J'ai utilisé avec succès des morceaux de la réponse d'Aristote et de 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

Cela réécrira également toutes les branches (pas seulement master) en plus des balises.


que fait cette dernière ligne?
Diederick C. Niehorster

Il recherche refs/original/et supprime chaque référence. Les références qu'il supprime devraient déjà être référencées par une autre branche, afin qu'elles ne disparaissent pas vraiment, soient simplement refs/original/supprimées.
ldav1s

Cela a fonctionné pour moi. De plus, je timedatectl set-time '2017-01-01 00:00:00'donnais newrootun ancien horodatage.
chrm

4

git rebase --root --onto $emptyrootcommit

devrait faire l'affaire facilement


3
$emptyrootcommitest une variable shell qui se développe à rien, sûrement?
Flimm

@Flimm: $ emptyrootcommit est le sha1 d'un commit vide que l'affiche originale semble déjà avoir.
Uwe Kleine-König

4

Je pense qu'utiliser git replaceet git filter-branchest une meilleure solution que d'utiliser un git rebase:

  • meilleure performance
  • plus facile et moins risqué (vous pouvez vérifier votre résultat à chaque étape et annuler ce que vous avez fait ...)
  • fonctionne bien avec plusieurs branches avec des résultats garantis

L'idée derrière cela est de:

  • Créer un nouveau commit vide dans le passé
  • Remplacer l'ancien commit racine par un commit exactement similaire sauf que le nouveau commit racine est ajouté en tant que parent
  • Vérifiez que tout est comme prévu et exécutez git filter-branch
  • Encore une fois, vérifiez que tout va bien et nettoyez les fichiers git qui ne sont plus nécessaires

Voici un script pour les 2 premières étapes:

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

Vous pouvez exécuter ce script sans risque (même si faire une sauvegarde avant de faire une action que vous n'avez jamais faite auparavant est une bonne idée;)), et si le résultat n'est pas celui attendu, supprimez simplement les fichiers créés dans le dossier .git/refs/replaceet réessayez; )

Une fois que vous avez vérifié que l'état du référentiel correspond à vos attentes, exécutez la commande suivante pour mettre à jour l'historique de toutes les branches :

git filter-branch -- --all

Maintenant, vous devez voir 2 historiques, l'ancien et le nouveau (voir l'aide sur filter-branchpour plus d'informations). Vous pouvez comparer les 2 et vérifier à nouveau si tout va bien. Si vous êtes satisfait, supprimez les fichiers dont vous n'avez plus besoin:

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

Vous pouvez retourner dans votre masterbranche et supprimer la branche temporaire:

git checkout master
git branch -D new-root

Maintenant, tout doit être fait;)


3

Je me suis excité et j'ai écrit une version «idempotente» de ce joli script ... il insérera toujours le même commit vide, et si vous l'exécutez deux fois, cela ne change pas vos hachages de commit à chaque fois. Alors, voici mon point de vue sur 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

Vaut-il la complexité supplémentaire? peut-être pas, mais je vais utiliser celui-ci.

Cela DEVRAIT également permettre d'effectuer cette opération sur plusieurs copies clonées du dépôt, et se retrouver avec les mêmes résultats, donc ils sont toujours compatibles ... test ... oui, ça marche, mais il faut aussi supprimer et ajouter votre télécommandes à nouveau, par exemple:

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

3

Pour ajouter un commit vide au début d'un référentiel, si vous avez oublié de créer un commit vide immédiatement après "git init":

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

1
4b825dc ... est le hachage de l'arbre vide: stackoverflow.com/questions/9765453/…
mrks

2

Eh bien, voici ce que j'ai trouvé:

# 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

Voici mon bashscript basé sur la réponse de Kent avec des améliorations:

  • il vérifie la branche d'origine, pas seulement master, une fois terminé;
  • J'ai essayé d'éviter la branche temporaire, mais git checkout --orphanne fonctionne qu'avec une branche, pas avec un état de tête détachée, elle est donc extraite suffisamment longtemps pour valider la nouvelle racine, puis supprimée;
  • il utilise le hachage du nouveau commit racine pendant le filter-branch(Kent a laissé un espace réservé là pour le remplacement manuel);
  • l' filter-branchopération ne réécrit que les succursales locales, pas trop les télécommandes
  • les métadonnées d'auteur et de committer sont normalisées de sorte que la validation racine est identique entre les référentiels.

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

Pour changer le commit racine:

Créez d'abord le commit que vous souhaitez comme premier.

Ensuite, changez l'ordre des validations en utilisant:

git rebase -i --root

Un éditeur apparaîtra avec les validations jusqu'à la validation racine, comme:

choisir 1234 ancien message racine

pick 0294 Un commit au milieu

choisissez 5678 commit que vous voulez mettre à la racine

Vous pouvez ensuite mettre le commit que vous voulez en premier, en le plaçant dans la première ligne. Dans l'exemple:

choisissez 5678 commit que vous voulez mettre à la racine

choisir 1234 ancien message racine

pick 0294 Un commit au milieu

Quittez l'éditeur, l'ordre de validation aura changé.

PS: Pour modifier l'éditeur git utilise, exécutez:

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
Maintenant que rebase a --root, c'est de loin la solution la plus intéressante.
Ross Burton

1

Combinant le dernier et le plus grand. Pas d'effets secondaires, pas de conflits, garder les tags.

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

Notez que sur GitHub, vous perdrez les données d'exécution de CI et PR pourrait être gâché à moins que d'autres branches ne soient également corrigées.


0

Réponse suivante Aristote Pagaltzis et autres mais en utilisant des commandes plus simples

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

Notez que votre dépôt ne doit contenir aucune modification locale en attente de validation.
Remarque git checkout --orphanfonctionnera dans les nouvelles versions de git, je suppose.
Notez que la plupart du temps git statusdonne des conseils utiles.


-6

Démarrez un nouveau référentiel.

Réglez votre date à la date de début que vous souhaitez.

Faites tout comme vous le souhaiteriez, en ajustant l'heure du système pour refléter le moment où vous auriez souhaité que vous le fassiez de cette façon. Tirez les fichiers du référentiel existant selon vos besoins pour éviter de trop taper inutilement.

Lorsque vous arrivez à aujourd'hui, échangez les référentiels et vous avez terminé.

Si vous êtes juste fou (établi) mais raisonnablement intelligent (probablement, parce que vous devez avoir une certaine quantité d'intelligence pour imaginer des idées folles comme celle-ci), vous scripterez le processus.

Cela sera également plus agréable lorsque vous déciderez que le passé s'est produit d'une autre manière dans une semaine.


J'ai de mauvais sentiments à propos d'une solution qui vous oblige à jouer avec la date du système, mais vous m'avez donné une idée, que j'ai développée un peu et, hélas, cela a fonctionné. Donc merci.
kch

-7

Je sais que ce message est ancien, mais cette page est la première lorsque Google recherche "insérer un git de validation".

Pourquoi compliquer les choses simples?

Vous avez ABC et vous voulez ABZC.

  1. git rebase -i trunk (ou n'importe quoi avant B)
  2. changer de choix pour éditer sur la ligne B
  3. apportez vos modifications: git add ..
  4. git commit( git commit --amendqui modifiera B et ne créera pas Z)

[Vous pouvez en faire autant git commitque vous le souhaitez ici pour insérer plus de commits. Bien sûr, vous pouvez avoir des problèmes avec l'étape 5, mais résoudre les conflits de fusion avec git est une compétence que vous devriez avoir. Sinon, entraînez-vous!]

  1. git rebase --continue

C'est simple, non?

Si vous comprenez git rebase, l'ajout d'un commit 'root' ne devrait pas être un problème.

Amusez-vous avec git!


5
La question demande d'insérer un premier commit: depuis ABC vous voulez ZABC. Un simple git rebasene peut pas faire ça.
Petr Viktorin
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.