Apuntes curso git

El sistema de ramificación y fusionado (branch and merging) es lo que distingue a git de otros controles de versiones.

La creación y el fusionado de ramas es muy rápido y se recomienda a los desarrolladores que no tengan miedo de tener muchas ramas en sus desarrollos, ya que es una operación poco costosa. Si hay que experimentar con algo, se crea una rama, si hay que desarrollar una nueva funcionalidad, se crea una rama, si hay que corregir un bug, se crea una rama. No hay que tener pánico a la creación de ramas, son nuestras “amigas”.

Como git es un SCV distribuido, cada desarrollador puede tener sus propias ramas que no tienen por que estar subidas al repositorio central. Así que podemos sentirnos libres de ramificar tanto como queramos sin afectar al resto de desarrolladores.

Git es:

1 Introducción

http://juandarodriguez.es/cursos/svn/curso-svn-conceptos-basicos.html

1.1 Git básico

El flujo de trabajo típico es como sigue:

  1. El usuario modifica los ficheros en la copia de trabajo (your working tree).
  2. El usuario selecciona qué cambios van a formar parte de la próxima confirmación (commit) añadiendo dichos cambios a la staging area.
  3. El usuario hace un commit, la operación pilla los ficheros de la staging area y guarda un snapshop (revisión) permanentemente en el directorio .git (repositorio).

Si un fichero está en el repositorio se considera commited

Si un fichero están marcado para commit, es decir, está en el área, se considera staged.

Si un fichero está modificado pero no está en el staged área staged, se considera modified.

1.2 Instalación

En todas las plataformas siempre se puede instalar a partir de las fuentes.

1.3 Configuración inicial de git

Se usa git config, y se puede hacer a tres niveles distintos:

1.3.1 Configuración de la identidad

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

1.3.2 Editor

$ git config --global core.editor emacs

1.3.3 Mirando la configuración

$ git config --list

$ git config user.name

1.3.4 Obteniendo ayuda

$ git help

$

2 Fundamentos básicos

2.1 Crear un repositorio de git

Dos maneras:

cd /path/to/project
git init
git
add file1 file2 ...
git commit -m
'mensaje de commit'

git clone http://url.repo/repo
o
git
clone http://url.repo/repo myrepo

2.2 Guardando cambios en el repositorio

Cuando se clona un repositorio o se hace un commit de todos los ficheros “seguidos” (tracked), es decir todos los ficheros que han sido añadidos con el comando git add, dichos ficheros están en estado commited.

Cuando los  modificamos, pasan a estado modified.

Los ficheros que no han sido añadidos al control de versiones no tienen ningún estado pues no tienen importancia para el sistema de control de versiones, no obstante git informa de la existencia de estos fichero diciendo que están untracked (no seguidos).

2.2.1 Chequeando el estado del repositorio

El comando git status nos informa sobre el estado actual del repositorio. Esta es una salida a dicho comando después de haber:

On branch master
Your branch is up to date with
'origin/master'.

Changes to be committed:
 (use
"git reset HEAD <file>..." to unstage)

   new file:   calc.c
   modified:   package.json

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:   tsconfig.json

Untracked files:
 (use
"git add <file>..." to include in what will be committed)

   README.txt

2.2.2 Siguiendo (tracking) nuevos fichero

Si añadimos algún fichero al proyecto y queremos someterlo a control de versiones, tenemos que indicarlo explícitamente mediante el comando git add:

git add newfile

Esto hace que el fichero pase al área de staging (“changes to be commited”).

2.2.3 Pasando al staging los ficheros modificados

De la misma manera procedemos si lo que pretendemos es que un fichero seguido (tracked) que hemos modificado, sea confirmado (commited) en la próxima operación de confirmación (commit).

git add modifiedfile

Esto hace que el fichero pase al área de staging (“changes to be commited”).

Atención, el fichero ha sido añadido al área de staging con las modificaciones en el momento en que se hace git add, si seguimos modificándolo, las nuevas modificaciones no se confirmarán. Esto haría que el fichero modificado después de hacer git add, esté a la vez en el área de staging y en la de ficheros modificamos. Compruébalo.

2.2.4 Ver el estado en formato corto

La operación git status da mucha información. Incluso nos dice cómo debemos proceder según los que deseemos hacer, lo cual se agradece especialmente cuando se empieza a usar git.

Si queremos ver el estado del repositorio en un formato más corto podemos usar:

git status -s
git status --short

Para el ejemplo anterior obtendríamos:

A  calc.c
M  package.json
M tsconfig.json
?? README.txt

A significa que es nuevo pero ha sido añadido para confirmación (al área de staging)

M en la columna de la izquierda que se ha modificado y se ha añadido al área de staging.

M en la columna de la derecha que se ha modificado pero no se ha añadido al área de staging.

?? que no está seguido.

En realidad la columna de la izquierda se refiere al área de staging y la de la derecha al working tree (ficheros seguidos pero no añadidos al área de staging).

2.2.5 Ignorando ficheros

En ciertos proyecto hay ficheros que no queremos seguir, ni siquiera por error. Podemos indicarlo mediante el fichero .gitignore. En él añadimos todos los ficheros y rutas que no queramos seguir.

En el repositorio de github https://github.com/github/gitignore, se encuentran una amplia colección de ficheros .gitignore para el desarrollo de proyectos en distintos lenguajes y entornos de programación.

2.2.6 Viendo los cambios realizados

