Git desde cero: init, add, commit y .gitignore

Tu primer commit de Git en cinco minutos: working tree, staging area, status, diff, log, .gitignore y qué pasa de verdad si haces commit de una contraseña.

Serie: curso de Git 1/6 (principiante). Para quien nunca usó Git o solo lo usó copiando comandos de una wiki o de un tutorial de bootcamp. Al terminar vas a crear un repositorio, hacer commits, leer status, diff y log, y mantener fuera del historial tanto los secretos como los archivos generados por la compilación.

Si acabas de entrar a un equipo y alguien te dijo "súbelo al repo", o si un cliente de tus trabajos freelance te pidió acceso a GitHub y no sabes por dónde empezar, esta clase es para ti. Resuelve el problema típico de quien empieza: no saber dónde está un cambio en este momento, si solo en el disco, preparado (staged) o guardado en el historial. En cuanto puedas responder eso mirando git status, la mayor parte de la confusión con Git desaparece.

Todo lo que sigue se ejecutó con Git 2.54.0 (Apple Git-157) en una Mac con Apple Silicon (M1 Pro), en una carpeta desechable, con una identidad de prueba llamada demo. Las salidas están copiadas tal como se imprimieron; solo la carpeta personal se acortó a ~.

Los tres lugares donde puede estar un cambio

Git usa tres términos que vas a ver en cada mensaje de error. Apréndelos primero.

  • Working tree (directorio de trabajo): los archivos normales de la carpeta de tu proyecto. Son los que editas.
  • Staging area (área de preparación, también llamada index): una lista de exactamente lo que va a contener el próximo commit. git add copia ahí el contenido actual de un archivo.
  • Repositorio: la carpeta oculta .git. Guarda cada commit. Un commit es una foto guardada del staging area, con autor, fecha, mensaje y un puntero al commit anterior.
Diagrama: el working tree, donde editas los archivos, pasa al staging area con git add. El staging area pasa al repositorio (la carpeta .git) con git commit. git diff compara el working tree con el staging area, y git diff --staged compara el staging area con el último commit.
Los tres lugares donde puede estar un cambio. git add copia un archivo al staging area; git commit guarda el staging area como una foto en el repositorio.

El staging area es la parte que sorprende. Existe para que puedas hacer commit de algunos de tus cambios: corriges un bug y un error de tipeo en dos archivos distintos y los guardas en commits separados.

Tu primer commit en cinco minutos

Abre una terminal y ejecuta estos comandos uno por uno. Primero, un error que comete casi todo el mundo: correr Git fuera de un repositorio.

git status
fatal: not a git repository (or any of the parent directories): .git

fatal: significa que Git se detuvo. El código de salida fue 128. La solución es crear un repositorio (o entrar con cd a uno):

mkdir shop && cd shop
git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint:   git config --global init.defaultBranch <name>
...
Initialized empty Git repository in ~/demo/shop/.git/

Las líneas hint: son consejos, no errores. La mayoría de los equipos usa main; git init -b main lo elige solo para este repositorio. Ahora crea un archivo y observa cómo git status lo va moviendo por los tres lugares:

printf 'print("hello, shop")\n' > app.py
git status
On branch master

No commits yet

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

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

Untracked (sin seguimiento) significa que Git ve el archivo pero nunca lo guardó. Prepáralo con add y luego haz commit:

git add app.py
git commit -m "Add app.py"
git log
[master (root-commit) 8d02147] Add app.py
 1 file changed, 1 insertion(+)
 create mode 100644 app.py
commit 8d02147eba3b707bfb452bed66288443ef8f808b
Author: demo <demo@example.com>
Date:   Thu Nov 12 01:00:00 2026 +0000

    Add app.py

8d02147… es el ID del commit, un hash de su contenido. Siete caracteres bastan para referirte a él. root-commit significa que no tiene padre: es el primero.

¿Quién eres? (configurar tu identidad)

Antes de tu primer commit real, configura una sola vez tu nombre y tu correo: git config --global user.name "Your Name" y git config --global user.email "<tu correo del trabajo>". En macOS, sin ninguna identidad configurada, nuestro commit de prueba no falló. Git armó un nombre a partir de la cuenta de la Mac y un correo a partir de la cuenta y el nombre del equipo, terminado en .local, hizo el commit y recién entonces imprimió Your name and email address were configured automatically…. Esa identidad medio equivocada ya quedó dentro del commit. Configúrala antes de empezar y usa la dirección que espera tu equipo.

Cómo leer status, diff y log

Cambia el archivo y pregúntale a Git qué pasó:

printf 'print("hello, shop")\nprint("total:", 3 * 1200)\n' > app.py
git status
git diff
On branch master
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:   app.py

no changes added to commit (use "git add" and/or "git commit -a")
diff --git a/app.py b/app.py
index d0b0cc7..fb97e2d 100644
--- a/app.py
+++ b/app.py
@@ -1 +1,2 @@
 print("hello, shop")
+print("total:", 3 * 1200)

