web-dev-qa-db-fra.com

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

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 ne soient pas aussi déformantes que d’autres choses chez Git, elles restent un sacré casse-tête si vous devez répéter la procédure plusieurs fois tout au long du développement de votre projet.

Donc, je préférerais ne souffrir qu'une seule fois, puis pouvoir utiliser pour toujours le rebase interactif standard.

Ce que je veux faire, alors, est d’avoir un commit initial vide qui existe uniquement dans le but d’être le premier. Pas de code, pas de rien. Je prends juste de la place pour que ça puisse être la base de rebase.

Ma question est donc la suivante: avoir un référentiel existant, comment puis-je insérer un nouveau commit vide avant le premier et faire avancer tous les autres?

203
kch

Réponse mi-2017

Il est probablement préférable de créer un nouveau commit totalement vide, sans effets secondaires, en utilisant directement la tuyauterie de Git. En procédant ainsi, vous éviterez les effets secondaires: ne touchez pas la copie de travail ni l’index, ne supprimez aucune branche temporaire à nettoyer, etc. Ainsi:

  1. Pour créer un commit, nous avons besoin d'une arborescence de répertoires. Nous allons donc en créer une vide:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Nous pouvons maintenant envelopper un commit:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Et maintenant nous pouvons nous baser sur cela:

    git rebase --onto $commit --root master
    

Et c'est tout. Vous pouvez réorganiser tout cela en une ligne si vous connaissez suffisamment votre Shell.

(NB: en pratique, j’utiliserais maintenant filter-branch. Éditerons cela plus tard.)


Réponse historique (référencé par d'autres réponses)

Voici une implémentation plus propre de la même solution, dans la mesure où elle fonctionne sans la nécessité de créer un référentiel supplémentaire, de fusionner des télécommandes et de corriger une tête détachée:

# 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

Voila, vous avez fini sur master avec son historique réécrit pour inclure un commit racine vide.


NB: sur les anciennes versions de Git qui n'ont pas le --Orphan passez à checkout, vous avez besoin de la plomberie pour créer une branche vide:

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

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)

29
Antony Hatchkins

J'aime la réponse d'Aristote. Mais nous avons constaté que pour un référentiel volumineux (> 5 000 validations), la branche filtre fonctionne mieux que rebase pour plusieurs raisons: 1) elle est plus rapide 2) elle ne nécessite aucune intervention humaine en cas de conflit de fusion. 3) il peut réécrire les étiquettes - en les préservant. Notez que filter-branch fonctionne car il n’ya aucune question sur le contenu de chaque commit, c’est exactement la même chose qu’avant cette rebase.

Mes pas 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.

11
Kent

J'ai utilisé des morceaux de la réponse d'Aristote et de Kent avec succès:

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

5
ldav1s

Je pense que l'utilisation de git replace et git filter-branch est 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 loin dans le passé
  • Remplacez l'ancien commit par un commit identique, à la différence que le nouveau commit est ajouté en tant que parent.
  • Vérifiez que tout se passe comme prévu et lancez 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'aviez 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/replace et essayez à nouveau ;)

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 histoires, l’ancien et le nouveau (voir l’aide sur filter-branch pour 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 revenir à votre branche master et supprimer la branche temporaire:

git checkout master
git branch -D new-root

Maintenant, tout devrait être fait;)

3
Philippe

Je me suis enthousiasmé et j'ai écrit une version "idempotente" de ce script Nice ... il insérera toujours le même commit vierge, et si vous l'exécutez deux fois, cela ne changera 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 référentiel et d’obtenir les mêmes résultats, de sorte qu’ils soient toujours compatibles ... en cours de test ... oui, cela fonctionne, mais vous devez également supprimer et ajouter vos fichiers. télécommandes à nouveau, par exemple:

git remote rm Origin
git remote add --track master user@Host:path/to/repo
3
Sam Watkins

git rebase --root --onto $emptyrootcommit

devrait faire l'affaire facilement

3
Uwe Kleine-König

Voici un one-liner simple qui peut être utilisé pour ajouter une validation vide au début d'un référentiel, si vous avez oublié de créer une validation vide immédiatement après "git init":

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

Eh bien, voici ce que je suis venu avec:

# 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
kch

Voici mon script bash 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 --Orphan fonctionne uniquement avec une branche, et non avec un état en tête détachée; elle est donc extraite suffisamment longtemps pour que la nouvelle racine soit validée, puis supprimée.
  • il utilise le hash du nouveau commit root pendant la filter-branch _ (Kent a laissé un espace réservé pour le remplacement manuel);
  • le filter-branch l'opération ne réécrit que les branches locales et non les télécommandes
  • les métadonnées auteur et committer sont normalisées de sorte que le commit racine soit identique dans 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='[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"
1
luxagen

Pour changer le commit root:

Commencez par créer le commit que vous voulez.

Deuxièmement, changez l'ordre des commits en utilisant:

git rebase -i --root

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

Choisissez 1234 ancien message racine

pick 0294 Un commit au milie

choisissez 5678 que vous voulez mettre à la racine

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

choisissez 5678 que vous voulez mettre à la racine

Choisissez 1234 ancien message racine

pick 0294 Un commit au milie

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

PS: Pour changer l’éditeur utilisé par git, lancez:

git config --global core.editor name_of_the_editor_program_you_want_to_use

1
tigre200

Suite de la réponse 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 rapport ne doit contenir aucune modification locale en attente de validation.
Remarque git checkout --Orphan fonctionnera sur les nouvelles versions de git, je suppose.
Notez la plupart du temps git status donne des indications utiles.

0
ony