Con el comando git status podemos saber los ficheros que han sufrido cambios, pero no qué cambios se han realizado respecto a la última versión confirmada o añadida al stage. Para ello podemos usar el comando git diff (o git diff fichero para un fichero concreto).

Si queremos ver las diferencias en los ficheros que han sido añadidos para confirmación (staged) usaremos git diff --staged.

2.2.7 Confirmando los cambios

Una vez que queremos confirmar los cambios realizados y ficheros añadidos añadidos al área de staging (marcados para confirmar) usamos git commit . Se abrirá el editor por defecto para añadir un mensaje de log. Cuanto más descriptivo sea este mensaje, tanto mejor.

git commit
o
git commit -m
'mensaje de log'

La salida muestra la rama y el código sha-1 que identifica a la confirmación (commit)

[master ed2d84c] primer commit
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 calc.c

2.2.8 Saltándose el área de staging

Muy frecuentemente queremos confirmar todos los ficheros que hemos modificados, con lo que, en principio, tendríamos que hacer:

git add file1 file2 file3 file4 ... // todos los ficheros
git commit -m
'mensaje de log'

Sin embargo, en este caso, podemos hacer la confirmación de todos los archivos modificados directamente, sin pasar por el área de staging, pasando el modificador -a.

git commit -a -m 'mensaje de log'

Mucho más directo.

2.2.9 Borrar ficheros

Si eliminamos directamente un fichero (rm file), el cambio pasará como modificación no registrada para confirmación, si queremos añadirla tendremos que usar git add. Pero si hacemos git rm file, entonces el borrado aparecerá ya registrado para confirmación. En los mismos mensajes de estado git indica cómo deshacer el borrado:

git reset HEAD <file> //para quitarlo del área de staging
git checkout -- <file> //para deshacer el borrado en la copia de trabajo (cuando no está en staging)

2.2.10 Moviendo ficheros

Git no registra los cambios de nombre en ficheros. Si, por ejemplo, hacemos:

mv tsconfig.json tsconfig

al hacer git status veremos que dice que el fichero  tsconfig.json ha sido eliminado y que se ha añadido un nuevo fichero (sin seguimiento) tsconfig.

Así una operación completa de movimiento de fichero sería:

mv tsconfig.json tsconfig
git add tsconfig tsconfig.json
git commit

o también

mv tsconfig.json tsconfig
git rm tsconfig.json
git add tsconfig
git commit

pero git dispone de un comando más directo equivalente al anterior:

git mv tsconfig.json tsconfig

2.3 Viendo el historial de confirmaciones (commit)

Una vez que hemos hecho varias confirmaciones en nuestro proyecto, a veces necesitamos saber qué es lo que ha ido pasando. Para ello contamos con el comando git log.

Usado sin ningún modificador presenta un listado con todas las confirmaciones. Para cada una de ellas ofrece el checksum sha-1 que la identifica, el nombre y email del usuario que hizo la confirmación y el mensaje de log. El listado se presenta en orden cronológico inverso.

Pero este comando tiene un montón de modificadores. Veremos en este apartado los más comunes.

Con git log -p (o --patch) podemos ver las diferencias introducidas en cada confirmación.

Con git log -<n> (<n> es un nº) vemos las últimas N entradas de log.

Con git log --stat vemos las estadísticas de inserciones y borrados sobre cada fichero que se modificó en cada confirmación.

Con git log --oneline se presenta cada log en una sola línea.

Con git log --pretty=[tipo] (tipo puede ser oneline, short, full y fuller), se presentan distintos formatos de log con más o menos información.

Con git log --pretty=format:"%h - %an, %ar : %s" se puede definir el formato que queramos.

La siguiente tabla muestra las opciones más útiles para usar con format.

Option

Description of Output

%H

Commit hash

%h

Abbreviated commit hash

%T

Tree hash

%t

Abbreviated tree hash

%P

Parent hashes

%p

Abbreviated parent hashes

%an

Author name

%ae

Author email

%ad

Author date (format respects the --date=option)

%ar

Author date, relative

%cn

Committer name

%ce

Committer email

%cd

Committer date

%cr

Committer date, relative

%s

Subject

La diferencia entre author y committer es que el primero es quien escribió el código original mientras que el segundo es quien hizo la modificación en cuestión.

Con git log --graph obtenemos un gráfico que muestra todo el árbol de confirmaciones con sus distintas ramas. Para mejorar la interpretabilidad de este árbol, esta opción suele usarse en combinación con --oneline.

Y muchas más opciones que puedes ver con git log --help.

2.3.1 Limitando la salida de log

Podemos limitar el nº de confirmaciones a mostrar de varias maneras:

Las <n> últimas confirmaciones con git log -<n>, que ya hemos visto anteriormente,

Las que se han llevado desde o hasta una fecha con git log --since (o --until).

Podemos filtrar buscando expresiones en los mensajes de confirmación con --grep, o por autor con --author, o por commiter con --commiter.

Con git log -S <text> se filtran las confirmaciones en que se cambian el nº de ocurrencias de <text>.

2.3.2 Combinándolo todo

Por último decir que todas las opciones de los apartados 2.13 y 2.14 se pueden combinar para producir salidas de log más específicas.

2.4 Deshaciendo cosas

Podemos deshacer muchas de las cosas que hacemos con git. Errar posiblemente sea la actividad que más nos define como humanos y un sistema que no permite deshacer cosas no es un sistema completo.

El problema es que la función principal de git es registrar todos los cambios que se vayan haciendo en el proyecto, de manera que no perdamos nada. Así que si, por lo que sea, decidimos usar las funcionalidades para deshacer cosas, hemos de tener en cuenta que si no estamos atentos podemos perder parte del trabajo realizado.

