20 October 2013

Un par de commits de casi 7 millones de líneas de código dieron inicio a Git, el ''manejador de contenidos del infierno ''

 commit e83c5163316f89bfbde7d9ab23ca2e25604af29 
 Author: Linus Torvalds <torvalds@ppc970.osdl.org> 
 Date: Thu Apr 7 15:13:13 2005 -0700

 Initial revision of "git", the information manager from hell

 commit 1da177e4c3f41524e886b7f1b8a0c1fc7321cac2 
 Author: Linus Torvalds <torvalds@ppc970.osdl.org> 
 Date: Sat Apr 16 15:20:36 2005 -0700

 Linux-2.6.12-rc2

 Initial git repository build. I'm not bothering with the full history, even though we have 
 it. We can create a separate "historical" git archive of that later if we want to, and 
 in the meantime it's about 3.2GB when imported into git - space that would just make the 
 early git days unnecessarily complicated, when we dont have a lot of good infrastructure 
 for it.

 Let it rip!

Después de la famosa controversia BitKeeper, Linus Torvalds decide crear su propio sistema de control de versiones, el resultado de ese ''stretch'' es Git.

Entre los aspectos más importantes en su diseño están:

Gratuito

Sip, cero pesos, cero centavos, otro ejemplo más de como el trabajo colaborativo le lleva ventaja a las licencias y patentes.

Facilitar el desarrollo distribuido

Hacer eficiente el desarrollo en paralelo, esto es, distintos equipos con repositorios independientes pueden trabajar en sus propias versiones sin un repositorio central. Cuando el momento sea apropiado, sincronizar con estos remotos, comparar y decidir que commits se quiere usar.

Rapidez y eficiencia

Cortos tiempos de transferencia, operaciones mucho mas rápidas y eficientes. Contar con un repositorio completo en todo momento para no depender de un servidor remoto para las operaciones cotidianas.

Diseño

Crear un diseño limpio y eficiente que aproveche al máximo todas las fortalezas de el sistema de archivos de Unix/Linux.

Como resultado de esto, Git no sólo es muy apropiado para escenarios en donde existen cientos de desarrolladores, trabajando en sus propios repos centrales o "Hubs". Funciona perfectamente como sistema local de control de cambios, sin necesidad de sincronizar nada en absoluto con ningún repositorio remoto si esto no es necesario. La base de datos del repositorio estará contenida en una sola carpeta dentro del proyecto: .git, usando una de las herramientas más poderosas del sistema operativo, su sistema de archivos.

Yo no utilizo sistema de control de versiones porque casi siempre trabajo solo. -- programador anónimo

Este argumento se oye mas a menudo de lo que se debería. Un sistema de control de versiones no solamente es útil en un entorno de trabajo colaborativo, diría que Git está precisamente diseñado para hacer la vida de cualquier programador "solo" mas sencilla.

Comenzamos a escribir una solución sencilla para un problema. Conforme avanzamos, se nos ocurren casos extremos o mejores formas de hacer alguna parte del algoritmo, pero queremos llegar a una versión inicial que funcione. En el mundo no-repo se suele sacar copias de archivos o carpetas completas, de cierta forma estamos versionando intuitivamente pero con las herramientas de la edad de piedra. Si la experimentación toma un par de "versiones", el resultado será casi siempre un montón de carpetas con una nomenclatura confusa y un desarrollador confundido perdiendo tiempo buscando la versión X de la función Y.

En el mundo de los sistemas de control de versiones centralizados, donde cada commit hace parte de un repo central público, este escenario es hasta cierto punto comprensible. Después de todo, nadie quiere incluir versiones "a medias" de un componente.

Sin embargo, con Git nada es realmente público hasta que no se hace un git push. Un commit en Git es como una fotografía del sistema de archivos del proyecto en un momento particular del tiempo, pero estas fotografías tienen nombre, descripción, fecha, autor y las diferencias entre commits pueden verse fácilmente. Cuando todo está listo, es posible agregar todos los commits en uno solo y compartir con el mundo si es el caso.

Este es la primera parte de una serie de HowTos que muestran paso a paso como incluir Git en el flujo diario de trabajo.

