¿Insertar un commit antes del root commit en Git?

231

He preguntado antes sobre cómo aplastar los dos primeros commits en un repositorio git.

Si bien las soluciones son bastante interesantes y no tan alucinantes como otras cosas en git, siguen siendo un poco proverbiales si necesitas repetir el procedimiento muchas veces durante el desarrollo de tu proyecto.

Por lo tanto, prefiero pasar por el dolor solo una vez, y luego poder usar para siempre el rebase interactivo estándar.

Lo que quiero hacer, entonces, es tener un commit inicial vacío que exista únicamente con el propósito de ser el primero. Sin código, sin nada. Solo ocupando espacio para que pueda ser la base para rebase.

Mi pregunta es, teniendo un repositorio existente, ¿cómo hago para insertar una nueva confirmación vacía antes de la primera y hacer avanzar a todos los demás?

kch
fuente
3
;) Supongo que garantiza una respuesta de todos modos. Estoy explorando las muchas formas en que uno puede volverse loco editando obsesivamente la historia. No te preocupes, no es un repositorio compartido.
kch
11
De un editor de historia obsesivo y loco a otro, ¡gracias por publicar la pregunta! ; D
Marco
10
En defensa de @ kch, una razón perfectamente legítima es aquella en la que me encuentro: agregando una instantánea de una versión histórica que nunca fue capturada en el repositorio.
Old McStopher
44
Tengo otra razón legítima! Agregar una confirmación vacía antes de la primera para poder volver a crear la primera confirmación y eliminar la hinchazón binaria agregada en la confirmación inicial de un repositorio (:
pospi

Respuestas:

314

Hay 2 pasos para lograr esto:

  1. Crear una nueva confirmación vacía
  2. Reescribe el historial para comenzar desde esta confirmación vacía

Colocaremos el nuevo commit vacío en una rama temporal newrootpor conveniencia.

1. Crear una nueva confirmación vacía

Hay varias formas de hacer esto.

Usando solo plomería

El enfoque más limpio es usar la tubería de Git para crear una confirmación directamente, lo que evita tocar la copia de trabajo o el índice o qué rama está desprotegida, etc.

  1. Cree un objeto de árbol para un directorio vacío:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Envuelva un compromiso a su alrededor:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Crea una referencia a ella:

    git branch newroot $commit
    

Por supuesto, puede reorganizar todo el procedimiento en una sola línea si conoce bien su caparazón.

Sin fontanería

Con los comandos de porcelana regulares, no puede crear una confirmación vacía sin verificar la newrootrama y actualizar el índice y la copia de trabajo repetidamente, sin una buena razón. Pero algunos pueden encontrar esto más fácil de entender:

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

Tenga en cuenta que en versiones muy antiguas de Git que carecen del --orphancambio checkout, debe reemplazar la primera línea con esto:

git symbolic-ref HEAD refs/heads/newroot

2. Reescribe el historial para comenzar desde esta confirmación vacía

Tiene dos opciones aquí: rebase o una reescritura de historial limpia.

Rebasando

git rebase --onto newroot --root master

Esto tiene la virtud de la simplicidad. Sin embargo, también actualizará el nombre y la fecha del committer en cada último commit en la rama.

Además, con algunos historiales de casos extremos, incluso puede fallar debido a conflictos de fusión, a pesar del hecho de que está volviendo a crear una confirmación que no contiene nada.

Reescribir la historia

El enfoque más limpio es reescribir la rama. A diferencia de git rebase, deberá buscar qué confirmación inicia su rama:

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

La reescritura ocurre en el segundo paso, obviamente; Es el primer paso que necesita explicación. Lo que git replacehace es decirle a Git que cada vez que vea una referencia a un objeto que desea reemplazar, Git debería buscar el reemplazo de ese objeto.

Con el --graftinterruptor, le estás diciendo algo ligeramente diferente de lo normal. Está diciendo que todavía no tiene un objeto de reemplazo, pero desea reemplazar el <currentroot>objeto de confirmación con una copia exacta de sí mismo, excepto que la confirmación principal (s) de la sustitución debe ser la que usted enumeró (es decir, la newrootconfirmación ) Luego git replacecontinúa y crea este commit para usted, y luego declara ese commit como el reemplazo de su commit original.

Ahora, si haces un git log, verás que las cosas ya se ven como quieres: la rama comienza newroot.

Sin embargo, tenga en cuenta que en git replace realidad no modifica el historial , ni se propaga fuera de su repositorio. Simplemente agrega una redirección local a su repositorio de un objeto a otro. Lo que esto significa es que nadie más ve el efecto de este reemplazo, solo usted.

Por eso filter-branches necesario el paso. Con git replaceusted cree una copia exacta con confirmaciones primarias ajustadas para la confirmación raíz; git filter-branchluego repite este proceso para todas las siguientes confirmaciones también. Ahí es donde la historia se reescribe para que puedas compartirla.

Aristóteles Pagaltzis
fuente
1
Esa --onto newrootopción es redundante; puede prescindir de él porque el argumento que lo pasa newrootes el mismo que el argumento anterior newroot.
wilhelmtell
77
¿Por qué no usar porcelana en lugar de comandos de plomería? Reemplazaría git symbolic-ref HEAD refs / heads / newroot con git checkout --orphan newroot
albfan
44
@nenopera: porque esta respuesta fue escrita antes git-checkouttenía ese cambio. Lo actualicé para mencionar ese enfoque primero, gracias por el puntero.
Aristóteles Pagaltzis
1
Si su raíz nueva no está vacía, use git rebase --merge -s recursive -X theirs --onto newroot --root masterpara resolver todos los conflictos automáticamente (vea esta respuesta). @AlexanderKuzin
usuario
1
@Geremia Puede modificar solo la última confirmación, por lo que si su repositorio solo contiene la confirmación raíz, puede funcionar, de lo contrario tendrá que volver a redactar todas las demás confirmaciones en el repositorio encima de la confirmación raíz modificada de todos modos. Pero incluso entonces, el tema implica que no desea cambiar la confirmación de la raíz, sino que desea insertar otra antes de la raíz existente.
usuario
30

Fusión de las respuestas de Aristóteles Pagaltzis y Uwe Kleine-König y el comentario 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

(solo para poner todo en un solo lugar)

Antony Hatchkins
fuente
Esto es excelente. Sería bueno si esto pudiera ser lo que git rebase -i --root hizo internamente.
aredridel
Sí, me sorprendió descubrir que no es así.
Antony Hatchkins
Tuve que cambiar el comando rebase a git rebase newroot master, por error.
marbel82
@ antony-hatchkins gracias por esto. Tengo un repositorio git existente y (por varias razones que no mencionaré aquí) estoy tratando de agregar un compromiso git NO VACÍO como mi primer compromiso. Así que reemplacé git commit --allow-empty -m 'initial' con git add.; git commit -m "commit laravel inicial"; git push; Y luego este paso de rebase: git rebase --onto newroot --root master está fallando con una TONELADA de conflictos de fusión. ¿Algún consejo? : ((
kp123
@ kp123 intente cometer un vacío :)
Antony Hatchkins
12

Me gusta la respuesta de Aristóteles. Pero descubrió que para un repositorio grande (> 5000 confirmaciones) la rama de filtro funciona mejor que el rebase por varias razones 1) es más rápido 2) no requiere intervención humana cuando hay un conflicto de fusión. 3) puede reescribir las etiquetas, preservándolas. Tenga en cuenta que filter-branch funciona porque no hay dudas sobre el contenido de cada commit; es exactamente lo mismo que antes de este 'rebase'.