Una de las operaciones típicas en la que nos equivocamos es en las confirmaciones (commits): se nos ha pasado añadir algún fichero o el mensaje de error no nos gusta o no es correcto. Podemos deshace con git commit --amend. Este comando permite añadir al último commit realizado los ficheros que hayamos añadido al área de staging (después del último commit) y cambiar el mensaje de log.

2.4.1 Quitando un fichero del área de staging

Se hace con git reset HEAD <file>..., esto lo puedes ver cuando hacer un git status, la propia salida indica cómo se puede quitar un fichero del área de staging.

2.4.2 Deshaciendo un fichero modificado

Se hace con git checkout -- <file>, también se indica en la salida de git status.

2.5 Trabajando con (repositorios) remotos

Para hacer posible la colaboración entre desarrolladores se requiere el uso de repositorios remotos, los cuales no son más que versiones de un repositorio que se encuentran, normalmente, alojados en algún servidor accesible por la red. En realidad también pueden estar en tu mismo equipo, pero si no hay manera de que otros desarrolladores puedan acceder a él tienen poco sentido.

Colaborar con otros implica colocar (push) y traer (pull) datos de los repositorios remotos, de manera que otros desarrolladores puedan usar (pull) nuestras modificaciones o enviar (push) las suyas.

Gestionar remotos significa:

2.5.1 Mostrando los remotos

Se hace con git remote. Este comando muestra una lista con todos los repositorios remotos que tenga asociado nuestro repositorio. Si hemos obtenido el repositorio mediante el clonado de otro repositorios, el comando mostrará como salida origin, que es el nombre que git da por defecto a los repositorios clonados.

2.5.2 Añadiendo repositorios remotos

Se hace con:

git remote add <shortname> <url>

Y ya se puede usar el nuevo repositorio, referenciándolo por su shortname,  para enviar (push) y recibir (pull) datos.

2.5.3 Recibiendo datos de remotos (fetching y pulling)

Nota: En este apartado comenzamos a hablar de ramas. Por lo pronto podemos pensar en una rama tal y como la podemos intuir; una historia del código distinta a la historia principal (conocida como rama master). Cuando estudiemos las ramas en la próxima unidad todo quedará más claro.

Para recibir del repositorio  los datos que aún no tenemos se hace:

git fetch <remote>

Después de esto debemos tener referencias a todas las ramas del remoto que podemos mezclar o inspeccionar en cualquier momento.

Es muy importante saber que traerse los datos de un repositorio remoto no significa que se mezclen (merge) con los ficheros de la copia de trabajo. Se tiene la información de novedades pero la mezcla tenemos que hacerla manualmente.

Cuando se sigue a una determinada rama de un repositorio determinado, no es necesario indicarlo en el parámetro <remote>, basta con hacer git fetch. Se puede seguir a una rama (branch) de determinado repositorio remoto mediante el comando:

git push -u <remote> <branch>

Cuando hacemos un clon de un repositorio remoto, automáticamente la rama master sigue a la rama master de  dicho remoto.

Si además de traernos los datos nuevos queremos mezclarlos con nuestra copia de trabajo del tirón (automáticamente), usaremos pull:

git pull <remote> <branch>

Siendo opcional los argumentos <remote> y <branch> si queremos operar con el repositorio remoto y rama asociados con la rama sobre la que estamos trabajando en nuestro repositorio local. A esta remoto/rama se le llama upstream, como veremos más adelante en el estudio de las ramas.

2.5.4 Enviando datos al remoto

Cuando hemos realizado modificaciones y las hemos confirmados, lo normal es que queramos enviarlas al repositorio remoto para que otros puedan usarlas. Esto se hace con

git push <remote> <branch>

En caso de que estemos siguiendo a la rama y repositorio en los que deseamos enviar los cambios podemos omitir los parámetros <remote> y <branch>.

Únicamente podemos realizar esta operación si nadie ha realizado un push desde la última vez que te trajiste datos desde el remoto. Si eso ocurre git te avisará y te dirá que debes traerte antes los nuevos cambios del remoto y mezclarlos con tu repositorio. Entonces podrás subir (push) tus cambios (solo si mientras tanto no se te ha colado de nuevo alguien).

2.5.5 Inspeccionado un repositorio remoto

Se hace con:

git remote show <remote>

Da información de las ramas remotas de las que tenemos referencias, la rama local configurada para git pull y las referencias locales configuradas para git push.

2.5.6 Renombrando y borrando repositorios remotos

Se hace con:

git remote rename <oldname> <newname>

y

git remote remove <remote>

2.6 Etiquetas

Una funcionalidad típica de los sistemas de control de versiones es el etiquetado de revisiones. Se usa para marcar revisiones (commits) que son más importantes por alguna razón. El caso típico son las releases (versiones de lanzamiento).

2.6.1 Listar etiquetas

Podemos ver todas las etiquetas de nuestro repo con:

git tag

La lista se muestra en orden alfabético (no cronológico)

Se pueden filtrar etiquetas en la lista así:

git tag -l "v1.8.5*"

2.6.2 Crear etiquetas

Se pueden crear dos tipos de etiquetas: ligeras y anotadas. Las primeras son simplemente un puntero al commit que se etiqueta. Las segundas tienen una entidad más compleja, ya que incluyen datos asociados como el autor, un mensaje de log, el email y la fecha, además pueden ser firmadas con GPG.

2.6.3 Etiquetas anotadas

Se crean con la opción -a:

