Carla ya ha arreglado el borrado de tareas gracias a bisect. Pero mientras estaba en app.js se ha encontrado con esto:
function normalizarTexto(texto) {
return texto
.replace(/ /g, ' ')
.replace(/[-]/g, '')
.trim();
}Nadie del equipo recuerda haber escrito esa función. No está documentada, no tiene comentarios y su aspecto es el de un parche defensivo contra algo muy concreto. La tentación de borrarla es enorme: son cinco líneas que "no hacen nada".
Esa tentación es exactamente el escenario que describe la valla de Chesterton: no quites una valla del medio del campo hasta que sepas por qué la pusieron. Y en un repositorio Git siempre se puede saber, porque cada línea de cada fichero procede de un commit concreto, con su autor, su fecha y su mensaje.
La herramienta que hace esa consulta es git blame, y en esta lección vamos a exprimirla: no solo para ver quién tocó la línea por última vez, sino para atravesar el ruido —reformateos, código movido, cambios de espaciado— y llegar al commit que de verdad explica el porqué.
Contenido
- Qué es exactamente
git blame - Leer la salida
- Acotar:
-Lpor líneas y por función - Ver más allá del ruido:
-w,-My-C - Viajar en el tiempo:
<sha>^y--since git log -L: la evolución completa de un rango de líneas- Ignorar commits de formateo:
--ignore-revy.git-blame-ignore-revs - Resolviendo el misterio de
normalizarTexto - Blame en la práctica: atribución, no culpa
- Qué es exactamente
git blame
git blamegit blame <fichero> responde a una pregunta muy concreta:
Para cada línea del fichero tal y como está ahora, ¿cuál fue el último commit que la modificó?
Conviene subrayar tres cosas de esa definición, porque los tres malentendidos más comunes salen de ahí:
- Es "el último que la tocó", no "el que la escribió". Si Ana escribió la línea en enero y Bruno le cambió una coma en junio,
blameseñala a Bruno. Los apartados 4 y 7 son precisamente las herramientas para atravesar ese ruido. - Trabaja sobre el estado actual del fichero. Las líneas borradas no aparecen: si te interesan, la herramienta es
git log -L(apartado 6) o el pickaxe-Sde la lección 02-06. - No calcula nada nuevo. Toda la información ya está en el modelo de datos de la lección 01-04:
blamerecorre el historial del fichero comparando versiones y atribuye cada línea. Es una consulta, no un metadato guardado.
- Leer la salida
9f3c7a1e (Ana Ferrer 2026-04-12 09:31:44 +0200 1) const lista = document.querySelector('#lista-tareas');
9f3c7a1e (Ana Ferrer 2026-04-12 09:31:44 +0200 2) const formulario = document.querySelector('#formulario-tarea');
4e8b2c9d (Bruno Salas 2026-05-03 17:22:08 +0200 3) const campo = document.querySelector('#nueva-tarea');
^7d2a1f8 (Ana Ferrer 2026-03-28 11:05:12 +0200 4)
2c6e9a4f (Carla Vidal 2026-05-19 14:47:31 +0200 5) let tareas = cargarTareas();
00000000 (No Cometido 2026-08-01 10:12:03 +0200 6) let filtroActivo = 'todas';Las cinco columnas, de izquierda a derecha:
| Columna | Contenido | Detalle |
|---|---|---|
| 1 | Hash abreviado del commit | Es el commit que tocó esa línea por última vez |
| 2 | Autor | El autor, no el committer (la distinción de la lección 01-06) |
| 3 | Fecha y hora | Fecha de autoría, con zona horaria |
| 4 | Número de línea | En el fichero actual |
| 5 | Contenido | La línea tal cual |
Y dos marcas especiales que hay que saber leer:
^7d2a1f8, con acento circunflejo: la línea viene del commit más antiguo del rango analizado. Suele ser el commit inicial (boundary commit). Significa "esto es tan viejo como el propio historial que estoy mirando".00000000/ "No Cometido" (Not Committed Yeten inglés): la línea está modificada en el árbol de trabajo y aún no se ha confirmado. Es tuya, ahora mismo.
Opciones de formato que ahorran mucho tiempo:
git blame -s app.js # solo hash y línea: compacto, sin autor ni fecha
git blame -e app.js # muestra el email en lugar del nombre
git blame --date=short app.js # fechas como 2026-04-12, sin hora ni zona
git blame -l app.js # hashes completos de 40 caracteres
git blame -c app.js # formato compatible con git annotateY el que de verdad se usa a diario, porque combina lo justo:
Un detalle práctico: la salida de blame es larga y suele pasar por el paginador. Para saltar directamente a la zona que te interesa, en less basta con /normalizarTexto + Enter. Pero es mucho mejor acotar de entrada, que es lo siguiente.
- Acotar:
-L por líneas y por función
-L por líneas y por funciónCasi nunca quieres el fichero entero. -L limita el análisis a un rango:
8d4f2a7c (Bruno Salas 2026-06-22 11:14:38 +0200 78) lista.addEventListener('click', (e) => {
8d4f2a7c (Bruno Salas 2026-06-22 11:14:38 +0200 79) if (e.target.closest('.borrar')) {
8d4f2a7c (Bruno Salas 2026-06-22 11:14:38 +0200 80) borrarTarea(e.target.closest('.borrar').dataset.id);Formas de expresar el rango, todas válidas:
| Sintaxis | Significado |
|---|---|
-L 78,92 |
De la línea 78 a la 92 |
-L 78,+15 |
Quince líneas a partir de la 78 |
-L 78,-5 |
De la 73 a la 78 |
-L 78 |
De la 78 al final del fichero |
-L ,20 |
Del principio a la 20 |
-L :normalizarTexto:app.js |
El bloque de esa función, sin contar líneas |
-L '/^function borrarTarea/',+30 |
Desde la línea que casa con la expresión regular |
La forma -L :<nombre>:<fichero> es la más cómoda y la menos conocida. Git localiza el bloque de la función usando las mismas reglas de funcname que emplea git diff para las cabeceras de los hunks (lección 02-05):
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 43) function normalizarTexto(texto) {
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 44) return texto
3f1a8d6c (Ana Ferrer 2026-05-30 10:18:22 +0200 45) .replace(/ /g, ' ')
3f1a8d6c (Ana Ferrer 2026-05-30 10:18:22 +0200 46) .replace(/[-]/g, '')
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 47) .trim();
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 48) }Primera pista del misterio: la función la creó Carla en febrero, y Ana añadió los dos replace en mayo. Dos commits, dos motivos posiblemente distintos.
El reconocimiento de funciones depende del lenguaje. Git trae patrones integrados para muchos (
c,java,python,php,rust...). Para JavaScript, el patrón integradojavascriptse activa declarándolo en.gitattributescon*.js diff=javascript. Ese fichero, y todo lo que permite configurar, es el tema de la lección 08-04; aquí basta saber que si-L :funcion:no acierta, esa es la razón.
- Ver más allá del ruido:
-w, -M y -C
-w, -M y -CAquí está el verdadero valor de blame, y lo que separa el uso ingenuo del uso experto. La atribución "en bruto" miente constantemente, porque:
- Alguien reindentó el fichero → todas las líneas son suyas.
- Alguien movió una función de sitio dentro del mismo fichero → todas las líneas son suyas.
- Alguien extrajo un módulo a un fichero nuevo → todas las líneas son suyas.
En los tres casos, el contenido no cambió, y blame está señalando al mensajero. Estas tres opciones lo corrigen:
-w: ignorar los cambios de espaciado
Ignora las diferencias que son solo de espacios en blanco al comparar versiones. Si Bruno pasó el fichero por el formateador y cambió la indentación de 4 a 2 espacios, -w mira a través de ese commit y sigue atribuyendo la línea a quien escribió el código real.
Es prácticamente gratis y casi nunca estorba. Ponlo siempre.
-M: seguir código movido dentro del mismo fichero
Detecta bloques de líneas que se movieron o copiaron dentro del mismo fichero y atribuye la línea a su origen, no al commit que la movió. Es el caso de "he reordenado las funciones para agrupar las de almacenamiento".
-C: seguir código movido entre ficheros
Detecta líneas que vienen de otro fichero modificado en el mismo commit. Es el caso de extraer código a un módulo nuevo.
-C tiene niveles, según cuántas veces se repita, y esta es la tabla que hay que tener a mano:
| Opción | Qué busca | Coste |
|---|---|---|
-M |
Movimientos y copias dentro del mismo fichero | Bajo |
-C |
Además, código venido de otros ficheros modificados en el mismo commit | Medio |
-C -C |
Además, código venido de cualquier fichero del commit que creó el fichero | Alto |
-C -C -C |
Además, código venido de cualquier fichero de cualquier commit | Muy alto |
Y se puede afinar el umbral de detección con un número: -M20 exige al menos 20 caracteres alfanuméricos para considerar que un bloque se movió (por defecto son 40 para -M).
En la práctica, la invocación que resuelve el 95 % de las investigaciones serias es:
Nótese que se puede combinar todo: ignorar espacios, seguir movimientos entre ficheros con nivel medio-alto, y acotado a una función. Es más lento, pero en un fichero de unos cientos de líneas ni se nota.
Comparación práctica del efecto, sobre las mismas líneas:
c9a2e7f4 (Bruno Salas 2026-07-11 09:02:17 +0200 43) function normalizarTexto(texto) {
c9a2e7f4 (Bruno Salas 2026-07-11 09:02:17 +0200 44) return texto
c9a2e7f4 (Bruno Salas 2026-07-11 09:02:17 +0200 45) .replace(/ /g, ' ')b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 43) function normalizarTexto(texto) {
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 44) return texto
3f1a8d6c (Ana Ferrer 2026-05-30 10:18:22 +0200 45) .replace(/ /g, ' ')El primer resultado señala a Bruno y a un commit de julio: es el commit en el que Bruno movió la función de index.html a app.js. Informativamente, es inútil. El segundo señala a los commits que escribieron el contenido, que es lo que buscábamos.
- Viajar en el tiempo:
<sha>^ y --since
<sha>^ y --sinceblame acepta un punto de partida distinto de HEAD:
git blame v1.0.0 -- app.js # el fichero tal como estaba en la etiqueta
git blame 8d4f2a7 -- app.js # tal como estaba en ese commitDe ahí sale la técnica más útil de todo el apartado: saltar por encima de un commit para ver qué había antes.
Cuando blame te dice que la línea 45 la tocó 3f1a8d6 y ese commit resulta ser un reformateo, quieres saber quién la tocó antes. La respuesta es blamear el padre de ese commit:
3f1a8d6^ es "el padre de 3f1a8d6" (lección 02-06). Al blamear el fichero en ese punto, el commit 3f1a8d6 deja de existir para el análisis y aparece el anterior. Repitiendo el proceso se puede ir retrocediendo capa a capa:
git blame 3f1a8d6^ -- app.js | sed -n '45p'
# → 1c8f4b2 (Carla Vidal 2026-03-02 ...)
git blame 1c8f4b2^ -- app.js | sed -n '45p'
# → b7e2c4a9 (Carla Vidal 2026-02-14 ...)Es tedioso a mano, y por eso existe git log -L (apartado 6), que hace ese recorrido de una vez. Pero conviene conocer la técnica, porque funciona siempre y en cualquier versión de Git.
Otras formas de acotar el rango temporal:
Con --since, las líneas cuyo último cambio es anterior al límite aparecen marcadas con ^ como líneas de frontera: "esto ya estaba antes del periodo que te interesa". Es útil para responder a "¿qué se ha tocado de este fichero este trimestre?" sin ruido de hace tres años.
git log -L: la evolución completa de un rango de líneas
git log -L: la evolución completa de un rango de líneasblame da una foto: el estado actual y quién tocó cada línea por última vez. git log -L da la película: todos los commits que han afectado a un rango de líneas, con su diff.
commit 3f1a8d6c9e2b4a7f1d5c8e3b6a9f2d4c7e1b5a8f Author: Ana Ferrer <[email protected]> Date: Fri May 30 10:18:22 2026 +0200 Eliminar caracteres invisibles al normalizar el texto de las tareas Al pegar texto desde el gestor de correo, las tareas llegaban con espacios duros (U+00A0) y marcas de anchura cero. El resultado era que dos tareas visualmente idénticas no se detectaban como duplicadas y que el filtro por texto no encontraba nada. diff --git a/app.js b/app.js --- a/app.js +++ b/app.js @@ -43,5 +43,7 @@ function normalizarTexto(texto) { return texto + .replace(/ /g, ' ') + .replace(/[-]/g, '') .trim(); } commit b7e2c4a9f1d6c3e8b5a2f7d9c4e1b8a6f3d5c2e9 Author: Carla Vidal <[email protected]> Date: Sat Feb 14 16:03:55 2026 +0100 Normalizar el texto de las tareas antes de guardarlas Los espacios sobrantes al principio y al final provocaban tareas duplicadas aparentes. diff --git a/app.js b/app.js --- a/app.js +++ b/app.js @@ -41,0 +43,5 @@ +function normalizarTexto(texto) { + return texto + .trim(); +}
Misterio resuelto en un solo comando. Los dos replace no son arbitrarios: protegen de los caracteres invisibles que llegan al pegar texto desde el correo. Borrar esa función habría reintroducido un fallo real y difícil de reproducir.
Las formas de -L son las mismas que en blame:
git log -L 43,48:app.js # rango de líneas
git log -L :normalizarTexto:app.js # una función
git log -L '/^function borrarTarea/',+20:app.jsCombinaciones muy útiles:
git log -L :normalizarTexto:app.js --oneline # solo la lista de commits, sin diffs
git log -L :normalizarTexto:app.js -n 3 # los tres más recientes
git log -L :guardarTareas:app.js -L :cargarTareas:app.js # dos rangos a la vezLa comparación entre las dos herramientas, que es la que hay que tener clara:
git blame |
git log -L |
|
|---|---|---|
| Qué muestra | Estado actual: quién tocó cada línea por última vez | Historia completa: todos los cambios del rango |
| Líneas borradas | No aparecen | Sí aparecen, en los diffs |
| Salida | Una línea por línea de código | Un commit con diff por cada cambio |
| Pregunta que responde | "¿Quién escribió esto?" | "¿Cómo hemos llegado hasta esto?" |
| Punto de partida típico | Estás leyendo el fichero y te chirría algo | Ya sabes qué zona te interesa |
| Coste | Rápido | Más lento (recorre el historial del fichero) |
El flujo natural es encadenarlas: blame para localizar la zona y el commit, log -L para entender la evolución, y git show <sha> para leer el commit completo con su mensaje.
- Ignorar commits de formateo:
--ignore-rev y .git-blame-ignore-revs
--ignore-rev y .git-blame-ignore-revsEste apartado resuelve el problema que más envenena el blame en proyectos reales.
El equipo de gestor-tareas decide adoptar Prettier. Ana lo ejecuta sobre todo el proyecto y confirma:
[main a3f9c2e] Aplicar formato de Prettier a todo el proyecto 3 files changed, 412 insertions(+), 398 deletions(-)
Un solo commit, ningún cambio de comportamiento... y a partir de ahora git blame app.js atribuye a Ana casi todas las líneas del fichero. Toda la información histórica ha quedado sepultada bajo un commit cosmético.
-w ayuda si el cambio fue solo de espaciado, pero Prettier también reordena, parte líneas largas y cambia comillas: -w no basta.
--ignore-rev
Git analiza el fichero como si ese commit no existiera a efectos de atribución. Las líneas que cambió se atribuyen al commit anterior que las tocó de verdad. Si una línea no se puede reasignar limpiamente, Git la marca con un asterisco * para avisar de que la atribución es aproximada.
.git-blame-ignore-revs
Repetir --ignore-rev a mano no escala. La solución es un fichero versionado con la lista de commits a ignorar:
# .git-blame-ignore-revs # # Commits puramente cosméticos que no deben aparecer en git blame. # Requiere: git config blame.ignoreRevsFile .git-blame-ignore-revs # # Aplicar formato de Prettier a todo el proyecto (2026-07-25) a3f9c2e7b1d4c8f2a6e9b3d5c7f1a4e8b2d6c9f3 # Migrar a comillas simples en todo el JavaScript (2026-06-02) 7d1c4a8f3e6b9d2c5a8f1e4b7d3c6a9f2e5b8d1c # Reindentar estilos.css a 2 espacios (2026-05-14) 2e9b5d8c1f4a7e3b6d9c2f5a8e1b4d7c3f6a9e2b
Dos reglas de formato que hay que respetar:
- Hashes completos de 40 caracteres. Los abreviados no valen; Git los rechaza con un error.
- Comentarios con
#. Documenta qué era cada commit: dentro de un año nadie lo recordará.
Para conseguir el hash completo:
Ahora se activa en la configuración (lección 01-05):
Y a partir de ese momento, todos los git blame del repositorio lo aplican solos:
b7e2c4a9 (Carla Vidal 2026-02-14 16:03:55 +0100 43) function normalizarTexto(texto) {
3f1a8d6c (Ana Ferrer 2026-05-30 10:18:22 +0200 45) .replace(/ /g, ' ')La historia real ha vuelto. Y como el fichero está versionado, todo el equipo se beneficia en cuanto ejecute ese git config una vez —el mismo patrón de "fichero versionado + un git config local" que vimos con core.hooksPath en la lección 06-01.
Otras opciones relacionadas:
git blame --ignore-revs-file otra-lista.txt app.js # usar otro fichero puntualmente
git blame --ignore-rev HEAD app.js # ignorar el último commit
git config blame.markIgnoredLines true # marcar con '?' las líneas afectadas
git config blame.markUnblamableLines true # marcar con '*' las no reasignablesCómo hacerlo bien desde el principio: cuando vayas a aplicar un reformateo masivo, hazlo en un commit propio y aislado que no toque nada más, y añádelo al
.git-blame-ignore-revsen el commit siguiente. Un reformateo mezclado con cambios funcionales es imposible de ignorar limpiamente y contamina el historial para siempre. Esa disciplina es parte de lo que veremos en la lección 08-02.Nota práctica: las plataformas de alojamiento (GitHub, GitLab) también leen
.git-blame-ignore-revsy lo aplican en su vista de blame, así que el fichero sirve tanto en local como en la web.
- Resolviendo el misterio de
normalizarTexto
normalizarTextoRecapitulando la investigación completa de Carla, que es el procedimiento estándar ante cualquier "¿esto para qué está?":
# 1. ¿Quién tocó estas líneas por última vez? Ignorando ruido.
git blame -w -C --date=short -L :normalizarTexto:app.js app.jsb7e2c4a9 (Carla Vidal 2026-02-14 43) function normalizarTexto(texto) {
b7e2c4a9 (Carla Vidal 2026-02-14 44) return texto
3f1a8d6c (Ana Ferrer 2026-05-30 45) .replace(/ /g, ' ')
3f1a8d6c (Ana Ferrer 2026-05-30 46) .replace(/[-]/g, '')
b7e2c4a9 (Carla Vidal 2026-02-14 47) .trim();
b7e2c4a9 (Carla Vidal 2026-02-14 48) }commit 3f1a8d6c9e2b4a7f1d5c8e3b6a9f2d4c7e1b5a8f Author: Ana Ferrer <[email protected]> Date: Fri May 30 10:18:22 2026 +0200 Eliminar caracteres invisibles al normalizar el texto de las tareas Al pegar texto desde el gestor de correo, las tareas llegaban con espacios duros (U+00A0) y marcas de anchura cero. [...] app.js | 2 ++
# 3. ¿Cuál ha sido la evolución completa de la función?
git log -L :normalizarTexto:app.js --oneline# 4. ¿Quién más usa esta función? (pickaxe, lección 02-06)
git log -S "normalizarTexto" --oneline
grep -n "normalizarTexto" *.jsb7e2c4a9 Normalizar el texto de las tareas antes de guardarlas 3f1a8d6c Eliminar caracteres invisibles al normalizar el texto de las tareas c9a2e7f4 Mover las funciones auxiliares de index.html a app.js
Cuatro comandos, dos minutos, y una respuesta definitiva: la función no se toca, y ahora Carla sabe además que debería llevar un comentario explicativo. Lo añade:
/**
* Normaliza el texto de una tarea antes de guardarla.
*
* Elimina espacios duros (U+00A0) y marcas de anchura cero (U+200B-U+200D,
* U+FEFF), que llegan al pegar texto desde el gestor de correo y provocan
* que dos tareas visualmente idénticas se traten como distintas.
* Ver commit 3f1a8d6.
*/
function normalizarTexto(texto) {
return texto
.replace(/ /g, ' ')
.replace(/[-]/g, '')
.trim();
}Fíjate en el detalle: el comentario referencia el commit. Es la forma de conectar el código con la explicación completa sin repetirla, y le ahorra a la siguiente persona toda esta investigación.
- Blame en la práctica: atribución, no culpa
El nombre del comando es un accidente histórico desafortunado. En inglés, to blame es "culpar", y eso ha creado una asociación desagradable: la idea de que git blame sirve para señalar al responsable de una chapuza.
No es para eso. Git tiene incluso un alias con mejor nombre, heredado de otros sistemas:
Y "anotar" describe mucho mejor lo que hace: enriquecer cada línea con su procedencia.
Los usos legítimos, todos ellos constructivos:
- Entender el porqué. El caso de esta lección. El mensaje del commit contiene el contexto que el código no puede expresar.
- Encontrar a quién preguntar. No para reprochar, sino porque quien escribió algo hace tres meses tiene contexto que tú no tienes.
- Datar un cambio. "¿Esta línea es anterior o posterior a la migración?"
- Evaluar el riesgo de tocar algo. Código de hace tres años que nadie ha tocado y del que nadie se acuerda: proceder con cuidado.
- Documentar hacia atrás. Como acaba de hacer Carla.
Y esto lleva a la conclusión de fondo de la lección, que conviene enunciar sin rodeos:
git blamevale exactamente lo que valen tus mensajes de commit.
Si el commit que aparece dice "Eliminar caracteres invisibles al normalizar el texto de las tareas" con tres líneas explicando el porqué, blame te ha resuelto el problema. Si dice "fix", "cambios" o "asdf", blame te ha dicho quién y cuándo, pero no por qué, que era lo único que necesitabas.
Esa es la conexión directa con la lección 08-01: Escribiendo Buenos Mensajes de Confirmación. Ahí veremos cómo se escribe un mensaje que explique la intención y no la mecánica. Y el argumento definitivo para hacerlo bien es este: no escribes el mensaje para hoy, lo escribes para el git blame que alguien hará dentro de dos años —y ese alguien probablemente serás tú.
Del mismo modo, los commits atómicos (lección 08-02) hacen que el blame señale a un cambio comprensible en lugar de a un commit de 900 líneas titulado "trabajo del viernes".
Errores Comunes y Consejos
Error 1: creer que blame dice quién escribió la línea. Dice quién la tocó por última vez. Sin -w, -M, -C e --ignore-rev, la respuesta suele ser "el último que pasó el formateador".
Error 2: blamear el fichero entero. Cientos de líneas de salida para investigar cinco. Usa siempre -L, y sobre todo -L :funcion:fichero.
Error 3: parar en el primer commit que aparece. Si es un movimiento o un reformateo, sigue tirando del hilo con git blame <sha>^ -- <fichero> o pasa directamente a git log -L.
Error 4: buscar con blame una línea que ya no existe. blame solo ve el estado actual. Para código borrado, git log -L, git log -S "texto" (pickaxe) o git log -p -- <fichero>.
Error 5: hashes abreviados en .git-blame-ignore-revs. Git exige los 40 caracteres y falla con un error poco claro si no los pones. Obténlos con git rev-parse.
Error 6: mezclar reformateo y cambios funcionales en un commit. Ese commit ya no se puede ignorar limpiamente y contamina el blame del fichero para siempre. El reformateo, siempre en su propio commit.
Error 7: usar blame para repartir responsabilidades. Además de ser tóxico, es técnicamente poco fiable: la atribución depende de las opciones que uses.
Consejo 1: crea un alias. Lo verás en la lección 06-04, pero adelántalo ya:
Consejo 2: encadena las tres herramientas. blame para localizar, log -L para la evolución, show para el commit completo. Es el procedimiento estándar y no falla.
Consejo 3: cuando resuelvas un misterio, documéntalo en el código. Un comentario con la referencia al commit ahorra la investigación a los siguientes.
Consejo 4: blame en el editor. Prácticamente todos los editores modernos tienen integración (GitLens en VS Code, :Git blame en Vim con fugitive, la anotación integrada de IntelliJ). Enseñan el commit de la línea del cursor sin salir del fichero. Configúralos con -w si te dejan.
Consejo 5: combínalo con bisect. Cuando bisect (lección 06-02) te da el commit culpable, blame sobre las líneas que cambió te dice qué había antes y por qué estaba así.
Ejercicios
Ejercicio 1: blame básico y acotado
Crea un repositorio con un fichero app.js que evolucione en cinco commits de tres autores distintos (puedes cambiar de autor con git commit --author="Nombre <correo>"). Después:
- Ejecuta
git blame app.jsy comprueba que cada línea se atribuye al commit correcto. - Acota con
-La un rango de tres líneas. - Usa
-L :nombreFuncion:app.jspara acotar a una función. - Prueba
-s,-ey--date=shorty compara las salidas.
Ejercicio 2: el efecto de un reformateo y cómo ignorarlo
Sobre el repositorio anterior:
- Haz un commit que reindente todo el fichero (de 2 a 4 espacios) sin cambiar nada más.
- Ejecuta
git blamey comprueba que ahora todas las líneas son de ese commit. - Comprueba que
-wrecupera la atribución original. - Haz otro commit que cambie las comillas dobles por simples en todo el fichero, y comprueba que
-wya no basta. - Crea un
.git-blame-ignore-revscon ese commit, configurablame.ignoreRevsFiley comprueba que la atribución vuelve.
Ejercicio 3: la historia completa de una función
Sobre el mismo repositorio:
- Usa
git log -L :nombreFuncion:app.jsy describe la evolución de la función. - Añade una línea a la función, confírmala, y bórrala en otro commit.
- Comprueba que
git blameno muestra rastro de esa línea, perogit log -Lsí. - Localiza esa misma línea borrada usando el pickaxe
git log -Sde la lección 02-06.
Soluciones
Solución 1:
mkdir /tmp/practica-blame && cd /tmp/practica-blame
git init -b main
cat > app.js <<'FIN'
function normalizarTexto(texto) {
return texto.trim();
}
FIN
git add . && git commit -q -m "Añadir la normalizacion basica de texto" \
--author="Carla Vidal <[email protected]>"
cat > app.js <<'FIN'
function normalizarTexto(texto) {
return texto.trim();
}
function guardarTareas(tareas) {
localStorage.setItem("tareas", JSON.stringify(tareas));
}
FIN
git commit -qam "Añadir el guardado de tareas en localStorage" \
--author="Ana Ferrer <[email protected]>"
cat > app.js <<'FIN'
function normalizarTexto(texto) {
return texto
.replace(/ /g, " ")
.trim();
}
function guardarTareas(tareas) {
localStorage.setItem("tareas", JSON.stringify(tareas));
}
FIN
git commit -qam "Convertir los espacios duros al normalizar" \
--author="Bruno Salas <[email protected]>"5f2a8c1 (Carla Vidal 2026-08-01 1) function normalizarTexto(texto) {
9d3e7b4 (Bruno Salas 2026-08-01 2) return texto
9d3e7b4 (Bruno Salas 2026-08-01 3) .replace(/ /g, " ")
9d3e7b4 (Bruno Salas 2026-08-01 4) .trim();
5f2a8c1 (Carla Vidal 2026-08-01 5) }
5f2a8c1 (Carla Vidal 2026-08-01 6)
2c7f4a9 (Ana Ferrer 2026-08-01 7) function guardarTareas(tareas) {Fíjate en un detalle instructivo: la línea 2 se atribuye a Bruno aunque el return texto conceptualmente lo escribió Carla. Es porque Bruno modificó esa línea al partir la expresión en varias. blame opera sobre líneas, no sobre intenciones.
git blame -L 1,3 app.js
git blame -L :normalizarTexto:app.js
git blame -s app.js # solo hash y línea
git blame -e app.js # con el emailSolución 2:
# 1. Reformateo de indentación
sed -i 's/^ / /' app.js
sed -i 's/^ \./ ./' app.js
git commit -qam "Reindentar app.js a 4 espacios" \
--author="Ana Ferrer <[email protected]>"
# 2. Todo es de Ana ahora
git blame --date=short app.js4a8c2e7 (Ana Ferrer 2026-08-01 2) return texto 4a8c2e7 (Ana Ferrer 2026-08-01 3) .replace(/ /g, " ") 4a8c2e7 (Ana Ferrer 2026-08-01 4) .trim();
9d3e7b4 (Bruno Salas 2026-08-01 2) return texto 9d3e7b4 (Bruno Salas 2026-08-01 3) .replace(/ /g, " ")
# 4. Cambio de comillas: -w ya no basta
sed -i 's/"/'"'"'/g' app.js
git commit -qam "Migrar a comillas simples en todo el JavaScript" \
--author="Ana Ferrer <[email protected]>"
git blame -w --date=short app.js1b6d9f3 (Ana Ferrer 2026-08-01 3) .replace(/ /g, ' ')
1b6d9f3 (Ana Ferrer 2026-08-01 8) localStorage.setItem('tareas', ...El cambio no es de espaciado, así que -w no lo atraviesa.
# 5. El fichero de commits ignorados
git rev-parse HEAD > /tmp/hash.txt
{
echo "# Commits cosméticos ignorados en git blame"
echo "#"
echo "# Migrar a comillas simples en todo el JavaScript"
cat /tmp/hash.txt
} > .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -qm "Añadir la lista de commits ignorados en blame"
git config blame.ignoreRevsFile .git-blame-ignore-revs
git blame -w --date=short app.js9d3e7b4 (Bruno Salas 2026-08-01 3) .replace(/ /g, ' ')
2c7f4a9 (Ana Ferrer 2026-08-01 8) localStorage.setItem('tareas', ...La atribución real ha vuelto, y ahora es automática para todo el repositorio.
Solución 3:
1b6d9f3 Migrar a comillas simples en todo el JavaScript 4a8c2e7 Reindentar app.js a 4 espacios 9d3e7b4 Convertir los espacios duros al normalizar 5f2a8c1 Añadir la normalizacion basica de texto
Cuatro commits: dos cosméticos y dos de contenido real. La película completa, incluido lo que blame con --ignore-revs oculta deliberadamente.
# 2. Añadir y borrar una línea
sed -i "s| .trim();| .replace(/\\\\s+/g, ' ')\n .trim();|" app.js
git commit -qam "Colapsar los espacios multiples al normalizar"
sed -i "/replace(\/\\\\s+\/g/d" app.js
git commit -qam "Revertir el colapso de espacios: rompia las tabulaciones"# 3. blame no la ve; log -L sí
git blame app.js | grep "s+" || echo "(blame no muestra la línea borrada)"
git log -L :normalizarTexto:app.js --oneline | head -38e2c5f1 Revertir el colapso de espacios: rompia las tabulaciones 3d9a7b4 Colapsar los espacios multiples al normalizar 1b6d9f3 Migrar a comillas simples en todo el JavaScript
8e2c5f1 Revertir el colapso de espacios: rompia las tabulaciones 3d9a7b4 Colapsar los espacios multiples al normalizar
-S cuenta apariciones de la cadena y muestra los commits donde ese número cambia: el que la introdujo y el que la quitó. Es la herramienta para lo que ya no está.
Conclusión
git blame convierte el historial en una anotación línea a línea del código que tienes delante. Lo esencial:
- Responde a "¿qué commit tocó esta línea por última vez?", sobre el estado actual del fichero. No dice quién la escribió originalmente, y no ve las líneas borradas.
- La salida tiene cinco columnas —hash, autor, fecha, número de línea y contenido—;
^marca el commit frontera y00000000los cambios sin confirmar. -Lacota, y-L :funcion:ficheroes la forma más cómoda de hacerlo sin contar líneas.-w,-My-Cson lo que separa una atribución útil de una inútil: ignoran espaciado y siguen el código movido o copiado, dentro y entre ficheros, con niveles crecientes (-C,-C -C,-C -C -C).git blame <sha>^ -- <fichero>salta por encima de un commit para ver qué había antes: la técnica manual para tirar del hilo.git log -Lda la película completa dondeblameda la foto: todos los commits que afectaron a un rango, con sus diffs, incluidas las líneas borradas.--ignore-revy.git-blame-ignore-revs(conblame.ignoreRevsFile) neutralizan los reformateos masivos y devuelven la historia real. Para que funcione, el reformateo tiene que estar en su propio commit aislado.- El procedimiento estándar ante cualquier "¿esto para qué está?":
blame→log -L→show, ygrep/log -Spara ver quién más depende de ello. - Y lo más importante: es atribución, no culpa, y vale lo que valgan tus mensajes de commit (lección 08-01).
Carla y el equipo tienen ya un arsenal de consulta considerable: log con sus filtros y su pickaxe, bisect, blame, log -L, show. El problema es que las invocaciones útiles son cada vez más largas —git blame -w -C --date=short -L :funcion:app.js app.js no se escribe a mano dos veces— y que hay vistas del historial que todavía no sabemos pedir: el grafo completo de ramas, solo las fusiones, quién ha contribuido cuánto, o un formato de una línea con colores que se lea de un vistazo.
Toca subir de nivel con git log y, sobre todo, dejar de escribir todo esto a mano. Es la lección 06-04: Git Log y Alias.
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