Mis pasos son:

# 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

Tenga en cuenta que las opciones '--tag-name-filter cat' significan que las etiquetas se reescribirán para apuntar a los commits recién creados.

Kent
fuente
Esto no ayuda a crear confirmaciones no vacías que también es un caso de uso interesante.
ceztko
En comparación con otras soluciones, la suya tiene solo un efecto secundario insignificante: cambia los valores hash, pero toda la historia permanece intacta. ¡Gracias!
Vladyslav Savchenko el
5

Usé fragmentos de la respuesta de Aristóteles y Kent con éxito:

# 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

Esto también reescribirá todas las ramas (no solo master) además de las etiquetas.

ldav1s
fuente
¿Qué hace esta última línea?
Diederick C. Niehorster
Busca refs/original/y elimina cada referencia. Las referencias que elimina ya deberían estar referenciadas por alguna otra rama, por lo que realmente no desaparecen, solo refs/original/se eliminan.
ldav1s
Esto funcionó para mí. Además, solía timedatectl set-time '2017-01-01 00:00:00'dar newrootuna marca de tiempo antigua.
chrm
4

git rebase --root --onto $emptyrootcommit

debería hacer el truco fácilmente

Uwe Kleine-König
fuente
3
$emptyrootcommites una variable de shell que se expande a nada, seguramente?
Flimm
@Flimm: $ emptyrootcommit es el sha1 de una confirmación vacía que el póster original ya parece tener.
Uwe Kleine-König
4