Configuración básica

El primer paso para utilizar git consiste en configurar la información del autor, de manera que cada commit incluya la información completa de quien realiza los cambios. La configuración mínima incluye un nombre y un e-mail

~ $ git config --global user.name "Maciek Ruckgaber"
~ $ git config --global user.emall maciek@example.com

Los anteriores comandos crearán el archivo .gitconfig en el directorio raíz del usuario, y serán utilizados a manera de datos predeterminados para todos los proyectos. Esto a razón de que estamos usando el flag --global. De no usar el flag, esta información se guardará en el archivo .git/config dentro del proyecto actual y será únicamente relevante a ese proyecto.

Nuestro archivo de configuración debe verse mas o menos así:

[user]
    name = Maciek Ruckgaber
    email = maciek@example.com

Otras opciones que resulta útil personalizar son las siguientes. Éstas bien pueden agregarse usando la sintaxis utilizada para agregar el nombre y e-mail, o editando directamente el archivo.

# Core hace referencia a la configuración general de Git
# - editor: Editor utilizado al hacer commit, nano es una opción popular si no eres 
#           amigo de vim
# - excludesfile: Especifica un archivo que define patrones de carpetas u otros archivos 
#           que normalmente se quiere ignorar, como .zip, .tar, .project, etc
[core]
  editor = vim 
  excludesfile = /Users/maciekrb/.gitignore 

# Las siguientes son simplemente preferencias de color en la salida de  
# git status, git diff y git branch
[color]
  ui = true
  branch = auto
  status = auto
  diff = auto
[color "diff"]
  meta = yellow
  frag = cyan
  old = red
  new = green
[color "branch"]
  current = yellow reverse
  local = yellow
  remote = green

Agregando contenido al repo

Hecho lo anterior, estamos listos para inicializar tantos repos que queramos. Usaremos una estructura sencilla para ejemplificar los pasos siguientes.

~ $ tree
.
gitTutorial
  ├── assets
  │   ├── css
  │   │   └── style.css
  │   └── images
  │       └── logo.png
  ├── config
  │   └── config.inc
  ├── vendor
  │   ├── libABC
  │   └── zend-framework
  └── public
      └── index.php

Inicializar Git en un proyecto existente es muy sencillo

~ $ git init
Initialized empty Git repository in /Users/maciekrb/Devel/gitTutorial/.git/

Lo anterior simplemente crea una carpeta llamada .git dentro del directorio del proyecto. Esta carpeta tendrá internamente la base de datos necesaria para hacer seguimiento de los archivos del proyecto.

Esta base de datos consiste básicamente de unos cuantos tipos de objetos:

Blobs

Contienen los datos de los archivos

Trees

Representan las rutas de un sistema de archivos y parte de su meta-información

Commits

Meta-información de los cambios a los archivos, toda la información de las "fotos" del sistema de archivos del proyecto.

Tags

Que no son otra cosa que nombres del tipo V2.0-RC1 para algo que de otra forma podría llamarse 606470d540e57844c522210ee013d0a2d382d784

Pack Files

Los Blobs, Trees, Commits y Tags se acumulan con el tiempo y a fin de usar eficientemente el espacio en disco y ancho de banda, se comprimen en Pack Files.

Index

Finalmente, el Indice, corresponde a un archivo binario temporal que describe la estructura del repositorio en un momento particular y guarda la información del estado actual del repositorio. El Indice registra los cambios hasta que estos sean incluidos en un Commit.

Regresando a la práctica, después de inicializar el repositorio, podemos revisar fácilmente el estado del Indice ejecutando :

~ $ git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   assets/
#   config/
#   vendor/
#   public/
nothing added to commit but untracked files present (use "git add" to track)

Git Normalmente nos dará pistas de lo que ocurre, en este caso nos indica que estamos en el branch master, que este es un commit Inicial, que hay una lista de Untracked files entre los que están las carpetas assets, config, vendor y public, y que no hemos agregado nada al repositorio, y para hacerlo, que debemos usar git add.

