Un sistema de control de versiones (SCV desde ahora), es un software que nos permite navegar a través de distintas versiones de un conjunto de archivos. Aplicado al desarrollo de software, es una herramienta extremadamente útil: nos servirá para poder realizar modificaciones al proyecto sin afectar a versiones que ya fueran funcionales. Una vez que las modificaciones son estables, y probadas, podrán incorporarse a las versiones guardadas del proyecto.
El SCV más utilizado actualmente es git. Si bien es una herramienta muy completa (y compleja), las bases son muy simples. En este primer artículo hablaremos de su flujo básico.
Git es un SCV distribuido: cada copia de un repositorio es tan válida como cualquier otra. Esto quiere decir que si perdiésemos o se corrompiera la versión que se encuentra en el servidor “principal” (GitHub, por ejemplo) aún podemos recuperar el proyecto con cualquiera de las copias de trabajo que se encuentren en nuestras máquinas. Esta característica le brinda robustez como SCV.
El desarrollo de esta herramienta es de código abierto, por lo que podemos contribuir a hacerla más eficiente, completa o simple. Además, podemos auditar el código fuente si tuviésemos esa inquietud. Que sea de código abierto también implica que miles de otros programadores alrededor del mundo hacen eso mismo, y cooperan para tener la mejor herramienta posible.
Como herramienta, git está pensado para ser veloz y eficiente. Consideremos que pasado un tiempo en la vida de un proyecto, es probable que la cantidad de commits sea grande, haya muchos branches y el flujo de trabajo se haya bifurcado e integrado incontables veces. Aún en esas situaciones, navegar por los distintos estados de los archivos es una tarea que git resuelve rápidamente: no notamos una baja en el rendimiento.
Estas características se aprecian aún más cuando venimos de otros SCV, que no poseen ninguna de ellas. Sin embargo, y como es probable que cada vez menos programadores hayan conocido otra herramienta antes de conocer git, nos centraremos en sus cualidades propias.
La cantidad de recursos para aprender a utilizar git es abrumadora. De hecho, yo mismo tengo un apunte que trata un poco más en profundidad el uso de git, y una charla en la RubyConf Argentina 2014, donde hablo de distintos flujos de trabajo con git.
Es por ello que voy a realizar un resumen operativo aquí. La demostración paso a paso podrán verla en el video que acompaña a esta nota, con algunas explicaciones extra contextuales.
El primer comando a utilizar, y se realiza por única vez. Estando posicionados en el directorio del proyecto, tipeamos git init
:
$ git init
Initialized empty Git repository in /current/path/.git/
Vemos que se ha inicializado un repositorio vacío en el directorio actual.
Con git status
podemos visualizar el estado actual del repositorio. Son varios los estados posibles, pero podríamos obtener los siguientes escenarios:
Un estado en el cual vemos que no hay trabajo realizado, por lo tanto no hay nada que registrar con un commit.
$ git status
On branch master
nothing to commit, working tree clean
O quizás un archivo creado, no registrado, con cambios para guardar. Vemos el untracked, que nos brinda la pista.
$ git status
On branch master
Initial commit
Untracked files:
(use "git add <file>..." to include in what will be committed)
something.txt
nothing added to commit but untracked files present (use "git add" to track)
Si hubiera un archivo registrado con cambios, este sería el mensaje:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: something.txt
no changes added to commit (use "git add" and/or "git commit -a")
Finalmente, si el archivo estuviera agregado para commitear, ya registrado y con cambios:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: something.txt
En fin, son varios los estados posibles. No debemos dejar de percibir que cualquiera sea el estado, el comando nos brindará el contexto necesario para comprender en qué situación está nuestro repositorio. Leer bien el estado nos garantizará el éxito de los otros comandos.
Los comandos git add
y git commit
serán necesarios cada vez que querramos registrar un cambio en nuestro repositorio:
$ git add something.txt
$ git commit -m "Some changes"
[master 5dbd560] Some changes
1 file changed, 1 insertion(+)
Vemos que el comando git add
no arroja resultados cuando funciona correctamente.
Nota: Se pueden agregar todos los archivos, con
git add .
, o individualmente, congit add filename.txt
. También hay un modo avanzado e interactivo, que sirve para comprender mejor los cambios puntuales:git add -i
.
El comando git commit
requiere de un mensaje significativo que nos permita analizar qué sucedió entre una versión registrada del repositorio y otra. Idealmente no sería muy largo, pero debe tener la información suficiente para obtener el contexto: si necesitásemos más espacio para describir el commit, podemos realizar un mensaje largo con el editor por defecto, si no lo enviamos junto al comando: git commit
(sin -m
).
El comando necesario para crear una nueva rama, será git checkout -b new_branch
. Éste, creará y cambiará a la rama llamada new_branch
.
Las ramas nos permitirán diverger en el desarrollo, sin modificar la rama anterior:
$ git checkout -b new_branch
Switched to a new branch 'new_branch'
Podemos crear tantas ramas como creamos conveniente, siempre y cuando recordemos eliminarlas al terminar de trabajar en ellas. Recordemos que no podemos eliminar una rama estando situados en ella, por lo que debemos movernos hacia otra con el comando git checkout
primero, para eliminarla con git branch -d branch_name
luego:
$ git checkout master
Switched to branch 'master'
$ git branch -d new_branch
Deleted branch new_branch (was 5dbd560).
Finalmente, y para resumir el último comando de esta breve nota, realizaremos una mezcla simple. En el video que acompaña esta nota resuelvo una mezcla conflictuada, recomiendo que lo vean.
Toda mezcla debe tener al menos dos ramas, y se hace con el comando git merge
:
$ git checkout master
Switched to branch 'master'
$ git merge date_branch
Merge made by the 'recursive' strategy.
new_file.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 new_file.txt
Este comando funciona trayendo a la rama actual, el contenido de la rama parámetro: si nos situamos en master
, por ejemplo, y ejecutamos git merge date_branch
, traerá el contenido de dicha rama a master
. En este caso, y dado que los cambios no conflictúan entre sí, la mezcla se realiza de un modo recursivo y automático.
En algún caso, y cuando la nueva rama avanza sin existir divergencia con la rama original, la mezcla se realiza por “Fast forward”. Este modo implica que sólamente se corrió la rama actual al final de la rama que deseaba mezclarse:
$ git merge another_branch
Updating 5dbd560..0f58a19
Fast-forward
another_file.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 another_file.txt
Hemos utilizado los siguientes comandos, tanto en el video como en esta nota:
git init
, para inicializar un repositorio nuevo en el directorio actual.git status
, para ver el estado de nuestra área de trabajo. Nos permite ver qué se está por commitear, o qué falta agregar.git add filenames
, para agregar uno o más archivos al próximo commit.git commit -m "comment"
, para realizar efectivamente un commit con todo lo guardado anteriormente.git checkout -b branch_name
, para crear una nueva rama desde el punto actual.git checkout branch_name
, para pasar de la rama actual hacia otra.git branch -D branch_name
, para eliminar una rama. Es necesario estar posicionado en otra rama.git merge branch_name
, para mezclar el contenido de la rama especificada dentro de la rama actual.El uso de git involucra muchos más comandos que escapan a los objetivos de esta breve introducción. Sin embargo, con esta base puede comenzar a trabajarse y a sumar experiencia con la herramienta.
La más evidente es que al comenzar a utilizar un SCV no perderemos más versiones de nuestros archivos: cada una que querramos conservar, lo podremos hacer. No importa cuántas sean, siempre se puede guardar una versión más, una intermedia, un trabajo en progreso, entre otras.
Derivada de la conservación de versiones, es que podemos recuperar versiones anteriores como si tuvieramos un “deshacer” que dura toda la vida del proyecto (y no un par de minutos).
Adicionalmente, al tener tanta una herramienta que facilita el trabajo con variadas versiones del software, podemos crear ramas experimentales y potencialmente descartables, para probar alguna funcionalidad sin temer por la integridad del proyecto.
La incorporación de una nueva herramienta involucra una curva de aprendizaje. Si bien los primeros pasos con git son simples, las funcionalidades más interesantes necesitan de un conocimiento más profundo de la herramienta.
Los problemas comunes derivados de la utilización de git, y sobre todo cuando se comienza a trabajar en equipo, suelen tener resoluciones que potencialmente pueden hacernos perder el trabajo no guardado. Si bien todo trabajo commiteado está seguro, aquel último que hayamos realizado no estará a salvo durante unos momentos. Sin entrar en pánico se deben resolver rebase a medias, conflictos de merge, bisect inconclusos, entre otros.
Cuando trabajamos con proyectos con una cierta historia, es probable que el repositorio local comience a ocupar un espacio considerable. Si bien hay soluciones de compromiso, siempre terminamos perdiendo la capacidad de volver a versiones más antiguas (aunque, para ser honesto, a veces no es necesario volver tanto hacia atrás).