git tag -a <etiqueta> -m <mensaje de log>

2.6.4 Etiquetas ligeras

Se crean sin pasar la opción -a y sin mensaje del log:

git tag <etiqueta>

2.6.5 Etiquetando commits anteriores

Basta con indicar el sha-1 del commit que deseamos etiquetar:

git tag -a v1.2 9fceb02

2.6.6 Enviando etiquetas al repositorio remoto

Por defecto git push no envía las etiquetas al repositorio, hay que hacerlo explícitamente:

git push <remote> <tag>

O si queremos enviar todas las etiquetas del tirón:

git push <remote> --tags o git push --tags (si lo hacemos sobre el remoto que seguimos)

2.6.7 Colocando la copia de trabajo en un commit marcado por una etiqueta

git checkout <tag>

Es importante saber (git avisa de ello), que esta operación deja la copia de trabajo en un estado DETACHED, lo que significa que si haces un commit, aunque la etiqueta siga siendo la misma, el commit no pertenecerá a ninguna rama y no puede ser encontrado, salvo que sepamos el hash y lo referenciemos explícitamente. Si queremos hacer commits, debemos crear una rama al hacer checkout del commit etiquetado:

git checkout -b <branch> <tag>

2.6.8 Alias

Podemos extender los comandos de git usando alias a nuestra medida. Por ejemplo si queremos referirnos al comando status como st, podemos hacer:

git config --global alias.st status

3 Ramas

Todos los sistemas de control de versiones proporcionan alguna forma de ramificación. De manera que el desarrollo se lleva a cabo entorno a una línea principal que con el tiempo se va ramificando en distintas versiones que dependen del proyecto en cuestión. Una rama puede servir para probar nuevas características, otra puede ser la rama a partir de la cual se realizan las releases (lanzamientos), etcétera. El código de las ramas comparte una historia común a partir de algún punto del desarrollo y es normal que se realicen mezclas y fusiones de código entre distintas ramas.

El sistema de ramificación de git es especialmente potente. Mientras que en la mayor parte de los sistemas de control de versiones crear una nueva rama es un proceso costoso, en git la ramificación es un proceso ligero que se realiza casi instantáneamente. Tanto es así que se recomienda a los usuarios a crear rama incluso para realizar cambios pequeños y hasta varias veces en el mismo día.

3.1 Las ramas en pocas palabras

Para entender cómo funcionan las ramas en git, primero hay que entender como git almacena los datos.

Cada vez que se hace un commit, se almacena un objeto commit que es un puntero a un snapshot de los ficheros que se han añadido al área de staging. Este objeto también contiene el nombre del autor, su email, la fecha de creación, el mensaje de log y un puntero al commit o los commits que le precede o preceden inmediatamente: ninguno en el caso del primer commit, uno en el caso de un commit normal o dos o más en el caso de que el commit sea el resultado de una fusión de dos o más ramas.

Veamos con un ejemplo que ocurre al hacer un commit. Supongamos que tenemos un proyecto y añadimos al área de staging tres ficheros.

Al añadir al staging los archivos, Git realiza una suma de control de cada uno de ellos (un resumen SHA-1), almacena una copia de cada uno en el repositorio (estas copias se denominan "blobs"), y guarda cada suma de control en el área de preparación (staging area)

Al hacer commit, Git realiza sumas de control de cada subdirectorio (en el ejemplo, solamente tenemos el directorio principal del proyecto), y las guarda como objetos árbol (tree) en el repositorio Git. Después, Git crea un objeto de confirmación con los metadatos pertinentes y un apuntador al objeto árbol raíz del proyecto.

En este momento, el repositorio de Git contendrá cinco objetos: un "blob" para cada uno de los tres archivos, un árbol con la lista de contenidos del directorio (más sus respectivas relaciones con los "blobs"), y una confirmación de cambios (commit) apuntando a la raíz de ese árbol y conteniendo el resto de metadatos pertinentes.

Cuando hacemos nuevos cambios y los confirmamos, Git crea nuevos objetos commits  tal y como hemos descrito que apuntan al commit anterior:

Esta última imagen, que resume toda la estructura de árbol en un cuadro etiquetado como Snapshot, es la que debemos tener para entender qué cosa es una rama y cómo funciona el proceso de ramificación.

Una rama en git no es más que un puntero ligero que apunta a uno de estos commits. La rama inicial, es decir la que se crea cuando iniciamos un repositorio con git, se llama master. Esta rama no tiene nada especial respecto a otras que creemos más adelante. Se llama master por defecto, pero este nombre se puede cambiar. A medida que hacemos commit, la rama master, es decir el puntero correspondiente a la rama master se mueve hacia adelante automáticamente, apuntando al último commit que hemos hecho.

Hacemos commit

3.1.1 Creando una rama

Llegó el momento de ramificar. La operación es tan sencillo como hacer lo siguiente:

git branch test

Con esta operación creamos una nueva rama denominada test. Lo cual no es más que un puntero que apunta al mismo commit en el que estamos en el momento de lanzar el comando. La situación ahora es así:

Tenemos dos ramas (punteros) que apuntan al mismo commit. ¿Cómo sabe git en que ramas estamos en cada momento? Un puntero especial denominado HEAD marca cual es la rama en la que estamos en cada momento. Cuando acabamos de crear la rama test, la situación es la siguiente:

Haciendo git log --decorate obtenemos por pantalla una representación de los commits con los punteros de ramas y el puntero HEAD:

# git log --decorate --oneline
5d1a1d7 (HEAD -> master,
test) cambio 2
a4d2554 cambio 1
27eb9b9 commit inicial

3.1.2 Cambiar de rama

Ya solo nos queda saber cómo cambiar de rama. Esto se hace con el comando checkout:

# git checkout test

Ahora la situación es:

# git log --decorate --oneline
5d1a1d7 (HEAD ->
test, master) cambio 2
a4d2554 cambio 1
27eb9b9 commit inicial

A partir de ahora, cuando hagamos commit, se moverá automáticamente el puntero de la rama test y el puntero HEAD lo seguirá:

# git log --decorate --oneline
fa4953c (HEAD ->
test) cambio3
5d1a1d7 (master) cambio 2
a4d2554 cambio 1
27eb9b9 commit inicial

Si ahora volvemos a la rama master con git checkout master, el puntero HEAD pasaría de apuntar a test a apuntar a master. La situación sería como sigue:

A partir de ahora los nuevos commits que hagamos desplazan el puntero de la rama master y el puntero HEAD lo seguirá. Si hacemos un cambio y lo confirmamos la situación será:

Acabamos de provocar una divergencia entre las ramas. Tanto test como master comparten un pasado común que tiene por ancestro al commit 5d1a1d7, pero a partir de ahí evolucionan de manera distinta.

Con el siguiente comando obtenemos una visión gráfica de la situación:

# git log --decorate --oneline --graph --all
* 5fded84 (HEAD -> master) cambio 3
| * fa4953c (
test) cambio3
|/  
* 5d1a1d7 cambio 2
* a4d2554 cambio 1
* 27eb9b9 commit inicial

Una rama en git es simplemente un fichero que contienen el checksum SHA-1 del commit al cual apunta. Este fichero ocupa 41 bytes, es decir, prácticamente nada. Por ello crear ramas en git es muy “barato”, al contrario que en otros sistemas de versiones, como subversion, en los que crear una rama implica realizar una copia completa del proyecto.

3.2 Ramificando y fusionando

Mostraremos lo básico de la creación de ramas y la fusión mediante dos ejemplos que muestra un flujo de trabajo típico.

Flujo 1

  1. Estamos trabajando en la rama master de un proyecto
  2. Queremos probar una idea y hacemos una rama para trabajarla
  3. Nos gusta la idea y la mezclamos en la rama master

Supongamos que partimos de la siguiente situación:

Ahora creamos una nueva rama para explorar la idea:

# git branch newfunc

Y nos cambiamos a ella

# git checkout newfunc
Switched to branch
'newfunc'

 

La situación es:

La combinación:

#git checkout -b newfunc
Switched to branch
'newfunc'

crea la rama y nos cambia a ella en un solo paso. Es equivalente a:

# git branch newfunc
# git checkout newwfunc

Comenzamos a trabajar y hacemos los commits que hagan falta. Supongamos que hemos hecho 1. La situación sería:

Y el resultado nos gusta. Queremos mezclarlo en la rama master. Así que volvemos a ella:

# git checkout master

Y fusionamos:

# git merge newfunc
Updating 730279d..3507c70
Fast-forward
database.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

La situación final es:

La fusión en este caso es extremadamente sencilla. Ya que todos los cambios realizados en la rama newfunc se han hecho sobre la rama master, basta llevar el puntero de la rama master a la newfunc para que se integren los cambios. Es lo que se llama un fast forward.

Como la rama newfunc no la necesitamos más, podemos borrarla:

# git branch -d bug

Flujo 2

  1. Estamos trabajando en la rama master de un proyecto.
  2. Creamos una rama para trabajar en una nueva funcionalidad.
  3. Hacemos algún trabajo en esa rama
  4. De pronto tenemos una emergencia; se ha detectado un bug en la versión de producción, que en este caso la haremos coincidir con master, y hay que arreglarlo tan pronto como se pueda.
  5. Volvemos a la rama master.
  6. Creamos una rama para arreglar el bug
  7. Trabajamos en la rama y comprobamos que realmente hemos arreglado el bug. Entonces fusionamos esta rama con master para que se pueda pasar a producción.
  8. Volvemos a la rama en que estábamos trabajando la nueva funcionalidad.

Supongamos que partimos de la siguiente situación:

Comenzamos a trabajar en la nueva funcionalidad. Para ello creamos la rama newfunc.

# git branch newfunc

La situación es:

Y ahora cambiamos de rama:

# git checkout newfunc
Switched to branch
'newfunc'

 

La situación es:

La combinación:

#git checkout -b newfunc
Switched to branch
'newfunc'

crea la rama y nos cambia a ella en un solo paso. Es equivalente a:

# git branch newfunc
# git checkout newwfunc

Comenzamos a trabajar y hacemos los commits que hagan falta. Supongamos que hemos hecho 1. La situación sería:

Ahora recibimos una notificación que nos fastidia nuestra concentración y ritmo en el desarrollo de la nueva funcionalidad; hay que arreglar urgentemente un bug que se ha detectado en producción. ¡De nuevo lo urgente no nos deja que hagamos lo necesario!. entonces nos aseguramos de que hemos dejado todo los cambios confirmados en la rama newfunc y volvemos a la rama master.

# git checkout master

Las situación es ahora:

Podríamos corregir ahora el bug, pero para dejar la rama master lo más limpia posible y no colisionar con el trabajo de otros desarrolladores es preferible crear una nueva rama para arreglar el bug. Llamemosla bug.

# git checkout -b bug

La situación es:

Y comenzamos a arreglar el bug. Supongamos que hacemos el un commit tenemos listo la corrección del bug. La situación ahora es:

Y ahora vamos a fusionar este cambio en la rama master. Para ello volvemos a la rama master:

# git checkout master

Y fusionamos con el comando merge:

# git merge bug
Updating 730279d..29a24d4
Fast-forward
database.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Esta fusión es muy sencilla de realizar para git. Como no se ha realizado ningún cambio en la rama master durante la corrección del bug, es decir el puntero de la rama master no se ha desplazado, la fusión es tan sencilla como desplazar dicho puntero (master) al commit apuntado por la rama bug. Es lo que git denomina un fast-forward. La situación es como sigue:

Podemos ver esta situación en la CLI así:

# git log --decorate --oneline --graph --all
* 29a24d4 (HEAD -> master, bug) cambio 4
| * 3507c70 (newfunc) cambio 3
|/  
* 730279d cambio 2
* a8caba3 cambio 1
* d2b0f87 commit inicial

Como la rama bug ya no la necesitamos, para dejar la cosa más limpia vamos a borrarla.

# git branch -d bug

Ahora podemos volver a la rama newfunc a seguir trabajando en el desarrollo de la nueva funcionalidad:

# git checkout newfunc

Y continuamos haciendo commits en esta rama.

Utilizando git para ver el árbol de commits:

# git log --decorate --oneline --graph --all
* b1734bd (HEAD -> newfunc) cambio 5
* 3507c70 cambio 3
| * 29a24d4 (master) cambio 4
|/  
* 730279d cambio 2
* a8caba3 cambio 1
* d2b0f87 commit inicial

El tema es que los cambios que hicimos para corregir el bug no se encuentran en la rama newfunc. Una cosa que podemos hacer es fusionar la rama master con la rama newfunc, haciendo git merge master. O también podemos esperar a integrar esos cambios hasta que decidamos colocar los cambios de newfunc en la rama master. Dependerá de lo que más nos convenga según el modelo de desarrollo que hayamos adaptado.

3.2.1 Fusión básica

Supongamos que hemos decidido que la rama newfunc ya está lista para producción. Lo que haremos ahora es volver a la rama master y fusionar newfunc en master. Ahora newfunc y master han divergido, es decir newfunc no tiene como ancestro  directo a master, aunque master y newfunc si que tiene un ancestro común (730279d). Git realiza la fusión utilizando estos tres commits: 730279d, b1734bd (newfunc) y 29a24d4 (master),

y crea un nuevo commit que tiene dos padres: el correspondiente a b1734bd y a 29a24d4.

# git checkout master
# git merge newfunc
Merge made by the
'recursive' strategy.
calc.c  | 1 +
front.c | 1 +
2 files changed, 2 insertions(+)

Cuando hacemos git merge en este caso, git nos pregunta por un mensaje de log para colocar en el nuevo commit que creará automáticamente para realizar la fusión a tres bandas.


La situación ahora es así:

Primero hacemos el cambio de rama de newfunc a master:

Y después hacemos la fusión con newfunc:

En CLI podemos ver esta situación así:

git log --decorate --oneline --graph --all
*   2dbb899 (HEAD -> master) Merge branch
'newfunc'
|\  
| * b1734bd (newfunc) cambio 5
| * 3507c70 cambio 3
* | 29a24d4 cambio 4
|/  
* 730279d cambio 2
* a8caba3 cambio 1
* d2b0f87 commit inicial

Como ya hemos terminado con la rama newfunc, si queremos podemos borrarla:

# git branch -d newfunc


Resolución de conflictos

Cuando realizamos una fusión de una rama en otra es muy habitual que git no pueda realizar automática y limpiamente todo el proceso. Cuando tanto en la rama que fusionamos como en la rama sobre la que fusionamos se haya realizado algún cambio en las mismas líneas de un fichero, git no sabrá cuál de los dos cambios es el correcto. Así que esta situación provocará un conflicto que tendremos que arreglar manualmente.

Volvamos a repetir los pasos para fusionar la rama newfunc sobre master como hicimos en el apartado anterior. Pero que en el fichero README.md se produce un conflicto. Tendremos lo siguiente:

# git checkout master
# git merge newfunc

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Ahora git no nos pregunta por un mensaje de log, ya que debido al conflicto, no va a crear ningún commit  automáticamente para realizar la fusión a tres bandas. Lo que sí hace es mezclar los archivos en los que no haya conflicto y en los que sí los haya realiza una mezcla con todos los cambios, tanto los de una rama como los de la otra:

<<<<<<< HEAD:index.html
<div id=
"footer">contact : email.support@github.com</div>
=======
<div id=
"footer">
please contact us at support@github.com
</div>
>>>>>>> newfunc:index.html

Lo que está arriba de ======= es lo que hay en la rama master (donde apunta HEAD ahora mismo) y lo que hay debajo es lo que hay en la rama newfunc. Ahora nos toca decidir qué es lo que hay que dejar, lo de arriba, lo de abajo o una mezcla de los dos.

Cuando lo tengamos claro y lo editemos, para resolver el conflicto añadimos al área de staging el archivo y entonces realizamos el commit:

# git add index.html
# git commit -m 'resolución conflicto'

Y se creará el commit que fusiona las ramas master y newfunc, es decir un commit cuyo puntero tiene por padres a los punteros de las ramas master y newfunc. Se trata del commit que se frustró en el momento de realizar la fusión debido al conflicto.

3.3 Gestión de ramas

Si queremos listar todas las ramas de un proyecto:

$ git branch
 newfunc