Las líneas que empiezan con + se agregaron y las que empiezan con - se quitaron. Ahora prepara el cambio y ejecuta los dos diffs:

git add app.py
git diff            # prints nothing
git diff --staged   # prints the change

Este es el dato más útil de toda la clase. git diff muestra lo que todavía no está preparado; git diff --staged muestra lo que va a contener el próximo commit. Después de git add, en nuestra prueba git diff a secas no imprimió nada, y mucha gente que empieza concluye que perdió su cambio. No lo perdió; se movió al staging area.

git commit -m "Print total"
git log --oneline
292126a Print total
8d02147 Add app.py
Quieres saber Comando
Qué archivos cambiaron y dónde está cada cambio git status (-s para la forma corta)
Ediciones sin preparar, línea por línea git diff
Qué va a contener el próximo commit git diff --staged
El historial, un commit por línea git log --oneline
Qué archivos tocó cada commit git log --stat
Una versión antigua de un archivo git show <commit>:<path>

.gitignore: deja fuera la basura y los secretos

Hay archivos que nunca deberían entrar en un commit: lo que genera la compilación, las cachés, los archivos .env con contraseñas. Cometamos el error clásico a propósito. Creamos un .env y una carpeta de caché de Python, y luego usamos git add . ("agregar todo"):

printf 'DB_PASSWORD=demo-secret-123\n' > .env
mkdir __pycache__; printf 'x' > __pycache__/app.cpython-39.pyc
git add .
git status --short
git commit -m "Add config"
A  .env
A  __pycache__/app.cpython-39.pyc
[master 0d2cbec] Add config
 2 files changed, 2 insertions(+)
 create mode 100644 .env
 create mode 100644 __pycache__/app.cpython-39.pyc

Ahora lo "arreglamos" como casi todos intentan primero: escribiendo un .gitignore.

printf '.env\n__pycache__/\n' > .gitignore
git status --short
git check-ignore -v .env; echo "exit=$?"
?? .gitignore
exit=1

git check-ignore no imprimió nada y salió con 1, que significa "no ignorado". .gitignore solo se aplica a archivos sin seguimiento. .env ya está rastreado, así que Git lo sigue vigilando. Con --no-index Git reconoce que la regla coincide (.gitignore:1:.env .env), pero la regla no se usa para un archivo rastreado. Para dejar de rastrearlo sin borrarlo del disco:

git rm --cached .env __pycache__/app.cpython-39.pyc
git add .gitignore
git commit -m "Stop tracking .env and pycache"
rm '.env'
rm '__pycache__/app.cpython-39.pyc'
[master ba6ab87] Stop tracking .env and pycache
 3 files changed, 2 insertions(+), 2 deletions(-)
 delete mode 100644 .env
 create mode 100644 .gitignore
 delete mode 100644 __pycache__/app.cpython-39.pyc

ls -a todavía mostraba .env en la carpeta, así que tu configuración local sigue funcionando.

Error común: "borré el secreto, así que ya no está"

Lo que pasa en realidad. El commit 0d2cbec sigue existiendo, y sigue conteniendo el archivo:

git show HEAD~1:.env
git log --oneline -- .env
DB_PASSWORD=demo-secret-123
ba6ab87 Stop tracking .env and pycache
0d2cbec Add config

Cualquiera que clone el repositorio puede ejecutar el mismo comando. Borrar un archivo solo agrega una foto nueva sin él; el historial no cambia.

¿Y un commit que todavía no subiste con push? Probamos la reparación habitual, git commit --amend, que reemplaza el último commit:

git rm --cached .env
printf '.env\n' > .gitignore
git add .gitignore
git commit --amend -m "Add settings (without .env)"
git reflog
git show 'HEAD@{1}:.env'
75ed8d6 HEAD@{0}: commit (amend): Add settings (without .env)
88dcb5b HEAD@{1}: commit: Add settings
7b51afc HEAD@{2}: commit (initial): Add app.py
DB_PASSWORD=demo-secret-123

La rama ya no muestra el secreto, pero el commit viejo sigue en tu .git local, accesible por el reflog (el diario local de Git de hacia dónde apuntó HEAD). Un push no lo envía porque ninguna rama apunta a él, y expira más adelante. La clase 5 explica el reflog en detalle.

La solución, en orden:

  1. Considera el secreto filtrado. Cambia la contraseña o revoca la clave primero. Es el único paso que de verdad te protege una vez que se subió.
  2. Si todavía no hiciste push: git rm --cached, agrega la ruta a .gitignore y git commit --amend.
  3. Si ya hiciste push: quitarlo del historial implica reescribir historial compartido (con herramientas como git filter-repo, que no viene con Git y no se ejecutó aquí), y cada persona del equipo tiene que volver a clonar. Rotar el secreto sigue siendo el paso 1.
  4. Prevenirlo: crea el .gitignore antes del primer git add ., y mientras aprendes prefiere git add <file>.

Medido: qué hace git add . cuando olvidaste ignorar una carpeta