El concepto clave para entender Git y des-aprender lo que sepamos de otros sistemas de control de versiones, es que Git es más un sistema de seguimiento de contenido, que un sistema de seguimiento de versiones. Esto quiere decir por ejemplo, que si dos archivos tienen exactamente el mismo contenido (evaluado usando un SHA1), Git creará un único objeto de este contenido en su base de datos, identificado con el SHA1 correspondiente, el nombre del archivo en el sistema, no tiene incidencia alguna en el cálculo del hash. La estructura de archivos de nuestro proyecto, no tiene incidencia alguna en la forma como Git almacenará los objetos de contenido. La información del nombre de archivo, se guarda en otro tipo de objeto, el Tree.

Adicionalmente, Git no guarda las diferencias entre un archivo y otro, guarda cada versión del archivo, hábilmente usando hard links cuando no existen diferencias entre la versión de un commit y la versión de otro. Dicho esto, es evidente que para regresar a una versión específica de un contenido, Git no necesita procesar cientos de diffs para obtener la versión, simplemente debe resolver un apuntador.

Es así que contrario a otros sistemas de control de versiones, add es un comando que usaremos con frecuencia, dado que nos permite "marcar" un archivo para ser incluido en el siguiente commit.

~ $ git add assets
~ $ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#new file:   assets/css/style.css
#new file:   assets/images/logo.png
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#  config/
#  vendor/
#  public/

Al ejecutar git add assets, Git marcó los archivos contenidos en la carpeta assets para ser incluidos en la próxima foto commit, esto nos permite fácilmente controlar los cambios que queremos incluir en el commit siguiente. Git nos sugiere, que en caso de querer des-marcar algún archivo, para que este no sea incluído en el próximo commit, podemos usar git rm --cached.

Podríamos usar git add carpeta por carpeta o archivo por archivo es útil, pero no suena práctico para agregar todo un proyecto. Para agregar el contenido de todos los archivos del proyecto, podemos usar git add . pero esto seguramente va a incluir archivos que no queremos en el repo.

.gitignore

Cada tipo de proyecto o entorno de desarrollo tiene ciertos archivos que no son bienvenidos en un repo. Cosas como .project, index.php~, index.php.swp, .venv, archivos .zip, .tar.gz, son ejemplos de patrones que queremos no incluir. Este tipo de casos comunes a la mayoría de proyectos en los que trabajaremos, son buenos candidatos para ser incluidos en el archivo .gitignore que definimos en el parámetro excludesfile = /Users/maciekrb/.gitignore en nuestro .gitconfig, así no tendremos que agregarlos en el .gitignore específico del proyecto cada vez que inicialicemos un nuevo repo. Las reglas configuradas en el .gitignore del proyecto, tiene prioridad respecto a las del archivo definido por excludesfile.

# Archivo /Users/maciekrb/.gitignore

.DS_Store
.env
.venv
.ropeproject
*.svn
*.pyc
*.swp
*.swo
*.sqlite
*.sql3
*.mo
*.php~
*.zip
# Archivo /Users/maciekrb/Devel/gitTutorial/.gitignore
vendor/

El archivo .gitignore del proyecto, puede tener cosas específicas a este, tales como por ejemplo vendor/ en este caso. Normalmente no queremos incluir el código fuente de las librerías dentro de nuestro código. Una mucho mejor estrategia consiste en usar composer para PHP, pip para Python o npm para node.js o Bower para Javascript, e incluir el archivo correspondiente de configuración de dependencias en el repo, e ignorar el directorio.

Por ejemplo, con composer, creamos un archivo composer.json, que agregamos al repo.

{
  "repositories": [
    {
      "type": "composer",
      "url": "https://packages.zendframework.com/"
    }
  ],
  "require": {
    "zendframework/zend-config": "2.0.*",
    "zendframework/zend-http": "2.0.*"
  }
}

Una vez ejecutado composer.phar install dentro del proyecto, composer creará un archivo adicional composer.lock, este archivo contiene las versiones exactas de las dependencias del proyecto, permitiendo fácilmente instalar todas las dependencias sin tenerlas dentro del repo. El mismo efecto se logra guardando en el repo el resultante de ejecutar pip freeze > dependencies.txt