* master
 testing

El asterisco (*) muestra la rama en la que nos encontramos en este momento.

con el modificador -v tenemos más información:

$ git branch -v
 newfunc  93b412c fix javascript issue
* master  7a98805 Merge branch
newfunc
 testing 782fd34 add scott to the author list
in the readmes

El modificador --merged filtra la lista de ramas a aquellas cuyos cambios ya han sido  sido mezclados con la rama sobre la que realizamos la fusión de manera que no hay nada nuevo en ellas que no esté en la rama sobre la que fusionamos.

$ git branch --merged
 newfunc
* master

Las ramas que aparecen sin asterisco en esta lista, pueden ser borradas con git branch -d, ya que el trabajo ha sido ya mezclado.

El modificador --no-merged hace lo contrario, muestra las ramas que aún tienen trabajo no mezclado con la rama sobre la que hacemos la fusión:

$ git branch --no-merged
 testing

Estas, no deberíamos borrarlas, de hecho git no nos dejaría al menos que lo forcemos con git branch d testing --force.

3.4 Ramas remotas

Hasta aquí todo el proceso de ramificación ha ocurrido localmente. Ahora llega el momento de ver como llevamos y traemos ramas hasta y desde los repositorios remotos.

Las ramas de seguimiento remoto (remote-tracking branches) son referencias locales al estado de ramas del repositorio remoto. Estas referencias no se pueden mover. Git se encarga de hacerlo cuando establece una conexión con el repositorio remoto. Podemos visualizarlas como marcadores para recordarnos donde estaba la rama en el repositorio remoto la última vez que nos conectamos a él.

Las ramas de seguimiento remoto tienen la forma <remote>/<branch>, por ejemplo: origin/master.

Si estás trabajando en una funcionalidad con un compañero y este último subió al remoto una rama newfunc, cuando actualices tu repositorio local tendrás una rama de seguimiento remoto que se llamará origin/newfunc. Además podrás tener tu propia rama local newfunc, la cual puede estar o no basada en origin/newfunc. Depende de cómo la hayas creado. Más adelante, en el apartado 3.10 Trazando ramas (tracking branches), se explica cómo crear ramas locales que siguen a ramas remotas.

Veámos cómo funcionan las ramas de seguimiento remoto con un ejemplo. Cuando hacemos un git clone de un repositorio, git añade automáticamente el remoto desde donde se clona y lo llama origin. Y crea un puntero denominado origin/master que apunta al commit al cual la rama master del remoto apunta en ese momento. En ese momento master y origin/master apuntan al mismo commit.

Si ahora comenzamos a trabajar y hacemos commits y mientras tanto alguien sube al repositorio sus cambios, las dos ramas comienzan a diverger.

La situación cuando hacemos commits y no conectamos aún con el servidor, aunque ya hayan subido cambios desde la última vez que estábamos sincronizados, es:

Si ahora queremos sincronizar con el servidor haremos un git fetch origin. Esta instrucción se traerá la parte nueva del árbol de commits  de la rama origin en el remoto y la posición de su  puntero:

Y esta sería la nueva estructura de nuestro repositorio local. Ahora master y origin/master no coinciden. Es importante dejar claro que el hecho de sincronizar la estructura de commits con el remoto no implica que nuestro código haya cambiado lo más mínimo. Como muestra el gráfico, simplemente tenemos dos ramas que tienen un commit común pero apuntan a commits distintos.

Si queremos mezclar los cambios nuevos del repositorio en nuestra rama master solo tenemos que fusionar origin/master sobre master haciendo git merge origin/master sobre la rama master (también vale hacer simplemente git merge, en un rato veremos por qué)

3.4.1 Pushing

Si queremos compartir con los colaboradores alguna rama que hayamos creado en nuestro repositorio, utilizaremos git push <remote> <branch>. Por ejemplo supongamos que hemos creado una rama local llamada serverfix, y queremos enviarla al repositorio remoto marcado como origin. Haremos:

$ git push origin serverfix
Counting objects: 24,
done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15),
done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s,
done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch]      serverfix -> serverfix

Esto hay que hacerlo explícitamente, git no actualiza el repositorio remoto automáticamente. Esto permite que podamos tener en nuestro repositorio local tantas ramas como queramos y compartir solo las que sean realmente relevantes para todos los colaboradores.

También podemos hacer de manera equivalente (aunque redundante):

$ git push origin serverfix:serverfix

Lo cual no tiene mucho sentido por la redundancia. Si que tiene sentido si queremos denominar a la rama remota con un nombre distinto:

$ git push origin serverfix:serverfix_remote

La próxima vez que un colaborador haga un git fetch origin, obtendrá la rama de seguimiento remoto origin/serverfix.

Si queremos fusionar las ramas remotas que traemos con fetch con la rama en la que estamos, simplemente hacemos un merge de la rama remota:

# git merge origin/serverfix

3.4.2 Trazando ramas (tracking branches)

Una rama de seguimiento es una ramas local que tiene una relación directa con alguna rama remota. Si estás en una rama de seguimiento y haces git pull, git automáticamente sabe de qué servidor debe traerse los cambios y en qué rama fusionar. Igualmente si haces git push desde una rama de seguimiento, git sabe a qué remoto rama debe enviar los cambios.

El ejemplo más inmediato de rama de seguimiento es la rama master que obtenemos cuando hacemos un git clone de algún remoto. Esta operación crea una rama local denominada master que está basada (sigue) en la rama master del remoto. De manera que cuando, desde esta rama, hacemos git pull, git se trae los cambios que hayan sucedido en la rama master remota desde la última vez que se actualizó y los mezcla.

