Archivo de la categoría: General

6 motivos por los que git no es un sistema de backup

Esta es una frase que escucho muy a menudo:

 “Yo tengo backup de mi repositorio git en github”.

Siempre que la escucho acabo teniendo la misma conversación con la persona que la ha dicho. Intento hacerle ver porqué github, bitbucket o cualquier otro servicio de hosting de repositorios no son una copia de seguridad de tu repositorio y cómo en determinadas circunstancias (muy habituales cuando trabajas en equipo) puedes perder información por usarlos para lo que no son.

Esto desde el punto de vista práctico. Si escarbamos un poco, lo normal es que detrás de esta frase se esconda un uso no del todo correcto de git o un desconocimiento de ciertas funcionalidades de la herramienta que espero corregir con este artículo.

En adelante me referiré a tu cuenta de github, bitbucket o cualquier otro sistema de hosting de repositorios git como servidor git.

1- Git es algo más que hacer un commit al final de nuestra jornada

El flujo más básico de uso de git que me he encontrado es el siguiente:

  • Llego a trabajar por la mañana y trabajo
  • Antes de irme hago un commit
  • Después del commit hago un push

Así, tienes en tu servidor git la última versión del código fuente. Si respetas este flujo, efectivamente tienes una copia de la última versión de tu código fuente en tu servidor. Ahora bien, no tienes una copia de tu repositorio, porque el repositorio del servidor al que haces push y el de tu máquina local son diferentes. ¡Por eso haces push!

Este forma de trabajo viola varias de las buenas prácticas de git:

  • No estás haciendo commits atómicos, pequeños y muy a menudo. Lo que significa que no estás pensando en cómo gestionar tu código sino en guardar la última versión en un «cajón de sastre» que es tu servidor
  • No estás trabajando con ramas locales para desarrollar funcionalidad
  • Probablemente, no tienes definido ningún flujo de trabajo que te permita gestionar cambios de contexto a la hora de trabajar

2- No estás guardando una copia de tus ramas locales

Las ramas locales, son eso, ramas locales. Las usas para muchas cosas, entre ellas para hacer pruebas, hacer refactorings del código o desarrollar funcionalidades en paralelo. Al no hacer push, de estas ramas, no tienes copia de los commits en tu servidor git.

Sé lo que estás pensando, basta con hacer push de todas las ramas y ya está. Bien, eso es correcto si estás tu solo con tu repositorio. Ahora bien, si estás trabajando en equipo y todos vosotros compartís el mismo repositorio git, la cosa cambia ¿Te imaginas a 5 personas todas subiendo sus ramas de prueba al servidor git para tener copia de seguridad? El caos está garantizado si no se organiza todo muy, muy, muy bien.

3- No estás guardando una copia del reflog

El reflog es un fichero de log que está en la carpeta .git de tu repositorio local donde se guarda un log de todos los cambios de referencias que ocurren en tu repositorio. Cada vez que cambias de rama, haces un merge, un rebase, un pull o un cherry-pick (entre otros) se guarda en el reflog lo que ha pasado.

Este fichero es el que te permite dar marcha atrás cuando la lías parda. Échale un vistazo a este hangout para un ejemplo sobre cómo se usa.

Tu reflog no está en tu servidor git. El servidor git tiene su propio reflog que no tiene porqué coincidir con el tuyo. De nuevo, si estás tu solo y haces push de todas tus ramas, el reflog del servidor se parecerá mucho al tuyo. Eso sí, en caso de desastre, tendrás que entrar al servidor a verlo o copiartelo a tu máquina local… oh, vaya, espera, que estás trabajando con github / bitbucket y no te dejan entrar a sus servidores a descargarte el reflog, desde luego qué tíos perros ¿no?.

Si estás trabajando en equipo, el reflog del servidor tiene todas las modificaciones de referencias hechas por tí y el resto del equipo. ¡Intentar encontrar algo en ese reflog es garantía de diversión durante horas!

4- No tienes backup de los hooks de tu repositorio git

Dentro de la carpeta .git/hooks de tu repositorio puedes poner hooks. Estos hooks no son más que scripts que se ejecutan antes o después de los comandos git y que te permiten interceptar la ejecución para hacer «cosas». Por ejemplo, el ‘pre-commit’ te permite manipular dinámicamente el mensaje del commit que se genera por defecto.

Cualquier hook que hayas puesto en tu máquina local en la carpeta .git/hooks, no tiene copia de seguridad en tu servidor git.

5- No tienes backup de tu configuración local

