{
  "title": "Control de Versiones (Git)",
  "excerpt": "Cómo usar el control de versiones _correctamente_ y aprovecharlo para salvarte de desastres, colaborar con otros y encontrar y aislar rápidamente cambios problemáticos. No más `rm -rf; git clone`. No más conflictos de fusión (bueno, al menos menos de ellos). No más grandes bloques de código comentado. No más preocupaciones sobre cómo encontrar qué rompió tu código. No más \"¡oh no, eliminamos el código que funcionaba?!\".",
  "content_html": "<p>Los sistemas de control de versiones (VCS, por sus siglas en inglés) son herramientas utilizadas para rastrear cambios en el código fuente (u otras colecciones de archivos y carpetas). Como su nombre lo indica, estas herramientas ayudan a mantener un historial de cambios; además, facilitan la colaboración. Los VCS rastrean cambios en una carpeta y su contenido en una serie de instantáneas, donde cada instantánea encapsula el estado completo de archivos/carpetas dentro de un directorio de nivel superior. Los VCS también mantienen metadatos como quién creó cada instantánea, mensajes asociados con cada instantánea, etc.</p>\n\n<p>¿Por qué es útil el control de versiones? Incluso cuando trabajas solo, te permite ver instantáneas antiguas de un proyecto, mantener un registro de por qué se realizaron ciertos cambios, trabajar en ramas paralelas de desarrollo y mucho más. Cuando trabajas con otros, es una herramienta invaluable para ver qué han cambiado otras personas, así como para resolver conflictos en el desarrollo concurrente.</p>\n\n<p>Los VCS modernos también te permiten responder fácilmente (y a menudo automáticamente) preguntas como:</p>\n\n<ul>\n<li>¿Quién escribió este módulo?</li>\n<li>¿Cuándo se editó esta línea particular de este archivo particular? ¿Por quién? ¿Por qué se editó?</li>\n<li>Durante las últimas 1000 revisiones, ¿cuándo/por qué dejó de funcionar una prueba unitaria particular?</li>\n</ul>\n\n<p>Aunque existen otros VCS, <strong>Git</strong> es el estándar de facto para el control de versiones. Este <a href=\"https://xkcd.com/1597/\">cómic de XKCD</a> captura la reputación de Git:</p>\n\n<p><img src=\"https://imgs.xkcd.com/comics/git.png\" alt=\"xkcd 1597\"></p>\n\n<p>Debido a que la interfaz de Git es una abstracción con fugas, aprender Git de arriba hacia abajo (comenzando con su interfaz / interfaz de línea de comandos) puede llevar a mucha confusión. Es posible memorizar un puñado de comandos y pensar en ellos como encantamientos mágicos, y seguir el enfoque del cómic anterior cada vez que algo sale mal.</p>\n\n<p>Si bien Git admitidamente tiene una interfaz fea, su diseño e ideas subyacentes son hermosos. Mientras que una interfaz fea tiene que ser <em>memorizada</em>, un diseño hermoso puede ser <em>comprendido</em>. Por esta razón, damos una explicación de abajo hacia arriba de Git, comenzando con su modelo de datos y luego cubriendo la interfaz de línea de comandos. Una vez que se comprende el modelo de datos, los comandos pueden entenderse mejor en términos de cómo manipulan el modelo de datos subyacente.</p>\n\n<h1>Modelo de datos de Git</h1>\n\n<p>Hay muchos enfoques ad-hoc que podrías tomar para el control de versiones. Git tiene un modelo bien pensado que habilita todas las características agradables del control de versiones, como mantener el historial, soportar ramas y habilitar la colaboración.</p>\n\n<h2>Instantáneas</h2>\n\n<p>Git modela el historial de una colección de archivos y carpetas dentro de algún directorio de nivel superior como una serie de instantáneas. En la terminología de Git, un archivo se llama \"blob\", y es solo un montón de bytes. Un directorio se llama \"árbol\" (tree), y mapea nombres a blobs o árboles (por lo que los directorios pueden contener otros directorios). Una instantánea es el árbol de nivel superior que se está rastreando. Por ejemplo, podríamos tener un árbol como el siguiente:</p>\n\n<pre><code>&lt;root&gt; (tree)\n|\n+- foo (tree)\n|  |\n|  + bar.txt (blob, contents = \"hello world\")\n|\n+- baz.txt (blob, contents = \"git is wonderful\")\n</code></pre>\n\n<p>El árbol de nivel superior contiene dos elementos, un árbol \"foo\" (que a su vez contiene un elemento, un blob \"bar.txt\"), y un blob \"baz.txt\".</p>\n\n<h2>Modelando el historial: relacionando instantáneas</h2>\n\n<p>¿Cómo debería un sistema de control de versiones relacionar instantáneas? Un modelo simple sería tener un historial lineal. Un historial sería una lista de instantáneas en orden temporal. Por muchas razones, Git no usa un modelo simple como este.</p>\n\n<p>En Git, un historial es un grafo acíclico dirigido (DAG) de instantáneas. Eso puede sonar como una palabra matemática elegante, pero no te intimides. Todo esto significa que cada instantánea en Git se refiere a un conjunto de \"padres\", las instantáneas que la precedieron. Es un conjunto de padres en lugar de un solo padre (como sería el caso en un historial lineal) porque una instantánea podría descender de múltiples padres, por ejemplo, debido a la combinación (fusión) de dos ramas paralelas de desarrollo.</p>\n\n<p>Git llama a estas instantáneas \"commits\". Visualizar un historial de commits podría verse algo así:</p>\n\n<pre><code>o &lt;-- o &lt;-- o &lt;-- o\n            ^\n             \\\n              --- o &lt;-- o\n</code></pre>\n\n<p>En el arte ASCII anterior, las <code>o</code>s corresponden a commits individuales (instantáneas). Las flechas apuntan al padre de cada commit (es una relación \"viene antes\", no \"viene después\"). Después del tercer commit, el historial se ramifica en dos ramas separadas. Esto podría corresponder, por ejemplo, a dos características separadas que se desarrollan en paralelo, independientemente una de la otra. En el futuro, estas ramas pueden fusionarse para crear una nueva instantánea que incorpore ambas características, produciendo un nuevo historial que se ve así, con el commit de fusión recién creado mostrado en negrita:</p>\n\n<pre class=\"highlight\">\n<code>\no &lt;-- o &lt;-- o &lt;-- o &lt;---- <strong>o</strong>\n            ^            /\n             \\          v\n              --- o &lt;-- o\n</code>\n</pre>\n\n<p>Los commits en Git son inmutables. Esto no significa que los errores no puedan corregirse, sin embargo; es solo que las \"ediciones\" al historial de commits en realidad están creando commits completamente nuevos, y las referencias (ver más abajo) se actualizan para apuntar a los nuevos.</p>\n\n<h2>Modelo de datos, como pseudocódigo</h2>\n\n<p>Puede ser instructivo ver el modelo de datos de Git escrito en pseudocódigo:</p>\n\n<pre><code>// un archivo es un montón de bytes\ntype blob = array&lt;byte&gt;\n\n// un directorio contiene archivos y directorios nombrados\ntype tree = map&lt;string, tree | blob&gt;\n\n// un commit tiene padres, metadatos y el árbol de nivel superior\ntype commit = struct {\n    parents: array&lt;commit&gt;\n    author: string\n    message: string\n    snapshot: tree\n}\n</code></pre>\n\n<p>Es un modelo de historial limpio y simple.</p>\n\n<h2>Objetos y direccionamiento por contenido</h2>\n\n<p>Un \"objeto\" es un blob, árbol o commit:</p>\n\n<pre><code>type object = blob | tree | commit\n</code></pre>\n\n<p>En el almacén de datos de Git, todos los objetos son direccionados por contenido mediante su <a href=\"https://en.wikipedia.org/wiki/SHA-1\">hash SHA-1</a>.</p>\n\n<pre><code>objects = map&lt;string, object&gt;\n\ndef store(object):\n    id = sha1(object)\n    objects[id] = object\n\ndef load(id):\n    return objects[id]\n</code></pre>\n\n<p>Los blobs, árboles y commits están unificados de esta manera: todos son objetos. Cuando hacen referencia a otros objetos, en realidad no los <em>contienen</em> en su representación en disco, sino que tienen una referencia a ellos por su hash.</p>\n\n<p>Por ejemplo, el árbol para la estructura de directorio de ejemplo <a href=\"#snapshots\">anterior</a> (visualizado usando <code>git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d</code>), se ve así:</p>\n\n<pre><code>100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt\n040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo\n</code></pre>\n\n<p>El árbol en sí contiene punteros a su contenido, <code>baz.txt</code> (un blob) y <code>foo</code> (un árbol). Si miramos el contenido direccionado por el hash correspondiente a baz.txt con <code>git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85</code>, obtenemos lo siguiente:</p>\n\n<pre><code>git is wonderful\n</code></pre>\n\n<h2>Referencias</h2>\n\n<p>Ahora, todas las instantáneas pueden identificarse por sus hashes SHA-1. Eso es inconveniente, porque los humanos no son buenos para recordar cadenas de 40 caracteres hexadecimales.</p>\n\n<p>La solución de Git a este problema son nombres legibles por humanos para los hashes SHA-1, llamados \"referencias\". Las referencias son punteros a commits. A diferencia de los objetos, que son inmutables, las referencias son mutables (pueden actualizarse para apuntar a un nuevo commit). Por ejemplo, la referencia <code>master</code> generalmente apunta al último commit en la rama principal de desarrollo.</p>\n\n<pre><code>references = map&lt;string, string&gt;\n\ndef update_reference(name, id):\n    references[name] = id\n\ndef read_reference(name):\n    return references[name]\n\ndef load_reference(name_or_id):\n    if name_or_id in references:\n        return load(references[name_or_id])\n    else:\n        return load(name_or_id)\n</code></pre>\n\n<p>Con esto, Git puede usar nombres legibles por humanos como \"master\" para referirse a una instantánea particular en el historial, en lugar de una larga cadena hexadecimal.</p>\n\n<p>Un detalle es que a menudo queremos una noción de \"dónde estamos actualmente\" en el historial, para que cuando tomemos una nueva instantánea, sepamos a qué es relativa (cómo establecemos el campo <code>parents</code> del commit). En Git, ese \"dónde estamos actualmente\" es una referencia especial llamada \"HEAD\".</p>\n\n<h2>Repositorios</h2>\n\n<p>Finalmente, podemos definir qué es (aproximadamente) un <em>repositorio</em> de Git: son los datos <code>objects</code> y <code>references</code>.</p>\n\n<p>En disco, todo lo que Git almacena son objetos y referencias: eso es todo lo que hay en el modelo de datos de Git. Todos los comandos <code>git</code> se mapean a alguna manipulación del DAG de commits agregando objetos y agregando/actualizando referencias.</p>\n\n<p>Siempre que estés escribiendo cualquier comando, piensa en qué manipulación está haciendo el comando a la estructura de datos de grafo subyacente. Por el contrario, si estás tratando de hacer un tipo particular de cambio al DAG de commits, por ejemplo, \"descartar cambios no confirmados y hacer que la referencia 'master' apunte al commit <code>5d83f9e</code>\", probablemente haya un comando para hacerlo (por ejemplo, en este caso, <code>git checkout master; git reset --hard 5d83f9e</code>).</p>\n\n<h1>Área de preparación</h1>\n\n<p>Este es otro concepto que es ortogonal al modelo de datos, pero es parte de la interfaz para crear commits.</p>\n\n<p>Una forma en que podrías imaginar implementar la captura de instantáneas como se describió anteriormente es tener un comando \"crear instantánea\" que cree una nueva instantánea basada en el <em>estado actual</em> del directorio de trabajo. Algunas herramientas de control de versiones funcionan así, pero no Git. Queremos instantáneas limpias, y puede que no siempre sea ideal hacer una instantánea del estado actual. Por ejemplo, imagina un escenario en el que has implementado dos características separadas y quieres crear dos commits separados, donde el primero introduce la primera característica y el siguiente introduce la segunda característica. O imagina un escenario en el que tienes declaraciones de impresión de depuración agregadas por todo tu código, junto con una corrección de error; quieres confirmar la corrección de error mientras descartas todas las declaraciones de impresión.</p>\n\n<p>Git acomoda tales escenarios permitiéndote especificar qué modificaciones deben incluirse en la siguiente instantánea a través de un mecanismo llamado \"área de preparación\" (staging area).</p>\n\n<h1>Interfaz de línea de comandos de Git</h1>\n\n<p>Para evitar duplicar información, no vamos a explicar los comandos a continuación en detalle. Consulta el altamente recomendado <a href=\"https://git-scm.com/book/en/v2\">Pro Git</a> para más información.</p>\n\n<h2>Conceptos básicos</h2>\n\n<p>El comando <code>git init</code> inicializa un nuevo repositorio de Git, con los metadatos del repositorio almacenados en el directorio <code>.git</code>:</p>\n\n<pre><code class=\"language-console\">$ mkdir myproject\n$ cd myproject\n$ git init\nInitialized empty Git repository in .git\n$ git status\nOn branch master\nNo commits yet\nnothing to commit (create/copy files and use \"git add\" to track)\n</code></pre>\n\n<p>¿Cómo interpretamos esta salida? \"No commits yet\" básicamente significa que nuestro historial de versiones está vacío. Arreglemos eso.</p>\n\n<pre><code class=\"language-console\">$ echo \"hello, git\" &gt; hello.txt\n$ git add hello.txt\n$ git status\nOn branch master\nNo commits yet\nChanges to be committed:\n  (use \"git rm --cached &lt;file&gt;...\" to unstage)\n        new file:   hello.txt\n$ git commit -m 'Initial commit'\n[master (root-commit) 4515d17] Initial commit\n 1 file changed, 1 insertion(+)\n create mode 100644 hello.txt\n</code></pre>\n\n<p>Con esto, hemos agregado un archivo al área de preparación con <code>git add</code>, y luego confirmamos ese cambio con <code>git commit</code>, agregando un mensaje de commit simple \"Initial commit\". Si no especificáramos una opción <code>-m</code>, Git abriría nuestro editor de texto para permitirnos escribir un mensaje de commit.</p>\n\n<p>Ahora que tenemos un historial de versiones no vacío, podemos visualizar el historial. Visualizar el historial como un DAG puede ser especialmente útil para comprender el estado actual del repositorio y conectarlo con tu comprensión del modelo de datos de Git.</p>\n\n<p>El comando <code>git log</code> visualiza el historial. Por defecto, muestra una versión aplanada, que oculta la estructura del grafo. Si usas un comando como <code>git log --all --graph --decorate</code>, te mostrará el historial completo de versiones del repositorio, visualizado en forma de grafo.</p>\n\n<pre><code class=\"language-console\">$ git log --all --graph --decorate\n* commit 4515d17a167bdef0a91ee7d50d75b12c9c2652aa (HEAD -&gt; master)\n  Author: Subramanya N &lt;subramanyanagabhushan@gmail.com&gt;\n  Date: Tue Dec 21 22:18:36 2020 -0500\n      Initial commit\n</code></pre>\n\n<p>Esto no se ve muy parecido a un grafo, porque solo contiene un solo nodo. Hagamos algunos cambios más, creemos un nuevo commit y visualicemos el historial una vez más.</p>\n\n<pre><code class=\"language-console\">$ echo \"another line\" &gt;&gt; hello.txt\n$ git status\nOn branch master\nChanges not staged for commit:\n  (use \"git add &lt;file&gt;...\" to update what will be committed)\n  (use \"git checkout -- &lt;file&gt;...\" to discard changes in working directory)\n        modified:   hello.txt\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n$ git add hello.txt\n$ git status\nOn branch master\nChanges to be committed:\n  (use \"git reset HEAD &lt;file&gt;...\" to unstage)\n        modified:   hello.txt\n$ git commit -m 'Add a line'\n[master 35f60a8] Add a line\n 1 file changed, 1 insertion(+)\n</code></pre>\n\n<p>Ahora, si visualizamos el historial nuevamente, veremos algo de la estructura del grafo:</p>\n\n<pre><code>* commit 35f60a825be0106036dd2fbc7657598eb7b04c67 (HEAD -&gt; master)\n| Author: Subramanya N &lt;subramanyanagabhushan@gmail.com&gt;\n| Date:   Tue Dec 21 22:26:20 2020 -0500\n|     Add a line\n* commit 4515d17a167bdef0a91ee7d50d75b12c9c2652aa\n  Author: Subramanya N &lt;subramanyanagabhushan@gmail.com&gt;\n  Date: Tue Dec 21 22:18:36 2020 -0500\n      Initial commit\n</code></pre>\n\n<p>Además, nota que muestra el HEAD actual, junto con la rama actual (master).</p>\n\n<p>Podemos ver versiones antiguas usando el comando <code>git checkout</code>.</p>\n\n<pre><code class=\"language-console\">$ git checkout 4515d17  # hash del commit anterior; el tuyo será diferente\nNote: checking out '4515d17'.\nYou are in 'detached HEAD' state. You can look around, make experimental\nchanges and commit them, and you can discard any commits you make in this\nstate without impacting any branches by performing another checkout.\nIf you want to create a new branch to retain commits you create, you may\ndo so (now or later) by using -b with the checkout command again. Example:\n  git checkout -b &lt;new-branch-name&gt;\nHEAD is now at 4515d17 Initial commit\n$ cat hello.txt\nhello, git\n$ git checkout master\nPrevious HEAD position was 4515d17 Initial commit\nSwitched to branch 'master'\n$ cat hello.txt\nhello, git\nanother line\n</code></pre>\n\n<p>Git puede mostrarte cómo han evolucionado los archivos (diferencias, o diffs) usando el comando <code>git diff</code>:</p>\n\n<pre><code class=\"language-console\">$ git diff 4515d17 hello.txt\ndiff --git c/hello.txt w/hello.txt\nindex 94bab17..f0013b2 100644\n--- c/hello.txt\n+++ w/hello.txt\n@@ -1 +1,2 @@\n hello, git\n +another line\n</code></pre>\n\n<ul>\n<li><code>git help &lt;command&gt;</code>: obtener ayuda para un comando de git</li>\n<li><code>git init</code>: crea un nuevo repositorio de git, con datos almacenados en el directorio <code>.git</code></li>\n<li><code>git status</code>: te dice qué está pasando</li>\n<li><code>git add &lt;filename&gt;</code>: agrega archivos al área de preparación</li>\n<li><code>git commit</code>: crea un nuevo commit\n<ul>\n<li>¡Escribe <a href=\"https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html\">buenos mensajes de commit</a>!</li>\n<li>¡Aún más razones para escribir <a href=\"https://chris.beams.io/posts/git-commit/\">buenos mensajes de commit</a>!</li>\n</ul></li>\n<li><code>git log</code>: muestra un registro aplanado del historial</li>\n<li><code>git log --all --graph --decorate</code>: visualiza el historial como un DAG</li>\n<li><code>git diff &lt;filename&gt;</code>: muestra los cambios que hiciste en relación con el área de preparación</li>\n<li><code>git diff &lt;revision&gt; &lt;filename&gt;</code>: muestra diferencias en un archivo entre instantáneas</li>\n<li><code>git checkout &lt;revision&gt;</code>: actualiza HEAD y la rama actual</li>\n</ul>\n\n<h2>Ramificación y fusión</h2>\n\n<p>La ramificación te permite \"bifurcar\" el historial de versiones. Puede ser útil para trabajar en características independientes o correcciones de errores en paralelo. El comando <code>git branch</code> se puede usar para crear nuevas ramas; <code>git checkout -b &lt;branch name&gt;</code> crea una rama y la activa.</p>\n\n<p>La fusión es lo opuesto a la ramificación: te permite combinar historiales de versiones bifurcados, por ejemplo, fusionar una rama de características de vuelta a master. El comando <code>git merge</code> se usa para fusionar.</p>\n\n<ul>\n<li><code>git branch</code>: muestra las ramas</li>\n<li><code>git branch &lt;name&gt;</code>: crea una rama</li>\n<li><code>git checkout -b &lt;name&gt;</code>: crea una rama y cambia a ella\n<ul>\n<li>igual que <code>git branch &lt;name&gt;; git checkout &lt;name&gt;</code></li>\n</ul></li>\n<li><code>git merge &lt;revision&gt;</code>: fusiona en la rama actual</li>\n<li><code>git mergetool</code>: usa una herramienta elegante para ayudar a resolver conflictos de fusión</li>\n<li><code>git rebase</code>: reorganiza un conjunto de parches sobre una nueva base</li>\n</ul>\n\n<h2>Remotos</h2>\n\n<ul>\n<li><code>git remote</code>: lista los remotos</li>\n<li><code>git remote add &lt;name&gt; &lt;url&gt;</code>: agrega un remoto</li>\n<li><code>git push &lt;remote&gt; &lt;local branch&gt;:&lt;remote branch&gt;</code>: envía objetos al remoto y actualiza la referencia remota</li>\n<li><code>git branch --set-upstream-to=&lt;remote&gt;/&lt;remote branch&gt;</code>: establece correspondencia entre rama local y remota</li>\n<li><code>git fetch</code>: recupera objetos/referencias de un remoto</li>\n<li><code>git pull</code>: igual que <code>git fetch; git merge</code></li>\n<li><code>git clone</code>: descarga el repositorio desde un remoto</li>\n</ul>\n\n<h2>Deshacer</h2>\n\n<ul>\n<li><code>git commit --amend</code>: edita el contenido/mensaje de un commit</li>\n<li><code>git reset HEAD &lt;file&gt;</code>: quita un archivo del área de preparación</li>\n<li><code>git checkout -- &lt;file&gt;</code>: descarta cambios</li>\n</ul>\n\n<h1>Git avanzado</h1>\n\n<ul>\n<li><code>git config</code>: Git es <a href=\"https://git-scm.com/docs/git-config\">altamente personalizable</a></li>\n<li><code>git clone --depth=1</code>: clonación superficial, sin todo el historial de versiones</li>\n<li><code>git add -p</code>: preparación interactiva</li>\n<li><code>git rebase -i</code>: reorganización interactiva</li>\n<li><code>git blame</code>: muestra quién editó por última vez qué línea</li>\n<li><code>git stash</code>: elimina temporalmente modificaciones del directorio de trabajo</li>\n<li><code>git bisect</code>: búsqueda binaria en el historial (por ejemplo, para regresiones)</li>\n<li><code>.gitignore</code>: <a href=\"https://git-scm.com/docs/gitignore\">especifica</a> archivos no rastreados intencionalmente para ignorar</li>\n</ul>\n\n<h1>Miscelánea</h1>\n\n<ul>\n<li><strong>GUIs</strong>: hay muchos <a href=\"https://git-scm.com/downloads/guis\">clientes GUI</a> para Git. Personalmente no los usamos y usamos la interfaz de línea de comandos en su lugar.</li>\n<li><strong>Integración con shell</strong>: es súper útil tener un estado de Git como parte de tu prompt de shell (<a href=\"https://github.com/olivierverdier/zsh-git-prompt\">zsh</a>, <a href=\"https://github.com/magicmonty/bash-git-prompt\">bash</a>). A menudo incluido en frameworks como <a href=\"https://github.com/ohmyzsh/ohmyzsh\">Oh My Zsh</a>.</li>\n<li><strong>Integración con editor</strong>: similar a lo anterior, integraciones útiles con muchas características. <a href=\"https://github.com/tpope/vim-fugitive\">fugitive.vim</a> es el estándar para Vim.</li>\n<li><strong>Flujos de trabajo</strong>: te enseñamos el modelo de datos, más algunos comandos básicos; no te dijimos qué prácticas seguir cuando trabajas en proyectos grandes (y hay <a href=\"https://nvie.com/posts/a-successful-git-branching-model/\">muchos</a> <a href=\"https://www.endoflineblog.com/gitflow-considered-harmful\">enfoques</a> <a href=\"https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow\">diferentes</a>).</li>\n<li><strong>GitHub</strong>: Git no es GitHub. GitHub tiene una forma específica de contribuir código a otros proyectos, llamada <a href=\"https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests\">pull requests</a>.</li>\n<li><strong>Otros proveedores de Git</strong>: GitHub no es especial: hay muchos hosts de repositorios Git, como <a href=\"https://about.gitlab.com/\">GitLab</a> y <a href=\"https://bitbucket.org/\">BitBucket</a>.</li>\n</ul>\n\n<h1>Recursos</h1>\n\n<ul>\n<li><a href=\"https://git-scm.com/book/en/v2\">Pro Git</a> es <strong>lectura altamente recomendada</strong>. Revisar los Capítulos 1--5 debería enseñarte la mayor parte de lo que necesitas para usar Git de manera competente, ahora que entiendes el modelo de datos. Los capítulos posteriores tienen material interesante y avanzado.</li>\n<li><a href=\"https://ohshitgit.com/\">Oh Shit, Git!?!</a> es una guía corta sobre cómo recuperarse de algunos errores comunes de Git.</li>\n<li><a href=\"https://eagain.net/articles/git-for-computer-scientists/\">Git for Computer Scientists</a> es una explicación corta del modelo de datos de Git, con menos pseudocódigo y más diagramas elegantes que estas notas de clase.</li>\n<li><a href=\"https://jwiegley.github.io/git-from-the-bottom-up/\">Git from the Bottom Up</a> es una explicación detallada de los detalles de implementación de Git más allá del modelo de datos, para los curiosos.</li>\n<li><a href=\"https://smusamashah.github.io/blog/2017/10/14/explain-git-in-simple-words\">How to explain git in simple words</a></li>\n<li><a href=\"https://learngitbranching.js.org/\">Learn Git Branching</a> es un juego basado en navegador que te enseña Git.</li>\n</ul>",
  "source_hash": "sha256:1882fed561269610d4ac35bc5a461efe73f8481070dd58a45db04a8899598a98",
  "model": "claude-sonnet-4-5-20250929",
  "generated_at": "2026-01-02T04:09:22.194270+00:00"
}