La lección anterior dejó a gestor-tareas con un historial legible, bisecable y reversible. Pero un historial impecable puede contener basura, y el de gestor-tareas la contiene: Bruno subió sin darse cuenta la carpeta node_modules completa —14.000 ficheros y 180 MB—, Carla ha ido sembrando Thumbs.db por los directorios de imágenes, el macOS de Bruno deja un .DS_Store en cada carpeta que abre en el Finder, y en algún commit de hace meses hay un fichero config.js con la contraseña de la base de datos de pruebas.
Esta lección trata de la primera línea de defensa contra todo eso: .gitignore, el mecanismo con el que se le dice a Git qué ficheros no debe ni proponerse versionar. Lo mencionamos de pasada en la lección 02-04, al ver git status, y prometimos desarrollarlo aquí.
Es un fichero engañosamente simple. Su sintaxis tiene esquinas que confunden a todo el mundo —la negación, la barra inicial, la trampa de los directorios— y, sobre todo, esconde una regla que es la causa del 90 % de las preguntas sobre Git en foros: .gitignore no sirve para dejar de seguir un fichero que ya está versionado.
Contenido
- Por qué importa lo que no se versiona
- Qué no debe entrar nunca: tabla por categorías
- Cómo funciona: precedencia y ámbito
- Sintaxis completa de los patrones
- La negación con
!y su trampa - Los tres niveles: proyecto, local y global
- La regla que confunde a todo el mundo: solo afecta a lo no seguido
- Sacar del seguimiento con
git rm --cached - Depurar con
git check-ignore -v - Forzar con
git add -f - Plantillas por lenguaje
- El patrón para secretos:
.envy.env.ejemplo
- Por qué importa lo que no se versiona
Versionar lo que no toca tiene cuatro costes, y los cuatro son mayores de lo que parecen:
| Coste | Consecuencia concreta |
|---|---|
| Tamaño permanente | Los 180 MB de node_modules quedan en el historial para siempre, aunque se borren después. Cada clon los descarga (lección 08-06). |
| Conflictos absurdos | Ficheros generados (compilados, bloqueos, .idea/workspace.xml) cambian en cada máquina y provocan conflictos en cada fusión, sin significado alguno. |
| Ruido en las revisiones | Una PR de 30 líneas útiles con 4.000 líneas de artefactos es irrevisable (lección 07-02). |
| Riesgo de seguridad | Un secreto en el repositorio se replica a cada clon y no se borra borrando el fichero. Es lo que veremos en la lección 08-05. |
Hay un principio que ordena todas las decisiones:
Se versiona la fuente, no el resultado. Si un fichero se puede regenerar a partir de otros ficheros del repositorio, no se versiona. Si contiene información propia de una máquina o de una persona, no se versiona. Si es un secreto, no se versiona jamás.
Con dos excepciones importantes que conviene conocer, porque son las que confunden:
- Los ficheros de bloqueo de dependencias (
package-lock.json,yarn.lock,Cargo.locken aplicaciones) sí se versionan. No son un resultado: son la fuente de la reproducibilidad. Sin ellos, dos personas instalan versiones distintas. - La configuración compartida del proyecto (
.editorconfig,.gitattributes, la parte del.vscode/que define tareas y extensiones recomendadas) sí se versiona. Es fuente, no resultado.
- Qué no debe entrar nunca: tabla por categorías
| Categoría | Ejemplos | Por qué no |
|---|---|---|
| Dependencias | node_modules/, vendor/, bower_components/, venv/, .venv/, target/ (Rust) |
Se regeneran con npm ci, composer install, pip install -r. Miles de ficheros, decenas o cientos de MB, y contenido distinto según el sistema operativo. |
| Artefactos de compilación | dist/, build/, out/, *.o, *.class, *.pyc, __pycache__/, *.min.js generados |
Son la salida del código que sí está versionado. Cambian en cada compilación y generan conflictos garantizados. |
| Ficheros del sistema operativo | .DS_Store, ._*, .Spotlight-V100 (macOS); Thumbs.db, Desktop.ini, $RECYCLE.BIN/ (Windows); .directory (Linux) |
No tienen nada que ver con el proyecto: son metadatos del explorador de ficheros. Bruno y Carla ensucian el repositorio sin querer. |
| Configuración del editor y del IDE | .idea/, .vscode/settings.json personal, *.swp, *.swo, .project, .classpath, *.sublime-workspace |
Reflejan preferencias personales y rutas absolutas de una máquina. Provocan conflictos entre quien usa un editor y quien usa otro. |
| Secretos y credenciales | .env, .env.local, config/secrets.yml, *.pem, *.key, id_rsa, ficheros de credenciales de servicios |
Nunca. Un secreto en el historial es un secreto comprometido, aunque lo borres después (lección 08-05). |
| Registros y temporales | *.log, logs/, tmp/, *.tmp, *.bak, *.swp, npm-debug.log* |
Efímeros por definición. Nadie los va a consultar dentro de un año. |
| Bases de datos locales y volcados | *.sqlite, *.db, dump.sql, datos-locales/ |
Grandes, binarios y con datos que pueden ser personales. |
| Ficheros grandes y binarios generados | Vídeos de prueba, imágenes exportadas, *.zip, *.tar.gz |
Cada versión se guarda entera (lección 08-06). Si son binarios necesarios, la solución es Git LFS, lección 10-03. |
| Cobertura y resultados de pruebas | coverage/, .nyc_output/, junit.xml, .pytest_cache/ |
Salida de una ejecución concreta. |
| Cachés de herramientas | .cache/, .parcel-cache/, .eslintcache, .next/, .nuxt/ |
Reconstruibles y específicas de la máquina. |
El .gitignore que el equipo de gestor-tareas acabó adoptando:
# --- Dependencias ---------------------------------------------------
node_modules/
# --- Artefactos de compilación --------------------------------------
dist/
build/
*.min.js
*.min.css
# --- Secretos (ver 08-05) -------------------------------------------
.env
.env.*
!.env.ejemplo
*.pem
*.key
# --- Registros y temporales -----------------------------------------
*.log
npm-debug.log*
tmp/
*.tmp
*.bak
# --- Cobertura y cachés ---------------------------------------------
coverage/
.eslintcache
.cache/
# --- Sistema operativo ----------------------------------------------
# macOS (Bruno)
.DS_Store
._*
.Spotlight-V100
.Trashes
# Windows (Carla)
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Linux (Ana)
.directory
*~
# --- Editores -------------------------------------------------------
.idea/
*.swp
*.swo
.vscode/*
!.vscode/extensions.json
!.vscode/tasks.jsonFíjate en los dos usos de ! (negación) al final de bloques: !.env.ejemplo y !.vscode/extensions.json. Los explicamos en el apartado 5, junto con la trampa que esconden.
- Cómo funciona: precedencia y ámbito
Tres reglas gobiernan el comportamiento:
Regla 1: .gitignore se aplica por directorio y hacia abajo. Un .gitignore afecta a su propio directorio y a todos sus subdirectorios. Puedes tener varios en un mismo repositorio.
gestor-tareas/
├── .gitignore ← reglas globales del proyecto
├── app.js
├── imagenes/
│ ├── .gitignore ← reglas solo para imagenes/ y sus hijos
│ └── originales/
└── pruebas/
└── .gitignore ← reglas solo para pruebas/Regla 2: gana el patrón más específico. Git evalúa, en este orden de prioridad creciente:
core.excludesFile(global del usuario).git/info/exclude(local del repositorio).gitignorede los directorios, de la raíz hacia abajo (el más profundo gana)- La línea de órdenes (
git add -f)
Regla 3: dentro de un mismo fichero, gana la última línea que coincida. Por eso las negaciones van después del patrón que anulan:
!errores.log # esta negación no sirve de nada...
*.log # ...porque esta línea posterior vuelve a ignorarlo todoEl .gitignore se versiona. Es parte del proyecto y debe estar en el primer commit. Si no lo está, cada persona del equipo tiene que descubrir por su cuenta qué no debe subir, y alguien fallará.
- Sintaxis completa de los patrones
Git usa una sintaxis derivada de los globs de shell, con algunas particularidades propias.
| Patrón | Qué hace | Ejemplo de lo que casa |
|---|---|---|
nombre |
Cualquier fichero o directorio llamado así, a cualquier profundidad | app.log, src/app.log, a/b/c/app.log |
nombre/ |
Solo si es un directorio | logs/ sí; un fichero llamado logs no |
/nombre |
Solo en la raíz del directorio del .gitignore |
/build casa build en la raíz, no src/build |
* |
Cualquier secuencia de caracteres, sin cruzar / |
*.log casa error.log, no logs/error.log (aunque el nombre suelto sí) |
? |
Exactamente un carácter | imagen?.png casa imagen1.png, no imagen10.png |
[abc] |
Uno de esos caracteres | imagen[123].png casa imagen2.png |
[0-9] |
Un carácter del rango | registro[0-9].txt casa registro7.txt |
**/ |
Cualquier número de directorios (incluido ninguno) | **/temporal casa temporal, a/temporal, a/b/temporal |
/** |
Todo lo que hay dentro | logs/** casa todo el contenido de logs/ |
a/**/b |
b bajo a, a cualquier profundidad |
a/b, a/x/b, a/x/y/b |
!patron |
Des-ignora lo que casaría antes (con límites: apartado 5) | |
# texto |
Comentario (línea entera) | |
\#literal |
Un fichero que empieza por # |
Casa el fichero llamado #literal |
nombre\ |
Espacio final literal (los espacios finales se ignoran salvo si van escapados) |
Los dos matices que hay que interiorizar
La barra inicial ancla a la raíz. Es la diferencia más importante y la más olvidada:
build/ # ignora CUALQUIER directorio build, a cualquier profundidad
/build/ # ignora SOLO el build de la raíz del proyectoSi tu proyecto tiene build/ en la raíz y también src/componentes/build/ que sí quieres versionar, necesitas la barra inicial.
Una barra en medio del patrón también ancla. Esta es la regla que sorprende: si el patrón contiene una / en cualquier posición que no sea la final, se interpreta como relativo al directorio del .gitignore, no como "a cualquier profundidad".
doc/notas.txt # solo el doc/notas.txt de la raíz
notas.txt # cualquier notas.txt, a cualquier profundidad
**/doc/notas.txt # cualquier doc/notas.txt, a cualquier profundidadEjemplos comentados
# Todos los .log, estén donde estén
*.log
# Pero no los de la carpeta de ejemplos (ver apartado 5)
!ejemplos/*.log
# Solo el directorio build de la raíz
/build/
# Cualquier directorio llamado tmp, a cualquier profundidad
tmp/
# El fichero config.local.js de la raíz, no otros config.local.js
/config.local.js
# Cualquier fichero cuyo nombre empiece por "borrador"
borrador*
# Ficheros numerados del 0 al 9
volcado[0-9].sql
# Todo el contenido de datos/, pero conservando la carpeta (ver más abajo)
datos/*
!datos/.gitkeep
# Un fichero que se llama literalmente "!importante"
\!importanteEl truco de .gitkeep
Git no versiona directorios, solo ficheros: un directorio vacío simplemente no existe para Git. Si tu aplicación necesita que exista datos/ al arrancar, el patrón habitual es:
Y creas un fichero vacío datos/.gitkeep. El nombre no tiene nada de especial —Git no lo conoce—; es solo una convención para decir "este fichero existe para que el directorio exista".
Fíjate en que usamos datos/* y no datos/. La razón es la trampa del apartado siguiente.
- La negación con
! y su trampa
! y su trampaLa regla que más tiempo hace perder en todo .gitignore:
Si un directorio está ignorado, Git no entra en él. Por tanto, no se puede des-ignorar un fichero que esté dentro de un directorio ignorado.
Es una optimización deliberada: Git no recorre miles de ficheros de node_modules/ solo por si alguna regla posterior rescata alguno. Y produce este fallo, que parece un error de Git y no lo es:
datos/importante.csv sigue ignorado. Git ha descartado datos/ entero y nunca ha llegado a evaluar la segunda línea.
La forma correcta es ignorar el contenido, no el directorio:
Con datos/*, Git sí entra en datos/, ignora cada uno de sus hijos individualmente, y la negación puede rescatar uno.
Cuando hay varios niveles
Si el fichero que quieres rescatar está más profundo, hay que des-ignorar cada nivel intermedio:
Traducido: ignora todo (*), pero no ignores los directorios (!*/, necesario para que Git pueda entrar en ellos), y rescata explícitamente ese fichero.
Este patrón de lista blanca —"ignorar todo y rescatar lo que quiero"— es agresivo pero muy útil en repositorios donde lo versionable es una minoría (por ejemplo, un repositorio de configuración):
El caso del .gitignore de gestor-tareas
Volvamos a las dos negaciones que dejamos pendientes:
Funciona porque .env.* ignora ficheros, no un directorio, y la negación posterior rescata uno. Correcto.
Fíjate en .vscode/* y no .vscode/. Si hubiéramos escrito .vscode/, las dos negaciones no harían nada. Este es exactamente el error que comete todo el mundo la primera vez.
- Los tres niveles: proyecto, local y global
Hay tres sitios donde declarar exclusiones, y elegir mal es la causa de una discusión recurrente en el equipo: Carla propuso añadir .idea/ al .gitignore del proyecto y Ana se opuso porque nadie más usa ese IDE. Los dos tenían parte de razón, y el problema era de nivel.
| Nivel | Fichero | ¿Se versiona? | ¿A quién afecta? | Para qué |
|---|---|---|---|---|
| Proyecto | .gitignore en el repositorio |
Sí | A todo el equipo | Lo que es del proyecto: node_modules/, dist/, .env, coverage/ |
| Local | .git/info/exclude |
No | Solo a ti, solo en ese repositorio | Ficheros temporales tuyos en ese proyecto: notas-ana.md, pruebas-locales/ |
| Global | Ruta indicada por core.excludesFile |
No | A ti, en todos tus repositorios | Lo que es de tu máquina o tu herramienta: .DS_Store, .idea/, *.swp |
El criterio
¿Este fichero aparecería en la máquina de cualquiera que trabajase en este proyecto? Si sí, va en el
.gitignoredel proyecto. Si aparece solo por tu sistema operativo o tu editor, va en tu global. Si es un capricho tuyo en este repositorio concreto, va en.git/info/exclude.
Estrictamente, .DS_Store e .idea/ van en el global. En la práctica, casi todos los proyectos los ponen también en el .gitignore del proyecto, como red de seguridad para quien no tenga el global configurado. Es defensivo y es razonable; simplemente hay que saber que es una duplicación deliberada.
Configurar el global
# 1. Crear el fichero
cat > ~/.gitignore_global <<'EOF'
# Sistema operativo
.DS_Store
._*
Thumbs.db
Desktop.ini
.directory
# Editores
.idea/
*.swp
*.swo
*~
.vscode/
# Herramientas personales
.env.personal
notas-privadas.md
EOF
# 2. Decírselo a Git
git config --global core.excludesFile ~/.gitignore_global
# 3. Comprobar
git config --get core.excludesFileEn macOS y Linux, Git ya usa ~/.config/git/ignore sin necesidad de configurar nada si el fichero existe. Es la ubicación por defecto según el estándar XDG:
.git/info/exclude
Es un .gitignore normal que vive dentro de .git/. Como todo lo que hay ahí (lección 01-04), no se versiona ni se clona:
cat >> .git/info/exclude <<'EOF'
# Mis cosas en este repositorio, que nadie más necesita ignorar
notas-ana.md
pruebas-locales/
volcado-de-hoy.sql
EOFVentaja frente a modificar el .gitignore del proyecto: no ensucias el fichero compartido con tus manías, y no tienes que justificarlas en una revisión. Desventaja: se pierde si borras y vuelves a clonar el repositorio.
- La regla que confunde a todo el mundo: solo afecta a lo no seguido
Aquí está la fuente del 90 % de las preguntas sobre .gitignore:
.gitignoresolo afecta a ficheros SIN SEGUIMIENTO. Si un fichero ya está en el índice, Git seguirá registrando sus cambios por muchos patrones que escribas.
Recuerda los tres estados de la lección 01-03 y el ciclo de la lección 02-03. .gitignore actúa exclusivamente sobre la transición de sin seguimiento a preparado: le dice a Git "ni me lo propongas en git status, ni lo cojas con git add .". Sobre un fichero que ya cruzó esa frontera, no tiene ningún efecto.
El escenario típico, que le pasó a Bruno:
# Lunes: Bruno sube node_modules sin darse cuenta
git add .
git commit -m "chore: primer commit"
git push# Martes: Ana lo ve y añade el .gitignore
echo "node_modules/" >> .gitignore
git add .gitignore && git commit -m "chore: ignora node_modules"
git statusEn la rama main Cambios no rastreados para el commit: modificado: node_modules/marked/package.json modificado: node_modules/.package-lock.json ...
Sigue ahí. El .gitignore no ha hecho nada, porque esos ficheros ya están seguidos.
flowchart LR
A["Sin seguimiento"] -->|"git add"| B["Preparado"]
B -->|"git commit"| C["Confirmado / seguido"]
A -.->|".gitignore actúa AQUÍ<br/>y solo aquí"| A
C -->|"git rm --cached"| A
La flecha de vuelta —git rm --cached— es la única forma de devolver un fichero al estado donde .gitignore puede protegerlo.
- Sacar del seguimiento con
git rm --cached
git rm --cachedgit rm --cached quita el fichero del índice pero lo deja en tu disco. A partir de ese momento pasa a estar sin seguimiento, el .gitignore empieza a aplicarse y desaparece de git status.
# Un fichero
git rm --cached config.js
# Un directorio entero (recursivo)
git rm -r --cached node_modules/
# Comprobar antes de tocar nada (--dry-run no modifica nada)
git rm -r --cached --dry-run node_modules/El flujo completo, tal como lo hizo Ana:
# 1. Asegurarse de que el patrón está en el .gitignore
grep -q "^node_modules/" .gitignore || echo "node_modules/" >> .gitignore
# 2. Sacar del índice, conservando en disco
git rm -r --cached node_modules/
# 3. Verificar que git status queda limpio
git status --short
# 4. Confirmar
git commit -m "chore: deja de versionar node_modules"
# 5. Avisar al equipo (ver más abajo)
git pushEl truco para limpiarlo todo de golpe
Cuando hay muchos ficheros que ahora deberían estar ignorados, hay una receta que los saca todos a la vez respetando el .gitignore actual:
# Vacía el índice y lo reconstruye aplicando el .gitignore vigente
git rm -r --cached .
git add .
git status
git commit -m "chore: aplica el .gitignore a los ficheros ya versionados"git rm -r --cached . no borra nada del disco: vacía el índice. git add . lo rellena de nuevo, y esta vez sí respeta el .gitignore. La diferencia entre ambos estados es exactamente el conjunto de ficheros que sobraban.
Comprueba siempre con
git statusantes de confirmar. Si el.gitignoretiene un patrón demasiado amplio, esta receta puede sacar del seguimiento ficheros que sí querías.
Las dos advertencias importantes
Advertencia 1: el fichero se borra en las demás máquinas. Para Git, un git rm --cached seguido de commit es un borrado. Cuando Bruno y Carla hagan pull, ese fichero desaparecerá de su copia de trabajo. Con node_modules/ da igual (se regenera con npm ci), pero si sacas del seguimiento un config.js que la gente necesita, se lo estás borrando. Avisa al equipo antes de hacerlo.
Advertencia 2: no borra nada del historial. Y esta es crítica:
El fichero desaparece de la punta, pero sigue en todos los commits anteriores:
git log --all --oneline -- config.js # ahí siguen
git show HEAD~5:config.js # y su contenido es accesibleSi config.js contenía una contraseña, la contraseña sigue en el repositorio y en todos los clones. git rm no es una herramienta de seguridad. El procedimiento correcto para un secreto ya filtrado —rotar primero la credencial, luego reescribir el historial— es el contenido de la lección 08-05.
- Depurar con
git check-ignore -v
git check-ignore -vLlega el día en que un fichero se ignora y no sabes por qué, o al revés. git check-ignore -v responde: te dice qué fichero de exclusión, qué línea y qué patrón están decidiendo.
Se lee: el patrón dist/, en la línea 8 del fichero .gitignore, es el que ignora dist/app.min.js.
Ejemplos de las tres respuestas posibles:
Opciones útiles
# Varios ficheros de una vez
git check-ignore -v app.js dist/app.min.js .DS_Store node_modules/marked/index.js
# Mostrar también los que NO se ignoran, y por qué
git check-ignore -v --no-index app.js
# Comprobar TODO lo que git status oculta
git status --ignored --shortgit status --ignored merece un apartado propio, porque es la forma de descubrir sorpresas:
Cada !! es un fichero o directorio que Git está ocultando. Si ves ahí algo que sí querías versionar, ya sabes que un patrón es demasiado amplio.
El caso especial de --no-index
Si un fichero está seguido, check-ignore puede resultar confuso, porque los patrones sí casan aunque no tengan efecto. Para preguntar "¿casaría algún patrón, con independencia de si está seguido?":
Es decir: hay un patrón que lo cubriría, pero como el fichero ya está en el índice, no surte efecto. Ese diagnóstico —"el patrón está bien, el problema es el apartado 7"— es exactamente lo que necesitas saber.
- Forzar con
git add -f
git add -fA veces hace falta versionar un fichero que un patrón amplio está ignorando. git add -f (o --force) salta las reglas para esa operación concreta:
Sin -f, Git avisa y no hace nada:
Las siguientes rutas son ignoradas por alguno de tus ficheros .gitignore: dist/app.min.js Usa -f si de verdad quieres añadirlas.
Cuidado con lo que ocurre después. Una vez añadido con -f, el fichero pasa a estar seguido, y por la regla del apartado 7 el .gitignore deja de tener efecto sobre él para siempre: cada modificación aparecerá en git status. -f no es una excepción puntual, es un cambio de estado permanente.
Por eso casi siempre es mejor arreglar el patrón que forzar:
Así la excepción queda documentada en el repositorio en lugar de vivir en la memoria de quien un día escribió -f.
Y una advertencia que enlaza con la lección siguiente: -f es la vía por la que se cuelan los secretos. Alguien tiene prisa, Git se queja de que .env está ignorado, y git add -f .env resuelve el problema de los próximos cinco minutos a cambio de crear uno permanente. Si Git se resiste a añadir un fichero, párate a pensar por qué.
- Plantillas por lenguaje
No hace falta escribir un .gitignore desde cero. Hay colecciones mantenidas por la comunidad con plantillas por lenguaje, framework, sistema operativo y editor; la más conocida es el repositorio github/gitignore, y la mayoría de plataformas ofrecen elegir una al crear el repositorio.
Recomendaciones de uso:
- Parte de la plantilla de tu lenguaje, pero léela. Contiene reglas para herramientas que quizá no uses, y a veces ignora cosas que tú sí quieres.
- Combina, no concatenes a ciegas. Una plantilla de Node más una de Python más tres de editores producen un fichero de 300 líneas que nadie mantiene.
- Agrupa por secciones con comentarios, como en el ejemplo del apartado 2. Un
.gitignorese lee muchas veces. - Las reglas de sistema operativo y editor, a tu global. Si todo el equipo tiene el global bien configurado, el
.gitignoredel proyecto queda corto y significativo. - Revísalo cuando cambien las herramientas. Un
.gitignorecon reglas para un empaquetador que dejasteis de usar hace dos años es ruido.
Una comprobación útil de vez en cuando, para ver qué reglas ya no sirven para nada:
# Ficheros ignorados que existen realmente en tu copia de trabajo
git status --ignored --short | grep '^!!'Si una regla no aparece nunca aquí en ninguna máquina del equipo, probablemente sobra.
- El patrón para secretos:
.env y .env.ejemplo
.env y .env.ejemploEl problema real: la aplicación necesita saber la URL de la base de datos, la clave de la API de correo y un secreto de sesión. Esos valores no pueden estar en el repositorio, pero quien clone el proyecto tiene que saber qué valores necesita y cómo se llaman.
La solución convencional son dos ficheros:
.env — con los valores reales. Ignorado, nunca versionado.
BASE_DATOS_URL=postgres://gestor:una-clave-de-verdad@localhost:5432/tareas
API_CORREO_CLAVE=el-valor-real-que-no-va-al-repositorio
SESION_SECRETO=otro-valor-real-generado-al-azar
PUERTO=3000.env.ejemplo — con las claves y valores de muestra, sin nada real. Versionado.
# Copia este fichero a .env y rellena los valores reales.
# El .env NUNCA se versiona (ver .gitignore).
# Cadena de conexión a PostgreSQL
BASE_DATOS_URL=postgres://usuario:contrasena@localhost:5432/tareas
# Clave del servicio de envío de correo.
# Pídesela a Ana o genera una de pruebas en el panel del servicio.
API_CORREO_CLAVE=pon-aqui-tu-clave
# Secreto de sesión. Genera uno con: openssl rand -hex 32
SESION_SECRETO=cambia-esto-por-un-valor-aleatorio
# Puerto local del servidor de desarrollo
PUERTO=3000Y en el .gitignore, con la negación que ya conocemos:
Por qué funciona bien:
- Diego clona su fork, copia
.env.ejemploa.env, rellena sus valores y arranca. La documentación de qué hace falta está en el repositorio, actualizada, porque forma parte del código. - Cuando alguien añade una variable nueva, la revisión de la PR detecta que falta en
.env.ejemplo. Es un buen candidato a comprobación automática en el CI. - Los valores de muestra son evidentemente falsos (
pon-aqui-tu-clave), así que nadie los confunde con reales ni intenta usarlos.
Los tres errores que arruinan el patrón:
- Poner valores reales en
.env.ejemplo"para que sea más cómodo". Entonces el fichero versionado es el que tiene el secreto, y no has ganado nada. - Escribir
.env*sin la negación. El.env.ejemplotambién queda ignorado, nadie lo sube, y el patrón entero deja de existir. - Añadir el
.gitignoredemasiado tarde. Si.envya se confirmó alguna vez,git rm --cachedlo quita de la punta pero no del historial (apartado 8). El secreto sigue ahí.
Ese tercer caso —el secreto ya filtrado— tiene un procedimiento propio, con un orden de pasos que importa mucho: primero se rota la credencial, después se reescribe el historial. Es el núcleo de la lección 08-05.
Errores Comunes y Consejos
Error 1: creer que .gitignore deja de seguir un fichero ya versionado. Es el malentendido número uno. Solo actúa sobre ficheros sin seguimiento; para el resto, git rm --cached.
Error 2: datos/ cuando querías datos/*. Si el directorio está ignorado, Git no entra y ninguna negación posterior funciona. Ignora el contenido, no el directorio.
Error 3: poner la negación antes del patrón. Gana la última línea que coincide. !errores.log seguido de *.log no sirve de nada.
Error 4: olvidar la barra inicial. build/ ignora cualquier build del proyecto; /build/ solo el de la raíz. Suele importar más de lo que parece.
Error 5: usar git add -f como solución habitual. Convierte el fichero en seguido para siempre y la excepción queda sin documentar. Arregla el patrón y escribe la negación.
Error 6: .env* sin !.env.ejemplo. Rompe el patrón de secretos entero.
Error 7: confiar en git rm --cached para eliminar un secreto. Solo lo quita de la punta. El historial y todos los clones lo conservan. Lección 08-05.
Error 8: llenar el .gitignore del proyecto con reglas personales. .idea/ y *.swp son de tu máquina; van a core.excludesFile. El .gitignore del proyecto debería contener solo lo que le pasaría a cualquiera.
Consejo 1: crea el .gitignore en el primer commit. Antes de instalar dependencias. Es infinitamente más barato que limpiarlo después.
Consejo 2: git status --ignored de vez en cuando. Es la única forma de ver qué está ocultando Git, y de detectar patrones demasiado amplios.
Consejo 3: git check-ignore -v en cuanto dudes. Responde en un segundo lo que si no se convierte en media hora de prueba y error.
Consejo 4: configura tu core.excludesFile una vez en la vida. Te ahorra ensuciar todos los repositorios en los que trabajes.
Consejo 5: comenta el .gitignore por secciones. Y explica los patrones raros. Dentro de un año nadie recordará por qué !config/produccion/ajustes.json está ahí.
Consejo 6: valida .env.ejemplo en el CI. Una comprobación que compare las claves de .env.ejemplo con las que el código lee de process.env evita que se desincronicen.
Ejercicios
Ejercicio 1: la trampa de la negación
En un repositorio de pruebas, crea esta estructura:
- Escribe un
.gitignorecondatos/y!datos/publico.csv. Comprueba congit statusy congit check-ignore -vquepublico.csvsigue ignorado, y explica por qué. - Corrígelo para que
publico.csvse versione y todo lo demás siga ignorado. - Ahora consigue que además se versione
datos/copias/privado-2026.csv, sin versionarprivado.csv. - Verifica cada paso con
git check-ignore -v.
Ejercicio 2: limpiar un repositorio ya contaminado
Simula el desastre de Bruno:
- Crea un repositorio, un
app.js, un directorionode_modules/con tres ficheros, un.envcon una contraseña ficticia y un.DS_Store. - Confírmalo todo sin
.gitignore(el error de partida). - Añade ahora un
.gitignorecorrecto, con el patrón de secretos del apartado 12. - Comprueba que
git statussigue mostrando los ficheros que deberían estar ignorados y explica por qué. - Sácalos del seguimiento con la receta del apartado 8, sin borrarlos del disco.
- Verifica que
git statusqueda limpio, que los ficheros siguen en tu disco y quegit status --ignoredlos muestra como ignorados. - Demuestra que la contraseña del
.envsigue accesible en el historial y di qué lección lo resuelve.
Ejercicio 3: los tres niveles
- Configura un
core.excludesFileglobal con las reglas de tu sistema operativo y tu editor. - En un repositorio de pruebas, añade a
.git/info/excludeun ficheronotas-personales.md. - Añade al
.gitignoredel proyecto la regladist/. - Crea los tres tipos de fichero y comprueba con
git check-ignore -vque cada uno se ignora por el nivel correcto. - Para cada uno de estos ficheros, decide razonadamente en qué nivel debería estar la regla:
node_modules/,.idea/workspace.xml,pruebas-de-carla.js,coverage/,.DS_Store,.env.
Soluciones
Solución 1:
mkdir -p /tmp/practica-ignore/datos/copias && cd /tmp/practica-ignore
git init -b main
echo "a,b" > datos/publico.csv
echo "clave,valor" > datos/privado.csv
echo "hist" > datos/copias/privado-2026.csv# 1. La versión que NO funciona
cat > .gitignore <<'EOF'
datos/
!datos/publico.csv
EOF
git status --shortdatos/publico.csv no aparece: sigue ignorado.
El diagnóstico es explícito: quien decide es la línea 1, con el patrón datos/. La negación de la línea 2 nunca se evalúa, porque Git, al ignorar el directorio datos/, no entra en él. Es una optimización deliberada de rendimiento.
# 2. La versión correcta: ignorar el contenido, no el directorio
cat > .gitignore <<'EOF'
datos/*
!datos/publico.csv
EOF
git status --shortgit check-ignore -v datos/publico.csv # sin salida, código 1: NO ignorado
git check-ignore -v datos/privado.csv# 3. Rescatar también un fichero de un subdirectorio
cat > .gitignore <<'EOF'
datos/*
!datos/publico.csv
!datos/copias/
datos/copias/*
!datos/copias/privado-2026.csv
EOF
git status --shortLa clave del paso 3: hay que des-ignorar el directorio intermedio (!datos/copias/) para que Git entre en él, y luego volver a ignorar su contenido (datos/copias/*) para poder rescatar solo lo que interesa. Cada nivel de profundidad exige su propio par de líneas.
Solución 2:
mkdir -p /tmp/practica-limpieza/node_modules/marked && cd /tmp/practica-limpieza
git init -b main
echo "console.log('gestor-tareas');" > app.js
echo '{"name":"marked"}' > node_modules/marked/package.json
echo "modulo" > node_modules/marked/index.js
echo "{}" > node_modules/.package-lock.json
echo "BASE_DATOS_URL=postgres://gestor:clave-ficticia@localhost/tareas" > .env
touch .DS_Store.DS_Store .env app.js node_modules/.package-lock.json node_modules/marked/index.js node_modules/marked/package.json
# 3. El .gitignore que debería haber existido desde el principio
cat > .gitignore <<'EOF'
node_modules/
.env
.env.*
!.env.ejemplo
.DS_Store
EOF
cat > .env.ejemplo <<'EOF'
# Copia a .env y rellena los valores reales.
BASE_DATOS_URL=postgres://usuario:contrasena@localhost:5432/tareas
EOFSigue apareciendo porque ya está seguido. .gitignore solo actúa en la frontera "sin seguimiento → preparado", y estos ficheros la cruzaron en el paso 2.
D .DS_Store D .env A .env.ejemplo A .gitignore D node_modules/.package-lock.json D node_modules/marked/index.js D node_modules/marked/package.json
Las D son exactamente los ficheros que sobraban. app.js no aparece porque su contenido no ha cambiado.
# 6. Verificación
git status --short # vacío
ls -a # .env, .DS_Store y node_modules/ SIGUEN en disco
git status --ignored --shortAhí está. Cualquiera con acceso al repositorio, ahora o dentro de cinco años, puede recuperarla con un solo comando, y todos los clones existentes la contienen. git rm --cached protege el futuro, no repara el pasado. El procedimiento correcto —rotar primero la credencial y después reescribir el historial con git filter-repo— es el contenido de la lección 08-05.
Solución 3:
# 1. Global
cat > ~/.gitignore_global <<'EOF'
.DS_Store
Thumbs.db
.idea/
*.swp
*~
EOF
git config --global core.excludesFile ~/.gitignore_global# 2 y 3. Local del repositorio y del proyecto
mkdir /tmp/practica-niveles && cd /tmp/practica-niveles && git init -b main
echo "notas-personales.md" >> .git/info/exclude
echo "dist/" > .gitignore# 4. Un fichero de cada tipo
mkdir dist && touch dist/app.min.js notas-personales.md .DS_Store app.js
git check-ignore -v dist/app.min.js notas-personales.md .DS_Store app.js.gitignore:1:dist/ dist/app.min.js .git/info/exclude:6:notas-personales.md notas-personales.md /home/ana/.gitignore_global:1:.DS_Store .DS_Store
app.js no aparece: no está ignorado por ninguna regla. Cada uno de los otros tres se ignora exactamente por el nivel que le corresponde.
5. Dónde va cada regla:
| Fichero | Nivel correcto | Razón |
|---|---|---|
node_modules/ |
Proyecto | Es del proyecto: le aparecerá a todo el que ejecute npm install. Sin él, cualquiera puede subirlo. |
.idea/workspace.xml |
Global | Es de tu IDE, no del proyecto. Ana con Vim y Bruno con VS Code no lo generan nunca. (Muchos proyectos lo duplican en el .gitignore como red de seguridad; es una decisión defensiva legítima.) |
pruebas-de-carla.js |
.git/info/exclude |
Es de Carla y solo de este repositorio. No tiene por qué aparecer en el fichero compartido ni justificarse en una revisión. |
coverage/ |
Proyecto | Lo genera la herramienta de pruebas del proyecto en la máquina de cualquiera. |
.DS_Store |
Global | Lo genera macOS, no el proyecto. Que Bruno lo tenga en su global protege a todos; ponerlo también en el proyecto es red de seguridad. |
.env |
Proyecto, sin ninguna duda | Es la regla de seguridad más importante del repositorio y no puede depender de que cada persona tenga bien configurado su global. Va en el .gitignore versionado, siempre. |
La última fila es el criterio de fondo: cuanto mayor sea la consecuencia de que la regla falte, más arriba debe estar y más versionada.
Conclusión
Lo esencial de esta lección:
- Se versiona la fuente, no el resultado. Fuera quedan dependencias, artefactos de compilación, ficheros del sistema operativo, configuración personal del editor, registros, temporales, binarios grandes y —por encima de todo— secretos. Dentro se quedan los ficheros de bloqueo de dependencias y la configuración compartida del proyecto.
- La sintaxis tiene tres reglas que hay que interiorizar: la barra final significa "solo directorios", la barra inicial ancla a la raíz, y cualquier barra en medio del patrón también ancla. Dentro de un fichero, gana la última línea que coincide.
- La negación con
!tiene un límite duro: no se puede des-ignorar un fichero dentro de un directorio ignorado, porque Git ni siquiera entra. Se ignora el contenido (datos/*), no el directorio (datos/). - Hay tres niveles: el
.gitignoredel proyecto (versionado, para lo que le pasa a cualquiera),.git/info/exclude(local, para tus cosas en ese repositorio) ycore.excludesFile(global, para lo que genera tu sistema operativo o tu editor). Cuanto más grave sea la consecuencia de que falte una regla, más arriba y más versionada debe estar. - La regla que confunde a todo el mundo:
.gitignoresolo afecta a ficheros sin seguimiento. Sobre lo ya versionado no tiene ningún efecto. La vuelta atrás esgit rm --cached, que quita del índice pero conserva en disco, con dos advertencias: borra el fichero en las demás máquinas al hacerpull, y no toca el historial. git check-ignore -vresponde en un segundo qué fichero, qué línea y qué patrón deciden.git status --ignoredmuestra lo que Git te está ocultando.git add -ffuerza, pero convierte el fichero en seguido para siempre. Casi siempre es mejor escribir la excepción como negación, para que quede documentada.- El patrón para secretos es
.envignorado +.env.ejemploversionado con claves y valores evidentemente falsos, más la negación!.env.ejemplo. Es la mejor documentación posible de qué configuración necesita el proyecto.
gestor-tareas ya sabe qué ficheros no deben entrar. Queda el otro lado del problema: los ficheros que sí entran, pero que Git no debería tratar como si todos fueran iguales. Un PNG no es un fichero de texto y no tiene sentido intentar fusionarlo. Un CHANGELOG.md sí querría fusionarse de una forma especial. Y, sobre todo, está el problema que Carla lleva arrastrando desde el módulo 1: cada vez que edita un fichero en Windows, el git diff marca todas las líneas como modificadas.
Ese es el terreno de la lección 08-04: Atributos de Fichero con .gitattributes.
Dominando Git: De Principiante a Avanzado
Módulo 1: Introducción a Git
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