Dentro de tu repositorio tienes un fichero con la configuración local de tu repositorio: .git/config. En este fichero puedes guardar varias cosas, entre ellas el nombre y correo electrónico del autor de los commits, alisas de comandos de esos molones que te has bajado de internet y, en general, opciones de configuración específicas de ese repositorio en esa máquina.

Este fichero tampoco se sube a tu servidor git, así que en caso de desastre, lo pierdes y tienes que volver a generar la configuración del repositorio.

6- git permite sobreescribir la historia de tu repositorio

Si trabajas con más gente, en tu repositorio no vas a estar hurgando solo tú. Tendrás código de otras personas, harás merges o pull requests. Hay alguien que puede estar tocando el repositorio detrás de tí así que entre el momento que ocurre el desastre y «recuperas» el repositorio haciendo un clon, muchas cosas pueden haber cambiado.

Ponte en esta situación: te vas de vacaciones mientras tus compañeros trabajan en el repositorio. A la vuelta tu disco duro ha muerto, o durante el viaje te roban el portátil, así que clonas de nuevo el repositorio y te pones a trabajar… Lo que vas a clonar no es lo que tenías antes de irte, el repositorio contendrá todos los cambios que tus compañeros han hecho en tu ausencia.

Me dirás: “tío, no tienes ni idea. Una vez clonado hago un reset al commit de antes de irme de vacaciones y ya está”… Y yo te digo ¿Y no sabes que tus compañeros del alma pueden haberse cargado esos commits con un rebase, por ejemplo?. Recuerda: git, a diferencia de mercurial, permite sobreescribir la historia del repositorio así que ¡¡nada te garantiza que cuando vuelvas de tus vacaciones los commits sigan ahí!!

Conclusión

Según la wikipedia:

a backup, or the process of backing up, refers to the copying and archiving of 
computer data so it may be used to restore the original after a data loss event.

Si pierdes tu disco duro, y recuperas un repositorio haciendo un nuevo clon, no estás recuperando el repositorio como lo tenías antes del desastre así que según esta definición, no es una copia de seguridad. Además ¿Tú crees que Linus Torvalds hizo git para esto? ¿Para hacer backup? ¡eso ya lo tenía con los ficheros tar con los que empezó! Si solo lo usas para hacer copias de respaldo te estás perdiendo muchas cosas.

Git no es un sistema de copia de seguridad de nuestro código. Ahora bien, si se dan estas condiciones:

  1. Estás trabajando tú solo
  2. Haces push de todos los commits al repositorio remoto
  3. Todas las ramas que creas son remotas
  4. Tienes un repositorio que llamas scripts_y_configuracion en el que guardas los hooks, alias, pro-tips y configuraciones personalizadas de tus repositorios.

y eres consciente de lo que estás haciendo, puedes utilizar github como un backup (parcial) de tu código fuente, no de tu repositorio.

De igual manera, si trabajas en equipo y cada integrante tiene su propio repositorio, cada uno de vosotros podría usar git de esta manera. Aunque es un flujo muy básico que sirve para empezar a trabajar, no lo recomiendo en casi ningún caso: en seguida se te queda corto.

En mi caso particular, TimeMachine me ha salvado ya en varias ocasiones de problemas, ahorrándome mucho tiempo y trabajo. Aquí tienes mi consejo: usa una herramienta de backups para hacer backups y una herramienta de gestión de código para gestionar código.

Organizando las ramas en carpetas

Si en el momento de crear una rama utilizas como nombre una ruta, como por ejemplo “mi-carpeta-de-ramas/nombre-de-rama”, verás que en la carpeta .git/refs/heads de tu repositorio se creará una subcarpeta “mi-carpeta-de-ramas”. Dentro de la misma verás el fichero “nombre-de-rama” con la referencia. Es decir, git convierte la cadena con formato de ruta que pasamos como argumento en una estructura real de carpetas.

Esta forma de crear ramas soporta subcarpetas, es decir, si creamos una rama que se llame carpeta1/carpeta2/mirama:

$ git checkout -b carpeta1/carpeta2/mirama

git creará el fichero .git/refs/heads/carpeta1/carpeta2/mirama con la referencia.

¿Y cómo muestra SourceTree las ramas creadas de esta manera?

Organizando las ramas en carpetas

Esta forma de clasificar las ramas en carpetas es especialmente útil cuando trabajamos con ramas para hacer hotfixes, corregir bugs o desarrollar funcionalidades organizadas en historias de usuario.

Desde la interfaz de línea de comandos, así es como veríamos las ramas:

$ git branch -av
   carpeta1/carpeta2/mirama 4604003 Introduciendo el texto definitivo
   develop                  b7bd517 Merge branch ‘feature-H-1’ into develop
