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 addcopia 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.
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:
- 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ó.
- Si todavía no hiciste push:
git rm --cached, agrega la ruta a.gitignoreygit commit --amend. - 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. - Prevenirlo: crea el
.gitignoreantes del primergit add ., y mientras aprendes prefieregit 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 | — |
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óngit config set advice…son de esta versión; las versiones anteriores de Git imprimen un aviso más corto. git restore, que sugieregit status, llegó en Git 2.23 (2019) junto congit switch. Los tutoriales viejos usangit 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
- En un repositorio nuevo, crea
a.txtyb.txt, pero haz commit solo dea.txt. ¿Qué comando muestra queb.txtsigue sin seguimiento? - Editaste
app.pyy ejecutastegit add app.py.git diffno imprime nada. ¿Qué comando muestra tu cambio? notes.logya está en el historial. Agregas*.loga.gitignore, perogit statussigue mostrandonotes.logcomo modificado. Arréglalo sin borrar el archivo del disco.
Respuestas
git add a.txt && git commit -m "Add a"y luegogit status(ogit status -s, que imprime?? b.txt).git diff --staged. Los cambios preparados no aparecen engit diffa secas.git rm --cached notes.logy luegogit commit -m "Stop tracking notes.log". Compruébalo congit 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.