Hecho esto, podemos terminar de agregar los archivos restantes

~ $ git add .
~ $ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
# new file:   .gitignore
# new file:   assets/css/style.css
# new file:   assets/images/logo.png
# new file:   composer.json
# new file:   composer.lock
# new file:   config/config.inc
# new file:   public/index.php
#

~ $ git commit
[master (root-commit) b249ede] Linea con descripción corta, hasta 50 chars
7 files changed, 150 insertions(+)
create mode 100644 .gitignore
create mode 100644 assets/css/style.css
create mode 100644 assets/images/logo.png
create mode 100644 composer.json
create mode 100644 composer.lock
create mode 100644 config/config.inc
create mode 100644 public/index.php

Al ejecutar el commit se cargará el editor configurado en nuestro archivo .gitconfig, incluyendo algunos detalles de lo que incluye el commit que estamos haciendo. Simplemente es necesario escribir los cambios que estamos incluyendo. La siguiente convención sirve bastante bien:

  • primera linea: descripción de cambios como si se tratase del asunto de un e-mail (max 50 caracteres)
  • bloque a continuación: Después de un salto de linea que ayuda a la legibilidad, se incluye una descripción detallada que puede tomar las lineas necesarias. Se obtiene mejores resultados usando una longitud de linea de máximo 72 columnas.
Linea con descripción corta, hasta 50 chars

Esta es una descripción mas larga y detallada. Ayuda limitar el número
de columnas a 72. Los mensajes deben escribirse en presente, "Resuelve
Issue XYZ", en lugar de "Resuelto Issue XYZ" o "Issue XYS resuelto",
esto coincide con el formato de mensaje de git merge y git revert.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
# new file:   .gitignore
# new file:   assets/css/style.css
# new file:   assets/images/logo.png
# new file:   composer.json
# new file:   composer.lock
# new file:   config/config.inc
# new file:   public/index.php
#

Al guardar y salir del editor set "tomará la foto" del Indice, y los cambios quedarán guardados en el repositorio. Esto corresponde a un conjunto de cambios atómico para convertir una versión anterior en la actual, en este caso puntual, la inclusión inicial de los archivos al proyecto. Podemos entonces ver los detalles del commit que acabamos de hacer.

~ $ git log
commit b249edeb237663aa183a854e074a320cb5e64afe
Author: Maciek Ruckgaber <maciekrb@gmail.com>
Date:   Wed Oct 23 15:45:46 2013 -0500

    Linea con descripción corta, hasta 50 chars

    Esta es una descripción mas larga y detallada. Ayuda limitar el número
    de columnas a 72. Los mensajes deben escribirse en presente, "Resuelve
    Issue XYZ", en lugar de "Resuelto Issue XYZ" o "Issue XYS resuelto",
    esto coincide con el formato de mensaje de git merge y git revert.

Ahora podemos comenzar a hacer modificar nuevamente los archivos, y simplemente repetimos el ciclo git add, git commit, tantas veces sea conveniente.

Remotos

Ya tenemos un repositorio local configurado y andando, sin embargo, el hecho de tener una única copia del código fuente es normalmente un riesgo. Si el disco de la maquina falla, o está sufre un cambio inesperado de propietario, se perdería todo nuestro trabajo, personalmente no encuentro nada mas tedioso que escribir nuevamente, código que ya escribí una vez.

La manera más sencilla de compartir código con Git, es hacerlo a través de un servicio que permita alojar código usando Git. Entre los servicios más populares se encuentra Github y BitBucket. Estos servicios harán las veces de remotos o remotes, a los que podemos empujar el código a fin de sincronizarlo desde otras máquinas o compartirlo con otras personas.

Los remotes o remotos no son otra cosa que alias, o nombres resumidos para direcciones que sería muy tedioso digitar cada vez en la linea de comando. Para agregar un remoto simplemente se debe ejecutar git remote add nombre_remoto git@github.com/usuarioABC/repositorioBCD. Cuando solamente existe un único remoto, normalmente se usa la convención de llamarlo origin. Una vez creado, las dos sentencias siguientes son equivalentes:

~ $ git pull nombre_remoto master
~ $ git pull git@github.com/usuarioABC/repositorioBCD master

nombre_remoto no es otra cosa que un alias a la dirección git@github.com/usuarioABC/repositorioBCD. Git ni siquiera verifica que esta dirección exista al momento de hacer git remote add. Una vez configurado el remoto es posible ejecutar git push nombre_remoto master o git pull nombre_remoto master para empujar o halar commits respectivamente.

Para verificar cuales remotos tenemos configurados podemos usar git remote -v.

Git y SSH

Los remotos alojados en servicios como Github o BitBucket se comunican usando SSH, un protocolo de red que permite la transmisión segura de datos. SSH utiliza criptografía asimétrica para autenticar una persona o maquina remota, y así se controla el acceso de quienes tienen privilegios de escritura en dado repositorio de Github o BitBucket. La lectura de dichos repositorios varía, un repositorio puede ser públicamente accesible (cualquier persona puede hacer clone, fetch) o privada, en cuyo caso la llave criptrográfica es el mecanismo para reconocer si alguien puede o no hacer clone o fetch.

El proceso para crear una llave privada y su contraparte pública (la que será registrada en el servicio de alojamiento de código) es muy sencillo: ssh-keygen -t rsa -C "maciek@example.com". El comando anterior crea una llave de tipo RSA con un comentario que contiene un e-mail. Al crear la llave se solicitará una contraseña, ésta contraseña es una medida de seguridad que únicamente permite usar la llave a quien que conozca la contraseña con la que fue creada. El resultado del proceso resulta normalmente en dos llaves id_rsa y id_rsa.pub en el folder .ssh el home del usuario, salvo que se haya definido una ruta diferente al momento de crearlas.

Una vez creadas, es necesario registrar el contenido de la llave id_rsa.pub en el servicio de alojamiento de código, normalmente en opciones de cuenta, en una seccion llamada SSH Keys. Es importante copiar el contenido del archivo id_rsa.pub sin incluir espacios o saltos de linea adicionales. Finalmente se puede realizar una prueba :

~ $ ssh git@github.com
Hi usuarioABC! You've sucessfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.

Dado que el servicio ofrece un shell restringido que únicamente permite ejecutar comandos de git, podemos entender esto como un mensaje exitoso de conexión usando la llave remotea para usuarioABC.

Errores comunes

Uno de los errores mas comunes en la configuración de Git, es crear una llave con un nombre que SSH no reconoce no reconoce con su configuración predeterminada. Salvo que se haya definido un valor distinto en el archivo .ssh/config, el cliente SSH siempre buscará una llave privada llamada id_rsa al conectarse con un servicio que realiza autenticación utilizando llaves criptográficas. Si la llave tiene un nombre distinto (mi_llave), el servicio remoto probablemente retornará un error como Permission denied (publickey).

Es muy frecuente que la gente pierda sus llaves públicas. Estas llaves, como su nombre lo indica, son en efecto públicas y revelarlas no tiene mayor inconveniente, contrario a las llaves privadas. Las llaves privadas contienen la información para generar la llave pública. En caso de perder una llave pública, solo es necesario correr ssh-keygen -y -f ~/.ssh/id_rsa > .ssh/id_rsa.pub para obtener la llave pública correspondiente.

Otro asunto frecuente, es confundir llaves públicas. Para identificar una llave pública, se puede obtener su fingerprint con el siguiente comando:

~ $ ssh-keygen -l -f ~/.ssh/id_rsa.pub 
2048 2a:d9:96:ef:ba:2a:ba:05:22:6d:26:b1:b2:68:29:b1  maciek@example.com (RSA)

Algunos servicios de alojamiento de código siempre muestran el fingerprint de cada llave registrada. Las llaves privadas, no tienen fingerprint, dado que de ellas es posible obtener la llave pública.

No sobra decir, que vale la pena guardar la llave privada en alguna parte adicional a la maquina (una memoria USB, Dropbox, Google Drive son algunas opciones), de manera que si algo ocurre con la máquina, podemos recuperar nuestro trabajo en unos pocos minutos.



blog comments powered by Disqus