*  feature/H-2              4604003 Introduciendo el text definitive
   master                   87ee184 Initial commit

Si quisiésemos ver sólo las ramas feature, podemos usar la opción –list:

$ git branch --list feature*
feature/H-2

Para borrar la rama, usamos el comando git-branch con la ruta completa a la rama:

$ git branch -D carpeta1/carpeta2/mirama

Un truco muy sencillo que nos ayuda a organizar las ramas un poco mejor, especialmente útil si estáis usando SourceTree.

Opciones del comando git add

Un comando con el que todos estamos familiarizados es git add. Cuando nos iniciamos en el uso de git, este es de los que no faltan en las recetas que podemos en ver en cualquier tutorial. Es de los primeros que empezamos a utilizar cuando aprendemos git ¡En el libro de git de Scott Chacon aparece en la sección 2.2!

Es una pieza fundamental del flujo básico de git ya que es el comando que mueve al índice las modificaciones que hayamos realizado. El índice es un snapshot del contenido del área de trabajo en un momento dado. Este snapshot es el que posteriormente se convertirá en un commit.

La receta de git add

La forma más habitual de invocar el comando git-add es:

$ git add .

Este comando añade al índice cualquier fichero nuevo o que haya sido modificado:

$ git add .
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#       new file: app/assets/javascripts/home.js.coffee
#       new file: app/assets/stylesheets/home.css.scss
#       new file: app/controllers/home_controller.rb
#       new file: app/helpers/home_helper.rb
#       new file: app/views/home/index.html.erb
#       modified: config/database.yml
#       modified: config/routes.rb
#       new file: test/functional/home_controller_test.rb
#       new file: test/unit/helpers/home_helper_test.rb
#
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#       deleted: app/assets/images/rails.png

Sin embargo, los ficheros borrados no se actualizan en el índice. En este caso, tendríamos que ejecutar el comando git rm app/assets/images/rails.png:

$ git rm app/assets/images/rails.png
rm 'app/assets/images/rails.png'
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: app/assets/images/rails.png
# new file: app/assets/javascripts/home.js.coffee
# new file: app/assets/stylesheets/home.css.scss
# new file: app/controllers/home_controller.rb
# new file: app/helpers/home_helper.rb
# new file: app/views/home/index.html.erb
# modified: config/database.yml
# modified: config/routes.rb
# new file: test/functional/home_controller_test.rb
# new file: test/unit/helpers/home_helper_test.rb
#

Las opciones -u y -A del comando git-add añaden al índice los ficheros eliminados, aunque gestionan de manera diferente los ficheros nuevos. Veamoslo usando este mismo ejemplo.

git-add -u

Partimos de la siguiente área de trabajo:

$ git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/
no changes added to commit (use "git add" and/or "git commit -a")

La opción -u sólo añade al índice aquellos ficheros que ya estén siendo monitorizados por git, así que en este caso, únicamente se subirán al índice rails.png y routes.rb:

$ git add -u
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

La opción -u acepta un patrón de búsqueda. Si este patrón está vacío, el resultado es que se actualicen todos los ficheros borrados o modificados en el área de trabajo. Si el patrón no está vacío, sólo se actualizarán en el índice los ficheros que encajen con el patrón. En el siguiente ejemplo, ejecutamos el mismo comando para añadir únicamente los ficheros de la carpeta config:

$ git add -u config/*
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    modified: config/routes.rb
#
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

git-add -a

Esta opción funciona como la opción -u añadiendo también a la búsqueda los ficheros del área de trabajo. El resultado es que los ficheros que no estén siendo monitorizados también se añadirán al índice.

Partimos de la siguiente situación:

$ git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/
no changes added to commit (use "git add" and/or "git commit -a")

Al ejecutar el comando git add -A, se añadirán al índice los ficheros borrados, los modificados y los nuevos:

$ git add -A
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    new file: app/assets/javascripts/home.js.coffee
#    new file: app/assets/stylesheets/home.css.scss
#    new file: app/controllers/home_controller.rb
#    new file: app/helpers/home_helper.rb
#    new file: app/views/home/index.html.erb
#    modified: config/routes.rb
#    new file: test/functional/home_controller_test.rb
#    new file: test/unit/helpers/home_helper_test.rb

Al igual que la opción -u, esta opción también acepta un patrón. En el siguiente ejemplo, actualizamos el índice con las modificaciones, ficheros nuevos y ficheros borrados sólo de la carpeta app/assets:

$ git add -A app/assets
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    new file: app/assets/javascripts/home.js.coffee
#    new file: app/assets/stylesheets/home.css.scss
#
# 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: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

git-add -n

Esta opción es muy práctica ya que nos mostrará en pantalla lo que el comando git-add haría sin actualizar el índice. Continuando con el comando anterior (git add -A app/assets) si quisiésemos ver qué ficheros se actualizarán en el índice sin realmente actualizarlos, haríamos los siguiente:

$ git add -n -A app/assets
remove 'app/assets/images/rails.png'
add 'app/assets/javascripts/home.js.coffee'
add 'app/assets/stylesheets/home.css.scss'

Resumen

Hemos visto tres opciones del comando git-add: -u, -A y -n. Las dos primeras nos permiten actualizar en el índice directamente ficheros borrados sin necesidad de ejecutar el comando git-rm. La última opción, -n, simula lo que hará el comando git-add sin realmente actualizar el índice. Los tres son opciones útiles que de haberlas conocido cuando empecé con git hace años me habrían ahorrado unos cuantos comandos.

Referencias

Ignorando ficheros en git – Parte IV: Más patrones

En la entrega anterior estuvimos analizando los patrones básicos que podemos utilizar para ignorar ficheros a través de .gitignore, .git/info/exclude y .gitignore_local. En esta entrega veremos cómo git utiliza los patrones de la shell dentro de estos ficheros.

Todos los patrones que usa git

Hagamos un pequeño repaso de lo que ya sabemos:

  • Las líneas en blanco no se consideran un patrón. Se usan para hacer los ficheros más legibles
  • Si la linea comienza con #, se considera un comentario
  • El prefijo ! sirve para negar un patrón
  • Si el patrón termina en / sólo se aplicará a carpetas, es decir, un patrón «web/» ignorará la carpeta web/ pero no ignorará un fichero que se llame web

Hasta aquí lo que ya habíamos visto ¿qué nos falta?

  • Si el patrón no termina en /, git lo interpreta como un «shell glob pattern«. Internamente git llama a la función fnmatch con el flag FNM_PATHNAME: esto significa que ni el * ni el ? incluyen el carácter /. En seguida veremos ejemplos.
  • Una / al principio del patrón indica el inicio de una ruta relativa a la carpeta en la que se encuentre el fichero .gitignore. Una línea del tipo /config.php ignorará el fichero config.php pero no ignorará web/config.php

Shell globs en git

Los ficheros .gitignore, .git/info/exclude y .gitignore_local permiten incluir patrones de tipo shell. Si habéis trabajado con cualquier shell de Unix o el CMD de windows, estaréis familiarizados con el funcionamiento de estos patrones. En caso contrario, aquí tenéis las reglas que se siguen.

En una expresión que contenga el símbolo «?», éste se corresponde con uno y sólo un carácter

Ejemplos:

# Ignora los ficheros bg-01.jpg bg-lt.jpg
#
# No ignora los ficheros bg-1.jpg y bg-001.jpg
#
bg-??.jpg

Este patrón ignorará los ficheros cuyos nombres empiezan por «bg-«, van seguidos exactamente de dos caracteres, y luego van seguidos de «.jpg». Por eso el fichero bg-1.jpg no se ignora, porque entre «bg-» y «.jpg» no hay dos caracteres, sólo uno. Este ejemplo ilustra que el símbolo «?» no indica un caracter opcional.

En una expresión que contenga el símbolo » * «, éste se corresponde con cualquier número de caracteres

Ejemplos:

# Ignora todos los ficheros acabados en .nib
#
# Ignora tanto MainWindow.nib y builds/MainWindow.nib
*.nib

#Ignora todos los ficheros que empiecen por "bg-" y acaben en ".jpg"
bg-*.jpg

En el primer ejemplo, estamos ignorando cualquier fichero que acabe en .nib, independientemente de la carpeta en la que se encuentre. En el segundo ejemplo, estamos ignorando cualquier fichero que empiece por «bg-» y acabe por «.jpg». Una pregunta: ¿se ignorará el fichero «bg-.jpg» usando este patrón?. La respuesta la tenéis un poco más abajo.


En una expresión que contenga corchetes [ ], éstos se corresponden con uno y sólo un caracter de los que están incluidos en los corchetes.

Ejemplos:

# Ignora los ficheros *.o y *.a
*.[ao]

# Ignora los ficheros .nib y .xib
*.[xn]ib

Estos patrones no ignoran los ficheros liberia.so o los ficheros mainwindow.ib. Es necesario que alguno de los caracteres que están entre corchetes aparezcan en el fichero.

Si se necesita utilizar los caracteres ? * [ ] en un patrón, estos deberán escaparse con \

Ejemplo:

# Ignorando ficheros que contienen [ y ] en el nombre
#
# Este patrón ignorará los ficheros
# cache/cache-[hG7].cache y cache/cache-[009].cache
cache/cache-\[???\].cache

Shell globs y carpetas

¿Qué significa que git utilice el flag FNM_PATHNAME al llamar a la función fnmatch? Que los símbolos * ? y [] no incluyen el caracter » / » de separación de directorios.

# El uso de FNM_PATHNAME hace que el siguiente patrón ignore los ficheros
# web/fichero.html, pero no ignore el fichero web/admin/fichero.html
#
# Como el " * " no incluye " / ", el fichero web/admin/fichero.html no 
# encaja con el patrón
web/*.html

¿Qué ocurre si necesitamos justo el efecto contrario? es decir, queremos ignorar todos los ficheros .html que están en las subcarpetas de web pero no ignorar los ficheros .html que están en web. El patrón sería el siguiente:

# Ignora los ficheros .html que están en subcarpetas de la carpeta web
# No ignora los ficheros web/*.html
#
# Con este patrón, el fichero web/fichero.html se incluirá en el repositorio
# mientras que ficheros con web/admin/fichero.html o web/images/index.html se 
# ignorarán
web/**/*.html