Creo que usar git replacey git filter-branches una mejor solución que usar un git rebase:

  • mejor presentación
  • más fácil y menos riesgoso (puede verificar su resultado en cada paso y deshacer lo que hizo ...)
  • funciona bien con múltiples sucursales con resultados garantizados

La idea detrás de esto es:

  • Cree una nueva confirmación vacía en el pasado
  • Reemplace el antiguo compromiso raíz por un compromiso exactamente similar, excepto que el nuevo compromiso raíz se agrega como padre
  • Verifique que todo esté como se esperaba y ejecute git filter-branch
  • Una vez más, verifique que todo esté bien y limpie los archivos git que ya no necesita.

Aquí hay un script para los 2 primeros pasos:

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

Puede ejecutar este script sin riesgo (incluso si hacer una copia de seguridad antes de realizar una acción que nunca antes hizo es una buena idea;)), y si el resultado no es el esperado, simplemente elimine los archivos creados en la carpeta .git/refs/replacee intente nuevamente; )

Una vez que haya verificado que el estado del repositorio es el que espera, ejecute el siguiente comando para actualizar el historial de todas las ramas :

git filter-branch -- --all

Ahora, debe ver 2 historias, la antigua y la nueva (consulte la ayuda filter-branchpara obtener más información). Puede comparar los 2 y verificar nuevamente si todo está bien. Si está satisfecho, elimine los archivos que ya no necesita:

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

Puede volver a su mastersucursal y eliminar la sucursal temporal:

git checkout master
git branch -D new-root

Ahora, todo debe hacerse;)

Philippe
fuente
3

Me emocioné y escribí una versión 'idempotente' de este lindo script ... siempre insertará el mismo commit vacío, y si lo ejecutas dos veces, no cambia tus hashes de commit cada vez. Entonces, aquí está mi opinión sobre 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 complejidad extra? tal vez no, pero usaré este.

Esto DEBERÍA también permitir realizar esta operación en varias copias clonadas del repositorio, y terminar con los mismos resultados, por lo que aún son compatibles ... pruebas ... sí, funciona, pero también necesita eliminar y agregar su controles remotos de nuevo, por ejemplo:

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

Para agregar una confirmación vacía al comienzo de un repositorio, si olvidó crear una confirmación vacía inmediatamente después de "git init":

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
Rory
fuente
1
4b825dc ... es el hash del árbol vacío: stackoverflow.com/questions/9765453/…
mrks
2

Bueno, esto es lo que se me ocurrió:

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