Además de la rama master podemos crear otras ramas de seguimiento que sigan a otras ramas remotas. A la rama remota seguida por la rama de seguimiento se le denomina upstream. El comando para hacer esto es:

git checkout -b <branch> <remote>/<branch>

Git ofrece el siguiente comando abreviado para hacer lo mismo.

git checkout --track origin/serverfix

Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

Se crea una rama serverfix que sigue (con base a) a la rama remota origin/serverfix.

A la rama origin/serverfix se le denomina entonces up stream de la rama local serverfix.

Esta operación es tan habitual que git ofrece un comando aún más simplificado para hacer esto mismo:

git checkout serverfix
Branch serverfix
set up to track remote branch serverfix from origin.
Switched to a new branch
'serverfix'

Crea la rama serverfix, siguiendo a la rama remota origin/serverfix y además cambia de rama.

Si tenemos una rama local existente que acabamos de crear y queremos hacerla seguir a una rama remota (origin/serverfix, por ejemplo), nos colocamos en la rama local y hacemos:

git branch -u origin/serverfix

Para ver un listado de todas las ramas locales indicando las ramas upstream podemos hacer:

$ git branch -vv
 iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
 master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should
do it
 testing   5ea463a trying something new

3.4.3 Pulling

La operación git fetch se trae desde el repositorio todos los cambios que hayan sucedido en él desde la última vez que sincronizaste. Pero no hace la fusión, para ello hay que utilizar git merge. Ya hemos visto en el apartado anterior que podemos hacer las dos operaciones del tirón mediante git pull. Si estamos en una rama de seguimiento, git se traerá los cambios del repositorio y mezclará los cambios de la rama upstream, es decir de la rama remota, sobre la rama local.

3.4.4 Borrado rama remota

Cuando, por la razón que sea, ya no se necesite más una determinada rama remota, podemos borrarla así:

git push origin --delete serverfix

 

4 Personalización

4.1 Configuración de git

Ya hemos visto que podemos configurar git a nivel de sistema, global o de proyecto:

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

En esta sección ampliaremos las personalizaciones que podemos hacer con git.

En primer lugar indicaremos que los directorios donde git guarda la configuración son:

/etc/gitconfig  a nivel de sistemas

~/.config/git/config a nivel global (usuario)

.git/config dentro del directorio de proyecto, a nivel de proyecto

4.1.1 Configuración del cliente git

Podemos ver todas las configuraciones que podemos hacer al cliente con

man git-config

Algunas de ellas:

4.2 Git Hooks

Los hooks son procedimientos que se ejecutan antes o después de que git realice alguna operación como, por ejemplo, un commit.

Los hooks son aplicaciones (shell scripts, python, perl, C, …) que se encuentran en el directorio .git/hooks. Cuando se hace la iniciación de un proyecto con git, se crea este directorio con algunos ejemplos de script:

Para que funcionen hay que eliminar la extensión .sample

5 Depuración

5.1 Anotación de ficheros

Con git blame podemos ver de un vistazo quién y en qué commit  se cambió cada línea de un determinado fichero:

git blame src/app/carta/carta.component.ts

^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  1) import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, SimpleChange } from '@angular/core';
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  2) import { ImagenService } from
'../services/imagen.service';
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  3) import { globalState } from
'../state';
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  4)
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  5) @Component({
4f03dfb8 src/app/carta/carta.component.ts   (Juan David Rodríguez García 2018-06-27 22:53:46 +0200  6)   selector:
'app-carta',
4f03dfb8 src/app/carta/carta.component.ts   (Juan David Rodríguez García 2018-06-27 22:53:46 +0200  7)   templateUrl:
'./carta.component.html',
4f03dfb8 src/app/carta/carta.component.ts   (Juan David Rodríguez García 2018-06-27 22:53:46 +0200  8)   styleUrls: [
'./carta.component.css']
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200  9) })
4f03dfb8 src/app/carta/carta.component.ts   (Juan David Rodríguez García 2018-06-27 22:53:46 +0200 10)
export class CartaComponent implements OnInit, OnChanges {
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 11)
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 12)   // El nº de imagen que presenta el componente
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 13)   @Input(
'n') n: number
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García
2018-06-27 00:13:38 +0200 14)   // El estado global, un array con no más de 2 elementos que
^2d2e45a src/app/imagen/imagen.component.ts (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 15)   // represent

Podemos limitar el rango de líneas que deseamos ver:

git blame  -L 10,20 src/app/app.component.ts
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 10)   // El estado global que consiste en un array de números.
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 11)   // Cada número del array representa a una imagen descubierta.
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 12)   // Por ello no puede haber más de 2 elementos en el array, es decir,
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 13)   // puede haber ninguno, uno o dos y va evolucionando en ese orden a
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 14)   // medida que se va jugando.
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 15)   gs: number[] = []
538f2bea (Juan David Rodríguez            2018-07-05 10:04:27 +0200 16)
538f2bea (Juan David Rodríguez            2018-07-05 10:04:27 +0200 17)   // La distribución aleatoria de cartas que conforman el juego
45211af4 (Juan David Rodríguez García 2018-06-28 00:06:43 +0200 18)   cardDistribution: number[]
^2d2e45a (Juan David Rodríguez García 2018-06-27 00:13:38 +0200 19)
ae4c9051 (Juan David Rodríguez            2018-07-05 10:05:12 +0200 20)   constructor(private imagenService: ImagenService){