Otra pregunta: Este patrón ¿ignora el fichero web/admin/subcarpeta/fichero.txt?

Rutas relativas

Un patrón que comienza por » / «, indica que para decidir si el fichero se ignora o no, se utiliza una ruta relativa al fichero .gitignore:

# Ignora los fichero *.c en la carpeta raiz del proyecto: memory.c
# No ignora los ficheros en subcarpetas como modules/cache.c
/*.c

Conclusiones

En esta entrega hemos visto cómo interpreta git los patrones glob shell y los patrones a rutas relativas. En la siguiente entrega veremos cómo podemos usar múltiples ficheros .gitignore. Estos nos ayudarán a salvar las limitaciones que tenemos a la hora de trabajar con múltiples subcarpetas.

Respuestas:

  1. Efectivamente, el patrón «bg-*.jpg» ignora que fichero «bg-.jpg». Eso es porque a diferencia de símobol «?», el símbolo «*» se corresponde con cualquier número de caracteres, incluido cero caracteres.
  2. El patrón web/**/*.html no ignora el fichero web/admin/subcarpeta/fichero.txt. Dado que se usa el parámetro FNM_PATHNAME, el » ** » no incluye el caracter » / » y por lo tanto este patrón sólo incluye las carpetas que son hijos de web, no incluye los nietos y niveles de más profundidad en la jerarquía de carpetas.

Ignorando ficheros en git – Parte III: Patrones

En la segunda entrega de esta serie aprendimos en qué orden se aplican los patrones para ignorar cambios en ciertos ficheros del repositorio. En esta tercera entrega veremos los patrones y reglas que git utiliza para interpretar los ficheros it ignore.

Comentarios y líneas en blanco

Podemos hacer nuestro fichero más legible si incluimos comentarios y separadores:

  • Cualquier línea que comience por # es un comentario
  • Una línea en blanco no se considera un patrón por parte de git, por lo que puede ser usada como un separador

Sé considerado con el resto de los miembros del equipo y comenta adecuadamente cada uno de los patrones que utilices. Indica el porqué de ese patrón de forma que el resto de compañeros o colaboradores sepan porqué has decidido en un momento dado ignorar ciertos ficheros. Y una cosa más, no todo el mundo usa las mismas herramientas que tú ni tiene tu mismo nivel de conocimientos en determinadas materias así que piensa un poco en los demás y ayúdales:

# Jetbrains IDEs: PHPStorm / RubyMine  
# (No todo el mundo tiene que saber que los ficheros .idea son de un 
# entorno de desarrollo)
.idea 

# Ficheros de documentación sphynx. Puedes generar tu copia local de la doc
# en /doc/build utilizando los scripts sphynx en /doc
/doc/build/

Negación

Utilizando el carácter podemos invertir un patrón ¿Qué significa invertir un patrón? Veamoslo con un ejemplo:

# Incluir expresamente en el repositorio todos los ficheros con extensión php
!*.php

¿Porqué podemos necesitar este tipo de patrones? Imagina que estás trabajando en un proyecto en PHP; como te puedes imaginar, si tu repositorio sólo tuviese estas dos líneas en los ficheros git/info/exclude, .ignore y .gitignore_local podríamos quitarlas ya que el efecto sería el mismo. Ahora bien ¿para qué podemos necesitar un patrón como este?:

  • Tal y como vimos en la anterior entrega, para invertir patrones de menor prioridad
  • Crear excepciones a patrones muy restrictivos

