Saltar a contenido

Uso básico de Git

Crear un proyecto

Crear un programa "Hola Mundo"

Note

Este tutorial indica muchas ordenes de git de esta forma:

$ git init

El símbolo $ lo que representa es el prompt del shell sistema operativo. Es un símbolo universal para indicar que git init debe ser escrito en tu terminal. Por tanto, no escribas el $ solo lo que esté a la derecha.

Eso también te ayudará a distinguir entre lo que es el comando que debes ejecutar y la salida del programa.

Creamos un directorio donde colocar el código

$ mkdir curso-de-git
$ cd curso-de-git

Creamos un fichero hola.py que muestre Hola Mundo.

print("Hola, Mundo.")

Info

Los ejemplos están en lenguaje python, que viene de serie instalado tanto en sistemas GNU/Linux como en otros sistemas propietarios.

No es necesario que lo tengas instalado, pero si quieres ejecutar el código y ver lo que hace puede ser interesante.

Para ver lo que hace este programa ejecuta en una terminal:

python hola.py

Crear el repositorio

Para crear un nuevo repositorio se usa la orden git init

$ git init
Initialized empty Git repository in /home/cc0gobas/git/curso-de-git/.git/

Enhorabuena, has aprendido tu primer comando de git. Lo usarás solo cuando quieras crear uno desde cero.

Estado del repositorio

Otro comando muy útil, es el que nos permite saber el estado en el que está nuestro repositorio, el area de trabajo (donde están nuestros archivos) y el área de staging (donde git guarda cambios antes de almacenarlos en el repositorio). Si no te suena lo que es, repasa el lo que son los tres estados en el capitulo de introducción.

$ git status
On branch main

No commits yet

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

nothing added to commit but untracked files present (use "git add" to track)

Aquí podemos ver que estamos en la rama main (o master si no has configurado el nombre de la rama por defecto). Que aún no hemos hecho ningún commit, y que tenemos un fichero sin registrar (untracked). Para git un fichero untracked es aquel que es nuevo y que aún no le hemos dicho que debe incorporarlo a su base de datos de cambios.

Por el último nos sugiere lo que podemos hacer.

Info

Cuando no sepas en qué estado se encuentra tu repositorio o qué tienes que hacer ejecuta siempre git status. Git te dará información sobre cómo se encuentra tu estado de trabajo y qué acciones puedes realizar.

Crear nuestro primer commit.

Vamos a almacenar el archivo que hemos creado en el repositorio para poder trabajar. Vamos a hacer todo de golpe y después explicaremos para qué sirve cada orden.

$ git add hola.py
$ git commit -m "Creación del proyecto"
[main (root-commit) e19f2c1] Creación del proyecto
    1 file changed, 1 insertion(+)
    create mode 100644 hola.py

Grafo del repositorio

Acabamos de iniciar nuestra rama de cambios y ya tenemos nuestro primer nodo. El siguiente gráfico es una representación visual del estado del repositorio. Podemos ver una linea que es la rama main y un commit representado por su hash.

gitGraph:
commit id: "e19f2c1"

Ahora mismo no entenderás mucho pero poco a poco lo irás haciendo.

Actualizar ficheros

Ejecutamos git status como vimos antes:

$ git status
On branch main
nothing to commit, working tree clean

Si modificamos el archivo hola.py:

import sys

if len(sys.argv) > 1:
    print(f"Hola, {sys.argv[1]}.")
else:
    print("Hola.")

Y volvemos a comprobar el estado del repositorio:

$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   hola.py

no changes added to commit (use "git add" and/or "git commit -a")

Git nos indica en color rojo que hola.py ha sido modificado y nos lo indica como Changes not staged for commit. Esto nos indica que hemos modificado el archivo original y que ahora tenemos que decidir que hacer con él, si queremos marcar esos cambios para guardarlos o si queremos restaurar el fichero a su estado anterior.

Preparar archivos

Vamos a seguir el flujo más habitual, vamos a decirle a git que queremos conservar los cambios. Con la orden git add indicamos a git que prepare los cambios para que sean almacenados.

$ git add hola.py
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   hola.py

Sigue indicandonos que el fichero está modificado, pero ahora en color verde y como Changes to be committed.

Info

Esto es lo que se conoce como fase de stagging o preparación.

Confirmar los cambios

Ahora vamos a guardar definitivamente la actualización de nuestro programa en nuestro repositorio. Con la orden git commit confirmamos los cambios definitivamente.

$ git commit -m "Parametrización del programa"
[main efc252e] Parametrización del programa
    1 file changed, 6 insertion(+), 1 deletion(-)
$ git status
On branch main
nothing to commit, working directory clean

Grafo del repositorio

