Control de versiones (Git)
Los sistemas de control de versiones (o VCSs en ingles) son herramientas utilzadas para registrar y seguir los cambios en el código fuente (u otros archivos o carpetas). Tal como su nombre índica, estas herramientas ayudan a mantener un historial de cambios y además facilitan la colaboración. Los VCSs registran los cambios realizados en las carpetas y su contenido en una serie de fotos, donde cada foto encapsula el estado completo de los archivos/carpetas dentro de un directorio superior. Los VCSs tambien mantienen metadata como quién fue el creador de la foto, mensajes asociados a cada foto, entre otros.
¿Porqué un sistema de control de versiones es útil? Incluso si estas trabajando solo, puedes mirar antiguas versiones del proyecto, mantener un log respecto al por qué de ciertos cambios realizados, trabajar en ramas paralelas de desarrollo y mucho más. Cuando estas trabajando colaborativamente, se transforma en una herramienta invaluable para saber los cambios realizados por otras personas, así como tambien para resolver conflictos en versiones simultáneas de desarrollo.
Los sistemas de control de versiones modernos te permiten responder fácilmente , y a menudo automatizar, preguntas como las siguientes:
- ¿Quién escribio este módulo?
- ¿Cuándo fue que esta línea particular de este archivo particular fue editada? ¿Por quién? ¿Y porqué fue editada?
- Sobre las últimas 1000 revisiones, ¿cuándo y por qué un test unitario particular dejó de funcionar?
Mientras otros sistemas de control de versiones existen, Git es el estandar de facto de los sistemas de control de versiones. Este comic de XKCD refleja la reputación de Git:
Debido a que la interfaz de Git es en enredada, aprender Git utilizando un enfoque de «arriba-a-abajo» (comenzando con su interfaz o línea de comandos) puede ser muy confuso. Claro que es posible memorizar un puñado de comandos y pensarlos como si fueran fórmulas mágicas, y si algo malo ocurre, siempre puedes aplicar la estrategia del comic de arriba.
Si bien la interfaz de Git es poco agraciada, su diseño e ideas fundamentales son hermosos. Mientras que una interfaz horrible tiene que ser memorizada, un hermoso diseño puede ser entendido. Es por esto que damos una explicación de «abajo-a-arriba» sobre Git. Comenzando con su modelo de datos para luego abarcar la interfaz o línea de comandos. Una vez que se entiende el modelo de datos, los comandos se pueden entender mejor en términos de cómo manipulan el modelo de datos subyacente.
El modelo de datos de Git
Existen muchos enfoques ad hoc que puedes realizar para controlar versiones. Git tiene un modelo bien pensado y que permite todas las características de un buen sistema de control de versiones, como mantener un historial, soportar ramas y permitir colaboración.
Fotos
La forma en que Git modela la historia de una colección de archivos y carpetas que comparten un mismo directorio superior es a traves de una serie de fotos. En terminología Git , un archivo es llamado “blob” y es un puñado de bytes. A un directorio se le llama “árbol” y vincula nombres a “blobs” o a otros “árboles” (ya que los directorios pueden contener otros directorios). Una foto es el árbol de nivel superior que esta siendo seguido. Por ejemplo, podríamos tener un árbol como el siguiente:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hola mundo")
|
+- baz.txt (blob, contents = "git es genial")
El árbol de nivel superior (root) contiene dos elementos, el primero es un árbol con el nombre de “foo”, que a su vez contiene un blob llamado “bar.txt”, y el segundo es un blob que se llama “baz.txt”.
Modelando la historia: conexión entre fotos
¿Cómo un sistema de control de versiones puede relacionar fotos? Un modelo simple podría ser una historia líneal. La historia podría ser una lista de fotos ordenadas en base al tiempo. Por varias razones, Git no utiliza un modelo simple como este.
En Git, la historia es un grafo acíclico dirigido (o DAG en inglés) de fotos. Esto puede sonar como una sofisticada palabra matemática, pero que no te intimide. Todo esto significa que cada foto en Git esta vinculada a un conjunto de “nodos padres”, que son las fotos que la preceden. Es un conjunto de nodos en vez de uno solo (como sería el caso de una historia líneal) porque una foto puede descender de multiples padres, por ejemplo, al ser fusionadas (merging) dos ramas paralelas de desarrollo.
Git llama a estas fotos “commits”. Visualizar una historia de commits puede verse parecido a algo como esto:
o <-- o <-- o <-- o
^
\
--- o <-- o
En el arte ASCII de arriba, las o
s corresponden a commits o fotos
individuales. Las flechas apuntan al padre de cada uno de los commit (es una
relación de “precedente” y no de “procedente”). Luego del tercer commit, la
rama de la historia se separa en dos ramas. Esto puede corresponder, por
ejemplo, a dos características que se están desarrollando en paralelo e
independiente de la otra. En el futuro, estas ramas se van a fusionar para crear
una nueva foto que incorporará ambas características, produciendo una nueva
historia que es como la siguiente, con el nuevo commit fusionado destacado en
negrita:
o <-- o <-- o <-- o <---- o ^ / \ v --- o <-- o
Los commits en Git son inmutables. Esto no significa que los errores no se pueden correjir, sino que cualquier “edición” a la historia de commits implica crear un nuevo commit, y las referencias (ver abajo) son actualizadas para apuntar a los nuevos commits.
Modelo de datos como pseudocódigo
Puede ser instructivo ver el modelo de datos de Git escrito en pseudocódigo:
// un archivo es un puñado de bytes
type blob = array<byte>
// un directorio contiene archivos y directorios con nombres
type tree = map<string, tree | blob>
// un commit tiene padres, metadata, y un arbol de alto nivel
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}
Es un limpio y simple modelo de la historia.
Objetos y direccionamiento del contenido
Un “objeto” es un blob, árbol o commit:
type object = blob | tree | commit
En el almacenamiento de datos de Git, todos los objetos son direccionados a su contenido por su SHA-1 hash.
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blobs, árboles y commits están unificados de esta forma: todos son objetos. Cuando estos referencian a otros objetos, realmente no es que esten contenidos en su “representación” de disco, sino que se refieren a ellos por su hash.
Por ejemplo, el árbol usado como ejemplo de una estructura de directorio
arriba (se visualiza usando el comando git cat-file -p
698281bc680d1995c5f4caaf3359721a5a58d48d
), y se ve algo parecido a esto:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
El árbol en sí contiene punteros a su contenido, baz.txt
(un blob) y foo
(un árbol). Si miramos el contenido que nos direcciona el hash correspondiente al
objeto baz.txt usando git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,
obtenemos lo siguiente:
git es genial
Referencias
Ahora, todas las fotos pueden ser identificas por su SHA-1 hash. Esto es inconveniente ya que los humanos no somos buenos recordando secuencias hexadecimales de 40 caracteres.
La solución de Git a este problema son nombres legibles por humanos para
los SHA-1 hashes, llamado “referencias”. Las referencias son punteros para los
commits.
A diferencia de los objetos que son inmutables, las referencias son mutables
(pueden ser actualizados para referenciar a un nuevo commit).
Por ejemplo, la referencia master
usualmente apunta al último commit de la
rama principal de desarrollo.
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
Con esto, Git puede utilizar nombres legibles por humanos como “master” para referirse a una foto particular en la historia, en vez de utilizar una larga secuencia hexadecimal.
Un detalle es que nosotros frecuentemente queremos esta noción de saber donde
estamos ahora en la historia, por lo tanto cuando tomamos una nueva foto, sabemos
que es respecto al como fijamos los padres
en el commit. En Git, el saber
donde estamos actualmente es la referencia especial “HEAD”.
Repositorios
Finalmente podemos definir (aproximadamente) que es un repositorio en Git: es
la data de los objetos
y referencias
.
En el disco, todo lo que Git almacena son objetos y referencias: esto es todo
el modelo de datos de Git. Todos los comandos de git
vinculan alguna
manipulación del commit DAG (grafo) ya sea añadiendo objetos y agregando/actualizando
referencias.
Cada vez que estas tipeando algún comando, piensa acerca de la manipulación
que el comando esta realizando en la estructura de grafo subyacente. Por el contrario,
si estas intentando hacer algún tipo de cambio particular en el DAG commit,
e.g. “descartar un cambio que no ha sido ingresado por commit y hacer que la
referencia master
apunte al commit 5d83f9e
”, existe probablemente un
comando para hacer esto (e.g. en este caso, git checkout master; git reset --hard
5d83f9e
).
Área de preparación
Este es otro concepto que es ortogonal al modelo de datos, pero que es parte de la interfaz para crear commits.
Una forma de imaginarse la implementación de la toma de fotos que se describe arriba es tener un comando para “tomar la foto” que lo haga basandose en el estado actual del directorio de trabajo. Algunas herramientas para controlar versiones trabajan de esta forma, pero no Git. Queremos fotos limpias, y podría ser que no siempre sea ideal tomar una foto del estado actual. Por ejemplo, imagina un escenario donde has implementado dos características separadas, y quieres crear dos commits separados, donde primero se incluye la primera característica, y luego se incorpore la segunda. O imagina un escenario donde tienes que debuggear imprimiendo sentencias por todo el código, para arreglar un error; y quieres realizar un commit para incorporar el arreglo pero descartando todas las sentencias utilizadas.
Git se acomoda a esos escenario permitiendote especificar cuáles modificaciones deben ser incluidas en la siguiente foto a través de este mécanismo llamado área de preparación (o staging area).
La interfaz de línea de comandos de Git
Para evitar duplicidad de información, no vamos a explicar en detalle los comandos de abajo. Se recomienda encarecidamente ver el libro Pro Git para mayor información, o ver el video de la clase.
Básicos
git help <command>
: obtener ayuda acerca de un comando de gitgit init
: crea un nuevo repositorio de git, con la data almacenada en el directorio.git
git status
: te dice que esta ocurriendogit add <filename>
: agrega un archivo al área staginggit commit
: crea un nuevo commit- Escribe buenos mensajes para los commit!
- Incluso más rázones para escribir buenos mensajes para los commit!
git log
: muestra un log plano de la historiagit log --all --graph --decorate
: muestra la historia como un DAG (grafo)git diff <filename>
: muestra las diferencias desde el último commitgit diff <revision> <filename>
: muestra las diferencias de un archivo entre fotosgit checkout <revision>
: actualiza HEAD y la rama actual
Ramificación y fusión (Branching - merging)
git branch
: muestra las ramificacionesgit branch <name>
: crea una ramagit checkout -b <name>
: crea una rama y te cambia a ella- esto es un atajo para
git branch <name>; git checkout <name>
- esto es un atajo para
git merge <revision>
: fusiona con la rama actual.git mergetool
: usa una sofisticada herramienta que te ayuda a resolver los conflictos por combinar ramasgit rebase
: rebase set of patches onto a new base
Remoto
git remote
: lista de ubicaciones remotasgit remote add <name> <url>
: agregar ubicación remotagit push <remote> <local branch>:<remote branch>
: envíar objeto a ubicación remota y actualizar las referencias remotasgit branch --set-upstream-to=<remote>/<remote branch>
: configurar correspondencia entre ubicación local y rama remotagit fetch
: extraer objetos/referencias desde una ubicación remotagit pull
: es lo mismo quegit fetch; git merge
git clone
: descargar un repositorio desde una ubicación remota
Undo
git commit --amend
: editar el contenido/mensaje de un commitgit reset HEAD <file>
: para deshacer la preparación del archivogit checkout -- <file>
: descartar cambios
Git Avanzado
git config
: Git es muy personalizablegit clone --depth=1
: clonar simplificadamente, sin el historial completo de las versionesgit add -p
: área de preparación (staging) interactivagit rebase -i
: rebase interactivogit blame
: muestra quien fue el último en editar alguna líneagit stash
: eliminar temporalmente las modificaciones del directorio de trabajogit bisect
: busqueda binaria en el historial (e.g. para regresión).gitignore
: específica los archivos que intencionalmente no estas siguiendo para que sean ignorados
Misceláneos
- GUIs: existen muchas interfaces gráficas para Git por ahí . Nosotros personalmente no utilizamos, y en vez de eso, solo usamos la interfaz de línea de comandos.
- Integración con la consola: Es súper útil tener el estado de Git como parte del prompt en la consola (zsh, bash). Generalmente incluido en entornos de trabajo como Oh My Zsh.
- Integraciones con el editor: similar a lo de arriba, existen integraciones útiles con muchas caracteristicas. fugitive.vim es el estándar para Vim.
- Flujos de trabajo: te enseñamos el modelo de datos, además de algunos comandos básicos; No te dijimos cuales son las practicas que debes seguir cuando estas trabajando en grandes proyectos (y hay muchos enfoques distintos).
- GitHub: Git no es GitHub. GitHub tiene una forma especifica de contribuir con código a otros proyectos, llamada pull requests.
- Otros proveedores de Git: GitHub no es especial; hay muchos hospedadores de repositorios de Git, como GitLab y BitBucket.
Recursos
- Pro Git es altamente recomendable de leer. Los primeros capítulos, del 1-al-5, abarcan casi todo lo necesario para usar Git de manera competente, dado que ahora entiendes el modelo de datos. Los últimos capítulos tienen material interesante y avanzado.
- Oh Shit, Git!?! es una breve guia de cómo recuperarse de algunos errores típicos usando Git.
- Git for Computer Scientists es una breve explicación del modelo de datos de Git, con menos pseudocódigo y más diagramas elaborados que los utilizado en las notas de esta clase.
- Git from the Bottom Up es una explicación detallada de los detalles de la implementación de Git más allá del modelo de datos, solo para los curiosos.
- How to explain git in simple words
- Learn Git Branching es un juego basado en el explorador web que te enseña Git.
Ejercicios
- Si no tienes experiencia previa utilizando Git, intenta con cualquiera de las dos alternativas siguientes: leer los primeros capítulos del libro Pro Git o hacer un tutorial como Learn Git Branching. A medida que vayas trabajando en esto, relaciona los comandos de Git a su modelo de datos.
- Clona el repositorio del sitio web de la clase.
- Explora el historial de versiones visualizándolo como un grafo.
- ¿Quién fue la última persona en modificar
README.md
? (Pista: usagit log
con un argumento). - ¿Cuál fue el mensaje del commit de la última modificación en
collections:
línea de_config.yml
? (Pista: usagit blame
ygit show
)
- Una equivocación común cuando se esta aprendiendo Git es hacer commit de grandes archivos que no se deberían manejar con Git o agregar información sensible. Intenta agregar un archivo al repositorio, realizar algunos commits y luego borra el archivo de la historia (quizás quieras echar un vistazo a esto).
- Clona algún repositorio desde Github, y modifica uno de los archivos existentes.
¿Qué ocurre cuando usas
git stash
? ¿Qué vez cuando corresgit log --all --oneline
? Ejecutagit stash pop
para deshacer lo que hiciste congit stash
. ¿Bajo que escenario esto puedo ser útil? - Como muchas otras herramientas de líneas de comando, Git provee un archivo
de configuración (o dotfile) llamado
~/.gitconfig
. Crea un alias en~/.gitconfig
para que cuando corrasgit graph
, obtengas el resultado degit log --all --graph --decorate --oneline
. - Puedes definir de manera general patrones a ignorar en
~/.gitignore_global
luego corrergit config --global core.excludesfile ~/.gitignore_global
. Haz esto, y configura tu archivo global gitignore para ignorar archivos temporales especificos de OS o del editor, como.DS_Store
. - Clona el repositorio del sitio web de la clase, encuentra un error de tipeo, traducción u otra mejora que puedas hacer, y envía un pull request por Github.
Licensed under CC BY-NC-SA.