Veamos un ejemplo del segundo caso. Siguiendo con el ejemplo de un proyecto en PHP, podemos eliminar la línea anterior ya que no hace falta. Ahora bien ¿qué hacemos con los ficheros html?:

# Ignorar todos los ficheros .html...
*.html

 #... excepto en la carpeta web
!web/*.html

En este caso, cualquier fichero con extensión .html en cualquier carpeta se ignorará, excepto los ficheros que estén en la carpeta web. Cuidado porque ficheros .html que estén en una subcarpeta de web, como por ejemplo web/archive, también se ignorarán. Más adelante veremos porqué.

Ignorando carpetas enteras

Si un patrón acaba con una barra /, git ignorará cualquier fichero dentro de esa carpeta:

# Ignorar el contenido de las carpetas log y cache
cache/
log/

Git ignorará cualquier fichero que esté dentro de cache/ o log/ o en cualquier subcarpeta que cuelgue de ellas.

Este patrón no se lleva muy bien con la negación ¿Por qué decimos esto? Imagina que quieres ignorar el contenido de la carpeta web entera (incluyendo todas las subcarpetas que cuelgan de ella) excepto el fichero web/index.php. Con lo que sabemos hasta ahora, intentaríamos una combinación de patrones como la siguiente:

#Esto no funciona:
# git ignorará también el fichero index.php
web/
!web/index.php

Desafortunadamente esto no funciona y git ignorará fichero index.php. Veremos en una futura entrega cómo resolver este problema.

Una última nota sobre el uso de /. Si usamos el siguiente patrón

#Ignorar el contenido de la carpeta web
web/

y web no es una carpeta sino un enlace simbólico o un fichero regular:

total 16
-rw-r--r-- 1 aalba staff 0 22 dic 14:42 Alumno.php
-rw-r--r-- 1 aalba staff 0 22 dic 14:41 Curso.php
-rw-r--r-- 1 aalba staff 0 22 dic 14:42 Licencia.php
-rw-r--r-- 1 aalba staff 339 7 dic 16:39 README.md
drwxr-xr-x 3 aalba staff 238 29 dic 08:29 builds
lrwxr-xr-x 1 aalba staff 11 29 dic 08:29 web -> builds/html

¡el fichero no se ignora!

$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# web
  ^^^ -> el enlace simbólico o fichero regular no se ignora ya que el patrón
         web/ sólo se aplica a carpetas

Esto es todo por hoy, en la siguiente entrega de la serie veremos los patrones shell que podemos utilizar.

Ignorando ficheros en git – Parte II: Prioridades

En la entrega anterior de esta serie, vimos las cuatro formas en las que podemos ignorar ciertos ficheros de forma que no se incluyan en nuestros repositorios.

En este artículo vamos a ver en qué orden se aplican estas cuatro maneras de leer los patrones y qué ocurre cuando en ellos tenemos patrones contradictorios.

Patrones contradictorios

Supongamos que tenemos los siguientes patrones en los ficheros exclude y .gitignore:

#.git/info/exclude
*.phar
*.p
#.gitignore
!*.php

Estos patrones son contradictorios ya que en el primer caso estamos ignorando los ficheros con extensión .php y en el segundo estamos diciendo que no se ignoren (ese es el significado del signo de exclamación «!» inicial en el patrón).

¿Qué va a pasar? Dado que el fichero .gitignore tiene prioridad sobre el fichero .git/info/exclude, los ficheros se incluirán en el repositorio. Si abrimos SourceTree en nuestro proyecto veremos lo siguiente:

Reglas contradictorias I

La regla es que git revisa por orden la línea de comandos, el fichero .gitignore, el fichero exclude y  el fichero indicado por core.excludesfile. El primero último de ellos que contenga un patrón que explícitamente ignore o no un fichero dado, es el que aplica.

¿Qué pasa si dentro de un mismo fichero existen reglas contradictorias? Imagínate un fichero .gitignore como el siguiente:

# .gitignore
!*.php

# Otros patrones
....

# Un compañero de equipo añade días después la siguiente regla
*.php

Git utiliza la última regla, por lo que los ficheros se ignoran y no podríamos añadirlos al repositorio:Patrones contradictorios en .gitignore

.git/info/exclude vs .gitignore vs core.excludesfile

Entendido cómo funciona el orden de prioridades ¿qué patrones ponemos en cada una de las tres opciones que nos da git?

  • En .gitignore pondremos aquellos patrones que queremos que sean iguales en todos los clones que existen del repositorio. Todos los miembros del equipo los comparten. Es por ello que este fichero debe incluirse en el repositorio. Este es un buen lugar para ignorar ficheros de log y cache, configuraciones, claves privadas, etc.
  • En .git/info/exclude se incluyen patrones que se van a aplicar sólo a nuestra copia local y no se van a distribuir al resto de miembros del equipo. Este fichero es útil cuando por ejemplo, creas una rama local para desarrollar una determinada característica de la aplicación, y necesitas ignorar ciertos ficheros sin que el resto del equipo se vea afectado por ello.
  • En el fichero indicado por la variable core.excludesfile, incluimos aquellos patrones que queremos ignorar de forma global en todos los repositorios. Este es un lugar para incluir los ficheros del proyecto generados por nuestro entorno de desarrollo. Por ejemplo, si estamos trabajando con PHPStorm o RubyMine, incluiríamos el siguiente patrón:
.idea/

Para saber qué fichero es el que se indica en la variable core.excludesfile, usamos el comando git-config:

# git config --get core.excludesfile
/home/myuser/.gitignore_global

Si queremos modificar la ruta al fichero utilizamos la opción –global del comando git-config:

# git config --global core.excludesfile /home/myser/myconfig/mygitignore
# git config --get core.excludesfile
/home/myuser/myconfig/mygitignore

Por una cuestión de comodidad, denominaremos de ahora en adelante este fichero como .gitignore_global, entendiendo que modificando esta opción de configuración, este fichero puede tener otro nombre y estar en una ruta diferente.

¿Conviene incluir en el repositorio los ficheros de configuración del entorno de desarrollo?

Una conversación que he tenido ya en varias ocasiones con diferentes personas es si conviene o no incluir en el fichero .gitignore los ficheros de configuración del entorno de desarrollo, ya sea Eclipse, Netbeans, PHPStorm o cualquier otro.

Mi política es la siguiente:

  • Si estás trabajando en un proyecto de software libre en el que puede participar cualquier persona, tendrías que incluir en el fichero todos y cada uno de los posibles IDE que los colaboradores utilicen. Esto complica el fichero innecesariamente y dificulta su mantenimiento. ¿Cuántos IDE o editores de texto diferentes utilizan todos los colaboradores de un proyecto? En este caso, lo mejor es que cada colaborador se responsabilice de configurar su fichero .git/info/exclude o .gitignore_global adecuadamente.
  • Si estás trabajando en un equipo en el que todos sus miembros utilizan el mismo entorno de desarrollo, entonces sí que incluyo los ficheros del IDE en el fichero .gitignore. El motivo es que no usamos más de dos o tres IDE diferentes que se pueden gestionar fácilmente con 3 o 4 patrones en el fichero .gitignore.

De cara a trabajar con gente novata en git dentro del equipo, la mejor opción es la segunda. El motivo es que no necesitan hacer nada una vez clonan el repositorio para ignorar los ficheros del entorno de desarrollo. Así les facilitamos un poco el trabajo al principio.

Esto es todo por hoy. En la siguiente entrega veremos los diferentes patrones que podemos utilizar para ignorar ficheros.

Ignorando ficheros en git – Parte I: Formas de ignorar ficheros

En esta serie vamos a profundizar un poco más en el fichero .gitignore, para qué sirve y cómo podemos usarlo, empezando por cómo decide git si ignorar un fichero o no.

En cualquier proyecto, existen ficheros que no deben incluirse en el repositorio. Por nombrar algunos de ellos:

  • Ficheros de log
  • Cachés
  • Ficheros que contienen claves privadas (App keys y App secrets) para acceder a las API o servicios web
  • Ficheros de configuración con contraseñas de bases de datos
  • Ficheros binarios compilados
  • Ficheros de configuración del entorno de desarrollo

Cuando empiezas a utilizar git, tardas muy poco en preguntarte cómo puedes evitar subir estos ficheros al repositorio. Si buscas la respuesta en google, encontrarás en todos sitios que lo que tienes que hacer es crear un fichero .gitignore que hay que subir al repositorio o crear un fichero .git/info/exclude para poner las reglas locales. Tú lo creas y entonces empiezan las preguntas: ¿Y cuál de los dos tiene preferencia? ¿Y porqué lo tengo que incluir en el repositorio?  ¿Y porqué subo uno sí y otro no? ¿Un fichero, no son propiedades como en subversion? ¿Y si yo quiero usar un .gitignore y el resto del mundo otro?

Para responder a estas preguntas debemos platearnos la pregunta adecuada que es…

¿Cómo decide git qué ficheros incluir o no en el repositorio?

La respuesta a esta pregunta está en la página de manual de gitignore. Por orden de prioridad, git busca patrones para excluir ficheros en los siguientes lugares:

  1. En la línea de comandos: ciertos comandos, como por ejemplo git ls-files o git read-tree, permiten excluir ficheros a través de argumentos. Estos patrones tendrán la máxima prioridad.
  2. En los ficheros .gitignore
  3. En el fichero $GIT_DIR/info/exclude
  4. En el fichero definido en la variable de configuración core.excludesfile
Todos los ficheros mencionados arriba incluyen en su interior un conjunto de patrones de ficheros que se van a ignorar. Cada patrón va en una línea. Por ejemplo, si incluimos la siguiente línea en alguno de estos ficheros:
*.jar

git ignorará cualquier fichero con extensión .jar en cualquier directorio.

¿Qué quiere decir que git ignora ficheros?

Veamos qué ocurre cuando ignoramos un fichero en un repositorio. Imagina que acabas de empaquetar tu aplicación PHP en un fichero .phar en la carpeta builds. Esto es lo que veríamos en SourceTree:Fichero phar marcado como untracked

Si no hacemos nada, tendremos que acordarnos siempre de no incluir este fichero al hacer commit. Esto es peligroso, ya que si alguna vez tenemos muchos ficheros pendientes de añadir o excluir del commit, puede que se nos «cuele» mynewapp.phar sin darnos cuenta.

Para evitar el estar pendiente de subir o no estos ficheros, usamos un editor de texto y abrimos el fichero .git/info/exclude de nuestro repositorio. Añadimos el siguiente patrón al final del fichero:

*.phar

Cuando volvamos al SourceTree, veremos que el fichero ya no está:SourceTree después de ignorar *.phar

Ya está, ya estamos tranquilos: no añadiremos por accidente el fichero al repositorio.

Seguro que os estaréis preguntando: «¿Porqué no lo ha puesto en .gitignore, como dice nuestro gran San Google?». A lo largo de la serie veremos si poner este patrón en el fichero .git/info/exclude es la mejor opción o conviene ponerlo en otro lugar. De momento lo dejaremos así.

Esto ha sido todo por hoy. En la siguiente entrega veremos qué patrones incluir en cada uno de los tres métodos que podemos utilizar y cómo trabajar con el orden de prioridades descrito anteriormente.

¿Qué es aprendegit.com?

Imaginaos la situación, voy a contratar a una nueva persona o estoy buscando un colaborador freelance con el que hacer un determinado proyecto. Durante la entrevista, surge la pregunta del millón: ¿Usas un sistema de control de versiones, como subversion, git o mercurial? La respuesta siempre ha estado en alguna de estas tres categorías:

  • No sé qué es eso
  • Lo he visto/escuchado en un blog, conferencia, screencast o similar
  • Uso el sistema XXXX aunque no he pasado de hacer commits y alguna vez crear una rama

En muy pocas ocasiones me he encontrado con profesionales que conociesen en profundidad estas herramientas. Hasta hoy, mi reacción siempre ha sido la misma: ¿Cómo puedo todavía, a punto de entrar en 2013, encontrarme con profesionales que no utilizan un sistema de control de código? Tomando cervezas, BitterKases, CocaColas o cafés con colegas de profesión (http://www.fmvalencia.com o http://nscoder-mad.tumblr.com) siempre me he quejado de estas carencias y limitaciones, he echado pestes y repetido hasta la saciedad el tan usado «Esto no puede ser».

Pues bien, como dice un amigo mío: «Si no eres parte de la solución entonces eres parte del problema»… y he decidido dejar de ser parte del problema.

¿Por qué? Porque quiero contribuir a cambiar esta situación, porque algo no estamos haciendo bien si ya casi en el 2013 no sabemos qué es un pull request, resolver conflictos de código o gestionar el producto de nuestro trabajo: el código fuente. ¿No eres consciente de que a día de hoy cualquier proyecto que se precie tiene su repositorio en github o bitbucket? ¡No podemos quedarnos atrás!

¿Cómo? A través de este blog, charlas, presentaciones, cursos y, por supuesto, acompañados de cervezas o cafés, compartiré con vosotros mi experiencia en el uso de git en la gestión de equipos de trabajo.

¿Qué hacemos desde aprendegit.com?

  • Compartir información y experiencias relacionadas con git
  • Actuar como punto de encuentro entre profesionales interesados en esta tecnología
  • Impartir cursos especializados a profesionales independientes, así como a equipos de desarrollo y empresas para ayudarles a implantar git en sus procesos de trabajo

¡Happy gitting!