Así esta el estado de nuestro repositorio. No te preocupes si no obtienes exactamente la misma salida, es completamente normal. Lo importante es que el estado del repositorio sea el mismo (con o sin ficheros para confirmar)

gitGraph:
    commit id: "e19f2c1"
    commit id: "efc252e"

Diferencias entre workdir y staging.

Modificamos nuestra aplicación para que soporte un parámetro por defecto y añadimos los cambios.

import sys

nombre = sys.argv[1] if len(sys.argv) > 1 else "Mundo"
print(f"Hola, {nombre}")

Este vez añadimos los cambios a la fase de staging pero sin confirmarlos (commit).

git add hola.py

Question

Si ejecutaras ahora git status. ¿Qué debería ocurrir? Ya lo hemos hecho antes. Imagina lo que debería pasar repasando el tutorial y comprueba si has acertado.

Volvemos a modificar el programa para indicar con un comentario lo que hemos hecho.

import sys

# El nombre por defecto es Mundo
nombre = sys.argv[1] if len(sys.argv) > 1 else "Mundo"
print(f"Hola, {nombre}")

Y vemos el estado en el que está el repositorio

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   hola.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   hola.py

Podemos ver como aparecen el archivo hola.py dos veces. El primero está preparado para ser confirmado y está almacenado en la zona de staging. El segundo indica que el directorio hola.py está modificado otra vez en la zona de trabajo (workdir).

La zona de trabajo es donde tú trabajas, y la zona de stagging es donde trabaja git. Los cambios que no se hayan confirmado en la zona de staggig no se guardarán en el repositorio. Cuando un fichero es distinto en ambas zonas, nos lo indicará para que sepamos que si guardamos los cambios, solo se guardarán los que confirmamos la primera vez.

Warning

Si volvieramos a hacer un git add hola.py sobreescribiríamos los cambios previos que había en la zona de staging. No lo hagas, sigue el tutorial.

Almacenamos los cambios por separado:

$ git commit -m "Se añade un parámetro por defecto"
[main 3283e0d] Se añade un parámetro por defecto
    1 file changed, 2 insertions(+), 4 deletions(-)

$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   hola.py

no changes added to commit (use "git add" and/or "git commit -a")

$ git add hola.py

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   hola.py

$ git commit -m "Se añade un comentario al cambio del valor por defecto"
[main fd4da94] Se añade un comentario al cambio del valor por defecto
    1 file changed, 1 insertion(+)

$ git status
On branch main
nothing to commit, working tree clean

Grafo del repositorio

gitGraph:
    commit id: "e19f2c1"
    commit id: "efc252e"
    commit id: "3283e0d"
    commit id: "fd4da94"

Ignorando archivos

Hasta ahora hemos añadido los archivos indicando siempre el nombre, pero si hemos cambiado muchos ficheros o están en subdirectorios, no es muy cómodo.

Si en vez de indicar el nombre de un fichero indicamos git add . le estaremos diciendo a git que añada todos los ficheros que se encuentre tanto en el directorio actual como en cualquier subdirectorio. Eso incluye tanto a los ficheros nuevos como a los modificados.

También podemos indicarle un directorio y solo buscará recursivamenet en el que le indimos, así como hacer una mezcla de ficheros y directorios. Por ejemplo:

git add directorio
git add directorio/subdir
git add docs/agenda fichero.txt practicas/1/main.c

La orden git add . o git add nombre_directorio es muy cómoda, ya que nos permite añadir todos los archivos del proyecto o todos los contenidos en un directorio y sus subdirectorios. Es mucho más rápido que tener que ir añadiéndolos uno por uno. El problema es que, si no se tiene cuidado, se puede terminar por añadir archivos innecesarios o con información sensible.

Por lo general se debe evitar añadir archivos que se hayan generado como producto de la compilación del proyecto, los que generen los entornos de desarrollo (archivos de configuración y temporales) y aquellos que contentan información sensible, como contraseñas o tokens de autenticación. Por ejemplo, en un proyecto de C/C++, los archivos objeto no deben incluirse, solo los que contengan código fuente y los make que los generen.

Para indicarle a git que debe ignorar un archivo, se puede crear un fichero llamado .gitignore, bien en la raíz del proyecto o en los subdirectorios que queramos. Dicho fichero puede contener patrones, uno en cada línea, que especiquen qué archivos deben ignorarse. El formato es el siguiente:

# .gitignore
dir1/           # ignora todo lo que contenga el directorio dir1
!dir1/info.txt  # El operador ! excluye del ignore a dir1/info.txt (sí se guardaría)
dir2/*.txt      # ignora todos los archivos txt que hay en el directorio dir2
dir3/**/*.txt   # ignora todos los archivos txt que hay en el dir3 y sus subdirectorios
*.o             # ignora todos los archivos con extensión .o en todos los directorios

