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:

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:

xkcd_1597

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 os 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

Ramificación y fusión (Branching - merging)

Remoto

Undo

Git Avanzado

Misceláneos

Recursos

Ejercicios

  1. 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.
  2. Clona el repositorio del sitio web de la clase.
    1. Explora el historial de versiones visualizándolo como un grafo.
    2. ¿Quién fue la última persona en modificar README.md? (Pista: usa git log con un argumento).
    3. ¿Cuál fue el mensaje del commit de la última modificación en collections: línea de _config.yml? (Pista: usa git blame y git show)
  3. 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).
  4. Clona algún repositorio desde Github, y modifica uno de los archivos existentes. ¿Qué ocurre cuando usas git stash? ¿Qué vez cuando corres git log --all --oneline? Ejecuta git stash pop para deshacer lo que hiciste con git stash. ¿Bajo que escenario esto puedo ser útil?
  5. 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 corras git graph, obtengas el resultado de git log --all --graph --decorate --oneline.
  6. Puedes definir de manera general patrones a ignorar en ~/.gitignore_global luego correr git 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.
  7. 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.

Editar esta página.

Licensed under CC BY-NC-SA.