Aquí está mi bashscript basado en la respuesta de Kent con mejoras:

  • comprueba la rama original, no solo mastercuando termina;
  • Traté de evitar la bifurcación temporal, pero git checkout --orphansolo funciona con una bifurcación, no en estado de cabeza separada, por lo que se desprotegió el tiempo suficiente para que la nueva raíz se confirme y luego se eliminó;
  • utiliza el hash de la nueva confirmación de raíz durante el filter-branch(Kent dejó un marcador de posición allí para el reemplazo manual);
  • la filter-branchoperación reescribe solo las sucursales locales, no los remotos también
  • los metadatos del autor y del confirmador están estandarizados para que la confirmación raíz sea idéntica en todos los repositorios.

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

Para cambiar la confirmación de raíz:

Primero, cree el commit que desea como el primero.

Segundo, cambie el orden de los commits usando:

git rebase -i --root

Aparecerá un editor con las confirmaciones hasta la confirmación raíz, como:

recoger 1234 mensaje raíz antiguo

recoger 0294 Una confirmación en el medio

seleccione 5678 commit que desea poner en la raíz

Luego, puede poner el commit que desee primero, colocándolo en la primera línea. En el ejemplo:

seleccione 5678 commit que desea poner en la raíz

recoger 1234 mensaje raíz antiguo

recoger 0294 Una confirmación en el medio

Salga del editor, el orden de confirmación habrá cambiado.

PD: para cambiar los usos del editor git, ejecuta:

git config --global core.editor name_of_the_editor_program_you_want_to_use

tigre200
fuente
1
Ahora que rebase tiene --root, esta es, con mucho, la mejor solución.
Ross Burton
1

Combinando lo último y lo mejor. Sin efectos secundarios, sin conflictos, manteniendo etiquetas.

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

Tenga en cuenta que en GitHub perderá los datos de ejecución de CI y PR podría estropearse a menos que también se arreglen otras ramas.

este es mi diseño
fuente
0

Siguiendo la respuesta Aristóteles Pagaltzis y otros pero usando comandos más 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).

Tenga en cuenta que su repositorio no debe contener modificaciones locales en espera de ser confirmadas.
Note git checkout --orphanfuncionará en nuevas versiones de git, supongo.
Tenga en cuenta que la mayoría de las veces git statusda pistas útiles.

ony
fuente
-6

Comience un nuevo repositorio.

Establezca su fecha de regreso a la fecha de inicio que desee.

Haga todo de la forma que desee, ajustando el tiempo del sistema para que refleje cuándo hubiera deseado hacerlo de esa manera. Extraiga archivos del repositorio existente según sea necesario para evitar una gran cantidad de escritura innecesaria.

Cuando llegue hoy, cambie los repositorios y ya está.

Si solo estás loco (establecido) pero razonablemente inteligente (probablemente, porque tienes que tener una cierta cantidad de inteligencia para pensar ideas locas como esta), escribirás el proceso.

Eso también lo hará más agradable cuando decidas que quieres que el pasado haya sucedido de otra manera dentro de una semana a partir de ahora.

MarkusQ
fuente
Tengo malos sentimientos sobre una solución que requiere que juegues con la fecha del sistema, pero me diste una idea, que desarrollé un poco y, por desgracia, funcionó. Así que gracias.
kch
-7

Sé que esta publicación es antigua, pero esta página es la primera cuando busco en Google "insertar commit git".

¿Por qué complicar las cosas simples?

Tienes ABC y quieres ABZC.

  1. git rebase -i trunk (o cualquier cosa antes de B)
  2. cambiar selección para editar en la línea B
  3. haz tus cambios: git add ..
  4. git commit( git commit --amendque editará B y no creará Z)

[Puede hacer tantos git commitcomo desee aquí para insertar más confirmaciones. Por supuesto, puede tener problemas con el paso 5, pero resolver el conflicto de fusión con git es una habilidad que debería tener. Si no, ¡practica!]

  1. git rebase --continue

Simple, ¿no es así?

Si comprende git rebase, agregar un commit 'root' no debería ser un problema.

Diviértete con git!

Alex
fuente
55
La pregunta pide insertar una primera confirmación: desde ABC desea ZABC. Un sencillo git rebaseno puede hacer esto.
Petr Viktorin