Cada tipo de proyecto genera sus ficheros temporales, así que para cada proyecto hay un .gitignore apropiado. Existen repositorios que ya tienen creadas plantillas. Podéis encontrar uno en https://github.com/github/gitignore

Ignorando archivos globalmente

Si bien, los archivos que hemos metido en .gitignore, deben ser aquellos ficheros temporales o de configuración que se pueden crear durante las fases de compilación o ejecución del programa, en ocasiones habrá otros ficheros que tampoco debemos introducir en el repositorio y que son recurrentes en todos los proyectos. En dicho caso, es más útil tener un gitignore que sea global a todos nuestros proyectos. Esta configuración sería complementaria a la que ya tenemos. Ejemplos de lo que se puede ignorar de forma global son los ficheros temporales del sistema operativo (*~, .nfs*) y los que generan los entornos de desarrollo.

Para indicar a git que queremos tener un fichero de gitignore global, tenemos que configurarlo con la siguiente orden:

git config --global core.excludesfile $HOME/.gitignore_global

Ahora podemos crear un archivo llamado .gitignore_global en la raíz de nuestra cuenta con este contenido:

# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
*~
*.swp

# IDEs               #
######################
.idea
.settings/
.classpath
.project

Trabajando con el historial

Observando los cambios

Con la orden git log podemos ver todos los cambios que hemos hecho:

$ git log
commit fd4da946326fbe8b24e89282ad25a71721bf40f6
Author: Sergio Gómez <sergio@uco.es>
Date:   Sun Jun 16 12:51:01 2013 +0200

    Se añade un comentario al cambio del valor por defecto

commit 3283e0d306c8d42d55ffcb64e456f10510df8177
Author: Sergio Gómez <sergio@uco.es>
Date:   Sun Jun 16 12:50:00 2013 +0200

    Se añade un parámetro por defecto

commit efc252e11939351505a426a6e1aa5bb7dc1dd7c0
Author: Sergio Gómez <sergio@uco.es>
Date:   Sun Jun 16 12:13:26 2013 +0200

    Parametrización del programa

commit e19f2c1701069d9d1159e9ee21acaa1bbc47d264
Author: Sergio Gómez <sergio@uco.es>
Date:   Sun Jun 16 11:55:23 2013 +0200

    Creación del proyecto

También es posible ver versiones abreviadas o limitadas, dependiendo de los parámetros:

$ git log --oneline
fd4da94 (HEAD -> main) Se añade un comentario al cambio del valor por defecto
3283e0d Se añade un parámetro por defecto
efc252e Parametrización del programa
e19f2c1 Creación del proyecto
git log --oneline --max-count=2
git log --oneline --since='5 minutes ago'
git log --oneline --until='5 minutes ago'
git log --oneline --author=sergio
git log --oneline --all

Info

Si quieres ver todas las opciones git log ejecuta:

$ git log --help

Una versión muy útil de git log es la siguiente, pues nos permite ver en que lugares está main y HEAD, entre otras cosas:

$ git log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short
* fd4da94 2013-06-16 | Se añade un comentario al cambio del valor por defecto (HEAD, main) [Sergio Gómez]
* 3283e0d 2013-06-16 | Se añade un parámetro por defecto [Sergio Gómez]
* efc252e 2013-06-16 | Parametrización del programa [Sergio Gómez]
* e19f2c1 2013-06-16 | Creación del proyecto [Sergio Gómez]

Crear alias

Como estas órdenes son demasiado largas, Git nos permite crear alias para crear nuevas órdenes parametrizadas. Para ello podemos configurar nuestro entorno con la orden git config de la siguiente manera:

git config --global alias.hist "log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short"

Note

En este tutorial solo vamos a usar el alias hist. Pero puedes crear tus propios alias si quieres.

Recuperando versiones anteriores

Cada cambio es etiquetado por un hash, para poder regresar a ese momento del estado del proyecto se usa la orden git checkout.

$ git checkout e19f2c1
Note: switching to 'e19f2c1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at e19f2c1 Creación del proyecto
$ cat hola.py
print("Hola Mundo");

Como vemos nuestro fichero esta como la primera vez que lo escribimos. Lo que hemos hecho es desplazar nuestro espacio de trabajo al primer commit

gitGraph:
    commit id: "e19f2c1" type: HIGHLIGHT
    commit id: "efc252e"
    commit id: "3283e0d"
    commit id: "fd4da94"

Pero una rama de git siempre tiene una referencia al ultimo commit de cada rama, y esa referencia para que no tengamos que aprendernos el hash, se llama como la rama.