Un accidente real muy común es git add . en un proyecto con una carpeta de compilación grande. Creamos 20,000 archivos pequeños (de 16 a 20 bytes cada uno, build/d0…d199/f0…f99.o) en un repositorio con un solo archivo en el historial, y medimos en la Mac con Apple Silicon (M1 Pro), Git 2.54.0. Los tiempos son de 7 ejecuciones cada uno (mediana); git add . se ejecutó una sola vez.

Estado Mediana de git status Tamaño de .git en disco
build/ sin seguimiento, antes del add 16.6 ms 112 KB
después de git add . (tardó 15.2 s) 55.0 ms 82,284 KB
después de git reset (otra vez sin preparar) — 80,116 KB
después de git gc --prune=now — 128 KB
build/ incluido en .gitignore 13.3 ms —
Gráfico de barras del tamaño de .git: 112 KB antes, 82,284 KB después del git add de 20,000 archivos, 80,116 KB después de git reset y 128 KB después de git gc --prune=now.
Tamaño de .git en disco tras un git add . accidental de 20,000 archivos pequeños de compilación. Quitarlos del staging con git reset dejó casi todo; solo git gc --prune=now lo eliminó. Git 2.54.0, Apple M1 Pro, APFS.

Dos lecciones. Primero, git add escribe de inmediato cada archivo dentro de .git como un objeto. Quitarlo del staging con git reset no borra esos objetos; quedan como datos sin referencia hasta que la recolección de basura (garbage collection) los elimina. Segundo, los tamaños son uso de disco: cada objeto diminuto ocupa un bloque de 4 KiB en APFS, así que 20,000 objetos de menos de 20 bytes aparecen como unos 80 MB. En un proyecto real con binarios grandes, un git add . accidental cuesta espacio y tiempo de verdad, y si haces commit queda para siempre en el historial.

Límites honestos y notas de versión

  • Ejecutado con Git 2.54.0 (Apple Git-157), macOS, Apple Silicon (M1 Pro). El aviso de master/Git 3.0 y la redacción git config set advice… son de esta versión; las versiones anteriores de Git imprimen un aviso más corto.
  • git restore, que sugiere git status, llegó en Git 2.23 (2019) junto con git switch. Los tutoriales viejos usan git checkout -- <file> para lo mismo.
  • El comportamiento de identidad automática en macOS es lo que vimos en esta Mac sin configuración global. En otros sistemas el commit puede rechazarse con Author identity unknown.
  • Los tiempos dependen del disco y de la cantidad de archivos; son números de una sola máquina, no un benchmark.
  • Las herramientas para reescribir el historial se describieron, no se ejecutaron.

Ejercicios

  1. En un repositorio nuevo, crea a.txt y b.txt, pero haz commit solo de a.txt. ¿Qué comando muestra que b.txt sigue sin seguimiento?
  2. Editaste app.py y ejecutaste git add app.py. git diff no imprime nada. ¿Qué comando muestra tu cambio?
  3. notes.log ya está en el historial. Agregas *.log a .gitignore, pero git status sigue mostrando notes.log como modificado. Arréglalo sin borrar el archivo del disco.

Respuestas

  1. git add a.txt && git commit -m "Add a" y luego git status (o git status -s, que imprime ?? b.txt).
  2. git diff --staged. Los cambios preparados no aparecen en git diff a secas.
  3. git rm --cached notes.log y luego git commit -m "Stop tracking notes.log". Compruébalo con git check-ignore -v notes.log, que ahora imprime la regla que coincide.

Siguiente: Curso de Git 2: branches (ramas), merges y conflictos · También en esta serie: remotos, push, pull y fetch (en inglés) · Vuelve a la ruta de estudio para tu primer trabajo. Para la línea de comandos en sí, empieza por lo básico de la shell.

Preguntas frecuentes

Agregué un archivo a .gitignore pero Git sigue mostrando sus cambios, ¿por qué?

.gitignore solo afecta a los archivos sin seguimiento (untracked). Si el archivo ya estaba en un commit, Git lo sigue rastreando. Ejecuta git rm --cached <archivo> para dejar de rastrearlo sin borrarlo del disco y luego haz commit. git check-ignore -v <archivo> no imprime nada para un archivo rastreado, y esa es la forma más rápida de darte cuenta.

Hice commit de una contraseña, ¿basta con borrar el archivo en el siguiente commit?

No. El commit anterior todavía la contiene: en nuestra prueba, git show HEAD~1:.env imprimió la contraseña. Considera el secreto filtrado y cámbialo primero. Si el commit nunca se subió (push), git commit --amend o un reset lo quitan de la rama, pero incluso así el commit viejo sigue accesible por el reflog hasta que expira.

¿Qué diferencia hay entre git diff y git diff --staged?

git diff compara el working tree con el staging area (lo que todavía no preparaste). git diff --staged compara el staging area con el último commit (lo que va a contener el próximo commit). Después de git add, en nuestra prueba git diff a secas no imprimió nada, mientras que git diff --staged mostró el cambio.

  • #git
  • #principiante
  • #control de versiones
  • #gitignore
  • #staging area

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

Acerca de · Apps

↑