Git
Chapters ▾ 2nd Edition

7.13 Herramientas de Git - Replace

Replace

Los objetos de Git son inmutables, pero proporciona una manera interesante de pretender reemplazar objetos en su base de datos con otros objetos.

El comando replace te permite especificar un objeto en Git y decirle "cada vez que vea esto, fingir que es esta otra cosa". Esto es más útil para reemplazar un “commit” en tu historial con otro.

Por ejemplo, supongamos que tienes un gran historial de códigos y deseas dividir tu repositorio en un breve historial para nuevos desarrolladores y una historia mucho más larga para las personas interesadas en la minería de datos. Puedes injertar una historia en la otra mediante replace ingresando el “commit” más antiguo en la nueva línea con el último “commit” en el anterior. Esto es bueno porque significa que en realidad no tienes que reescribir cada “commit” en la nueva historia, como normalmente tendrías que hacer para unirlos juntos (porque el parentesco lo efectúan los SHA-1s).

Vamos a probar esto. Tomemos un repositorio existente, lo dividimos en dos repositorios, uno reciente y otro histórico, y luego veremos cómo podemos recombinarlos sin modificar los repositorios recientes SHA-1 a través de replace.

Usaremos un repositorio sencillo con cinco compromisos simples:

$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Queremos dividir esto en dos líneas de la historia. Una línea pasa de comprometer uno a cometer cuatro - que será el histórico. La segunda línea sólo compromete cuatro y cinco - que será la historia reciente.

replace1

Bueno, la creación del historial histórico es fácil, sólo tenemos que poner una rama en la historia y luego empujar esa rama a la rama principal de un nuevo repositorio remoto.

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
replace2

Ahora podemos hacer push de la nueva rama history a la rama` master` de nuestro nuevo repositorio:

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

OK, así que nuestra historia se publica. Ahora la parte más difícil es truncar nuestra historia reciente para hacerla más pequeña. Necesitamos una superposición para que podamos reemplazar un “commit” en una con un “commit” equivalente en la otra, por lo que vamos a truncar esto a sólo cometer cuatro y cinco (y así cometer cuatro superposiciones).

$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

En este caso, es útil crear un “commit” de base que tenga instrucciones sobre cómo expandir el historial, por lo que otros desarrolladores saben qué hacer si acceden al primer “commit” en el historial truncado y necesitan más. Por lo tanto, lo que vamos a hacer es crear un objeto de confirmación inicial como nuestro punto base con instrucciones, luego hacemos un “rebase” de los compromisos restantes (cuatro y cinco) encima de él.

Para ello, debemos elegir un punto para dividir, que es 9c68fdc en SHA-speak (para nosotros es el tercer commit). Por lo tanto, nuestra comisión base se basará en ese árbol. Podemos crear nuestro “commit base” con el comando commit-tree, que solo toma un árbol y nos dará un nuevo objeto de “commit” sin padres SHA-1.

$ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Nota

El comando commit-tree es uno de un conjunto de comandos que comúnmente se denominan comandos plumbing. Estos son comandos que no suelen ser utilizados directamente, sino que son utilizados por ** otros comandos Git para hacer trabajos más pequeños. En ocasiones, cuando estamos haciendo tareas más extrañas, como éstas, nos permiten hacer cosas de nivel muy bajo, pero no son para uso diario. Puedes leer más acerca de los comandos de plomería en Fontanería y porcelana

replace3

OK, así que ahora que tenemos un “commit” de base, podemos hacer “rebase” al resto de nuestra historia encima de éste con 'rebase de git --onto`. El argumento --onto será el SHA-1 que acabamos de regresar de commit-tree y el punto de “rebase” será el tercer commit (el padre del primer “commit” que queremos mantener, 9c68fdc):

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
replace4

Así que hemos re-escrito nuestra historia reciente en la parte superior de un tiro de base de comisión que ahora tiene instrucciones sobre cómo reconstruir toda la historia si quisiéramos. Podemos empujar esa nueva historia a un nuevo proyecto y ahora, cuando las personas clonen ese repositorio, solo verán los dos compromisos más recientes y luego un commit de base con instrucciones.

Cambiemos de roles a alguien que clonara el proyecto por primera vez y que quiere toda la historia. Para obtener los datos del historial después de clonar este repositorio truncado, habría que añadir un segundo mando a distancia para el repositorio histórico y buscar:

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

Ahora el colaborador tendría sus compromisos recientes en la rama master y los compromisos históricos en la rama project-history/master.

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Para combinarlos, simplemente puede llamar a git replace con el “commit” que desea reemplazar y luego el “commit” con el que desea reemplazarlo. Así que queremos reemplazar el "cuarto" “commit” en la rama maestra con el "cuarto" “commit” en la rama project-history/master:

$ git replace 81a708d c6e1e95

Ahora bien, si nos fijamos en la historia de la rama master, parece que se ve así:

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Genial, ¿verdad? Sin tener que cambiar todos los SHA-1s upstream, pudimos reemplazar un “commit” en nuestra historia con un “commit” totalmente diferente y todas las herramientas normales (bisect,` blame`, etc.) funcionarán como esperamos .

replace5

Curiosamente, todavía muestra 81a708d como el SHA-1, a pesar de que en realidad está utilizando los datos de confirmación c6e1e95 con los que lo reemplazamos. Incluso si ejecuta un comando como cat-file, le mostrará los datos reemplazados:

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

Recuerde que el padre real de 81a708d fue nuestro “placeholder commit” (622e88e), no 9c68fdce, como se indica aquí.

Otra cosa interesante es que estos datos se guardan en nuestras referencias:

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

Esto significa que es fácil compartir nuestro reemplazo con otros, porque podemos empujar esto a nuestro servidor y otras personas pueden descargarlo fácilmente. Esto no es tan útil en el escenario de injerto de historia que hemos pasado aquí (ya que todo el mundo estaría descargando ambas historias de todos modos, ¿por qué separarlas?). Pero puede ser útil en otras circunstancias.