El aviso que nos está dando git es que estamos en una posición que no está enlazada como la última posición de ninguna rama, si hicieramos algún cambio estaríamos creando una bifurcación sin nombre de rama. Podríamos perderlos salvo que anotemos esos hash en un papel o creemos una nueva rama desde esta posición. Por el momento no nos vamos a preocupar más y esto lo veremos en el capítulo de ramas con mayor profundidad.

Volver a la última versión de la rama main.

Usamos git switch indicando el nombre de la rama:

$ git switch main
Previous HEAD position was e19f2c1 Creación del proyecto
Switched to branch 'main'

Info

Hemos visto dos comandos nuevos: checkout y switch que aparentemente hacen lo mismo. En realidad git checkout hace muchas más cosas, desde cambiar de ramas, hasta recuperar ficheros, o deshacer cambios. Debido a que tenía tantas opciones, el proyecto git dividió checkout en dos comandos distintos: switch y restore. En este tutorial se usará sobre todo switch y restore porque son más simples de usar y recordad. Además son los que sugiere usar git status.

¿Cuándo se usa entonces git checkout? Pues dado que git switch solo funciona con ramas, lo usaremos cuando queremos mover nuestro espacio de trabajo a algo que nosea una rama, como fue el caso anterior con un hash de un commit, o como veremos a continuación con los tags.

Etiquetando versiones

Para poder recuperar versiones concretas en la historia del repositorio, podemos etiquetarlas, lo cual es más facil que usar un hash. Para eso usaremos la orden git tag.

$ git tag v1

Ahora vamos a etiquetar la versión inmediatamente anterior como v1-beta. Para ello podemos usar los modificadores ^ o ~ que nos llevarán a un ancestro determinado. Las siguientes dos órdenes son equivalentes:

$ git checkout v1^
$ git checkout v1~1
$ git tag v1-beta

Note

Los modificadores ^ y ~ permiten movernos hacia atrás en la historia de commits partiendo de una referencia.
- ^ indica el commit padre inmediato.
- ~n indica el ancestro n de la referencia.
Por ejemplo, v1^ y v1~1 apuntan al commit anterior a v1, mientras que v1~3 saltaría tres commits atrás desde v1.

Si ejecutamos la orden sin parámetros nos mostrará todas las etiquetas existentes.

$ git tag
v1
v1-beta

Grafo del repositorio

`mermaid gitGraph: commit id: "e19f2c1" commit id: "efc252e" commit id: "3283e0d" tag: "v1-beta" commit id: "fd4da94" tag: "v1"

Y para verlas en el historial:

$ git hist --all
* fd4da94 2013-06-16 | Se añade un comentario al cambio del valor por defecto (tag: v1, main) [Sergio Gómez]
* 3283e0d 2013-06-16 | Se añade un parámetro por defecto (HEAD, tag: v1-beta) [Sergio Gómez]
* efc252e 2013-06-16 | Parametrización del programa [Sergio Gómez]
* e19f2c1 2013-06-16 | Creación del proyecto [Sergio Gómez]

El parámetro --all le indica a git log que nos muestra todas las ramas, ya que por defecto solo muestra desde la posición de nuestro espacio de trabajo. Si te fijas, verás un HEAD en la posición en la que estamos ahora. Ese HEAD simboliza en qué commit estamos y en qué rama actualmente. Si quitamos --all ya no veremos el inicio de la rama main

$ git hist
* 3283e0d 2013-06-16 | Se añade un parámetro por defecto (HEAD, tag: v1-beta) [Sergio Gómez]
* efc252e 2013-06-16 | Parametrización del programa [Sergio Gómez]
* e19f2c1 2013-06-16 | Creación del proyecto [Sergio Gómez]

Borrar etiquetas

Para borrar etiquetas:

git tag -d nombre_etiqueta

Visualizar cambios

Para ver los cambios que se han realizado en el código usamos la orden git diff. La orden sin especificar nada más, mostrará los cambios que no han sido añadidos aún, es decir, todos los cambios que se han hecho antes de usar la orden git add. Después se puede indicar un parámetro y dará los cambios entre la versión indicada y el estado actual. O para comparar dos versiones entre sí, se indica la más antigua y la más nueva. Por ejemplo si queremos ver la diferencia entre los dos commits que hemos etiquetado:

$ git diff v1-beta v1
diff --git a/hola.py b/hola.py
index a31e01f..25a35c0 100644
--- a/hola.py
+++ b/hola.py
@@ -1,5 +1,4 @@
 import sys

-# El nombre por defecto es Mundo
 nombre = sys.argv[1] if len(sys.argv) > 1 else "Mundo"
 print(f"Hola, {nombre}")