Un archivo .docx es un archivo ZIP lleno de XML. Si se le muestran dos borradores a Git, lo único que sabrá decir es que los archivos binarios difieren. Un manuscrito LaTeX, en cambio, es texto plano, y git diff señala exactamente la frase que movió un coautor: esa sola propiedad explica por qué el control de versiones y la escritura colaborativa encajan tan bien con LaTeX. El encaje, sin embargo, no llega solo. Git compara líneas, no frases, y un párrafo escrito en una única línea larga es una línea indivisible. Peor aún: LaTeX compila sin rechistar un archivo que todavía contiene <<<<<<< HEAD e imprime el conflicto en el PDF. Esta página trata de qué conviene versionar, dónde cortar las líneas para que un diff siga siendo legible, cómo sobrevivir a un conflicto de fusión y de cómo latexdiff, todonotes y changes convierten un árbol de fuentes en algo que un coautor puede revisar de verdad.
Por qué git diff funciona con un .tex y no con un .docx
Porque git diff no es más que una comparación línea a línea de dos archivos. Un archivo .tex contiene los caracteres que alguien tecleó, en orden, de modo que la comparación devuelve un informe legible: esta línea pasó a ser aquella. El interior de un .docx es XML comprimido, y añadir una sola coma puede cambiar los bytes comprimidos por completo, así que Git no sabe decir nada más allá de «distintos». De ahí que colaborar sobre un documento de procesador de textos acabe siendo un ir y venir de adjuntos que alguien fusiona a mano al final. Con LaTeX ese vaivén desaparece y las ramas y fusiones asumen el trabajo. Dicho al revés: conservar en el repositorio solo los archivos que merece la pena leer como texto y dejar fuera desde el principio los generados, que no significan nada para un diff. El diseño de un repositorio de escritura compartida se reduce casi por entero a eso.
Git guarda además otra cortesía. La cabecera de cada bloque de diferencias —la línea que empieza por @@ -3,2 +3,2 @@— suele arrastrar cualquier línea cercana que Git haya encontrado. Basta añadir la línea *.tex diff=tex a .gitattributes para que Git pase a su regla integrada para TeX y coloque en esa cabecera el nombre de la \section que envuelve el bloque. En un manuscrito de varios cientos de páginas, poder repasar un diff y ver a qué sección pertenece cada cambio no es poca cosa. Una línea de configuración y ningún efecto secundario.
# .gitattributes — teach git the structure of a .tex file
*.tex diff=tex
# hunk headers now name the enclosing sectioning command:
# @@ -3,2 +3,2 @@ \section{First}
# without it, git prints an arbitrary nearby line instead.Qué versionar y qué va en .gitignore
Se versiona solo lo que ha escrito una persona: los archivos .tex, la base .bib, la forma fuente de las figuras, latexmkrc o el Makefile, y cualquier clase u hoja de estilo de la que dependa el documento. Con eso, cualquiera reconstruye el mismo PDF. Todo lo que la compilación regenera va sin excepción a .gitignore. Una sola pasada de latexmk sobre un documento mínimo que use biblatex y biber ya produce .aux, .bbl, .bcf, .blg, .fdb_latexmk, .fls, .log, .run.xml y .toc. Un índice añade .idx, .ilg e .ind; hyperref añade .out; SyncTeX activado añade .synctex.gz. Seguir esos archivos hace que cada commit arrastre cientos de líneas de ruido aunque no se haya movido un solo carácter del texto.
# .gitignore — everything below is regenerated by a build
*.aux
*.log
*.out
*.toc
*.lof
*.lot
*.fls
*.fdb_latexmk
*.synctex.gz
*.bbl
*.blg
*.bcf
*.run.xml
*.idx
*.ilg
*.ind
# generated PDFs: ignore the working build, keep tagged releases by hand
main.pdf
*-diff*.texEl PDF generado es el único candidato a excepción. Seguir un main.pdf que cambia en cada compilación da diffs ilegibles y un repositorio que solo engorda. Resulta más llevadero ignorarlo en el día a día y adjuntar a una etiqueta o una Release únicamente las versiones que quizá haya que reproducir después carácter por carácter: un envío, una publicación. El .bbl sigue la misma lógica: es un producto de compilación, pero si una editorial pide un paquete de fuentes con el .bbl incluido, se genera en el momento del envío y se adjunta aparte. Eso no justifica mantenerlo de forma permanente en el repositorio. Otro archivo que conviene ignorar es el *-diff*.tex que escribe latexdiff. Es un artefacto, no un manuscrito, y dejar que se cuele en la rama principal significa editar, en la vuelta siguiente, un borrador plagado de comandos \DIF.
Una frase por línea: la unidad del diff es el salto de línea
Conviene escribir el manuscrito con una frase por línea. Un salto de línea aislado no es más que un espacio para LaTeX, de modo que el resultado compuesto no cambia ni un carácter; lo que cambia es la legibilidad del diff. Con un párrafo en una sola línea, corregir una coma basta para que Git informe del párrafo entero como borrado y vuelto a añadir. Con un salto tras cada frase, informa de la única frase que se ha movido. Para un coautor que revisa los cambios, esa sola diferencia lo cambia todo. Como los saltos de línea no afectan a la composición tipográfica, un manuscrito existente puede convertirse en cualquier momento sin que varíe el PDF; ahora bien, la conversión mueve todas las líneas y debe ir en un commit propio, nunca mezclada con un cambio de contenido.
# whole paragraph on one line: git rewrites the entire paragraph
-The fox jumps over the dog. The morning was fine. Nobody minded.
+The fox jumps over the dog. The morning was cold. Nobody minded.
# one sentence per line: git points at the sentence that moved
The fox jumps over the dog.
-The morning was fine.
+The morning was cold.
Nobody minded.Para el japonés, el chino y el coreano el consejo deja de ser una comodidad. En inglés hay una salida: git diff --word-diff muestra solo las palabras que cambiaron, en la forma [-viejo-]{+nuevo+}, incluso en una línea larga. Pero lo que --word-diff toma como frontera de palabra es el espacio en blanco. Aplicado al japonés, que se escribe sin espacios, cambiar un solo carácter en 吾輩は猫である。名前はまだ無い。 sigue haciendo que Git borre la línea entera y la vuelva a añadir. Recurrir a --word-diff-regex=. para comparar carácter a carácter tampoco salva nada: el patrón se aplica byte a byte, los caracteres UTF-8 multibyte quedan troceados y la salida vuelve convertida en galimatías, del tipo 吾輩は?[-??-]{+??+}である。. Dicho de otro modo, un manuscrito CJK no tiene escapatoria. Una frase por línea es un buen hábito en inglés; en japonés, chino y coreano es prácticamente la única opción.
Resolver un conflicto de fusión en un .tex: LaTeX no le avisará
Ejecutar pdflatex sobre un archivo cuyas marcas de conflicto se olvidó eliminar no produce ni un solo error. <<<<<<<, ======= y >>>>>>> son secuencias de caracteres perfectamente legales en modo texto; para LaTeX son simple puntuación. La compilación termina, pues, con código de salida 0 y genera un PDF que contiene ambas versiones y las marcas entre ellas. En la codificación OT1 por omisión, < y > se corresponden con signos invertidos, de modo que en la página aparecen dos líneas insólitas: ¡¡¡¡¡¡¡ HEAD y ¿¿¿¿¿¿¿ feature. Si aparecen, lo primero que hay que sospechar es un conflicto sin resolver.
% what git leaves behind - and what LaTeX happily typesets
\begin{document}
<<<<<<< HEAD
Main branch sentence.
=======
Feature branch sentence.
>>>>>>> feature
\end{document}
% check before every build:
% git grep -n "^<<<<<<< " -- "*.tex"La resolución en sí es trabajo corriente de Git: abrir los archivos que lista git status, decidir qué lado de <<<<<<< … >>>>>>> sobrevive —o reescribir ambos en uno—, borrar las marcas y hacer git add. Dos cosas son propias de LaTeX. Primero, si un conflicto cae en medio de un par \begin{itemize} … \end{itemize}, quedarse con un solo lado puede romper el emparejamiento, y entonces la compilación falla de verdad; al quitar las marcas conviene comprobar con la vista que cada entorno sigue abriéndose y cerrándose. Segundo, los conflictos se pueden prevenir. Con una frase por línea, Git fusiona automáticamente frase a frase, y mientras dos personas editen frases distintas no surge conflicto alguno. Si dos personas van a reescribir la misma sección a la vez, dividir el archivo con \include y repartir el trabajo por archivos es el camino más seguro.
latexdiff: convertir dos versiones en un PDF con los cambios marcados
Lo que latexdiff escribe no es un PDF sino un nuevo archivo .tex con el marcado de cambios incrustado. Al compilarlo uno mismo se obtiene un PDF parecido a la vista de control de cambios de un procesador de textos. En el estilo por omisión, las palabras añadidas aparecen con subrayado ondulado azul (\uwave, de ulem) y las suprimidas con tachado rojo (\sout); las líneas \RequirePackage necesarias se añaden solas al preámbulo del archivo generado. Todos los comandos que inserta empiezan por \DIF —\DIFadd, \DIFdel, \DIFaddbegin y, dentro de flotantes, \DIFaddFL—, así que después resultan fáciles de localizar. Su autor es F. J. Tilmann; la versión incluida en TeX Live 2024 es la 1.3.3.
latexdiff --flatten old.tex new.tex > diff.tex
pdflatex diff.tex # additions blue and underlined, deletions red and struck out
# what latexdiff actually writes into the body:
# The quick \DIFdelbegin \DIFdel{brown fox jumps }\DIFdelend
# \DIFaddbegin \DIFadd{red fox leaps }\DIFaddend over the lazy dog.Lo que merece atención aquí es que latexdiff compara palabra por palabra. En el ejemplo anterior enfrenta «brown fox jumps» con «red fox leaps» en lugar de informar de una línea entera reemplazada. Eso contrasta con la granularidad de línea de git diff, y ambos se reparten el trabajo más que competir: Git para el historial y la fusión automática, latexdiff para enseñar a un coautor qué cambió realmente. Tres trampas prácticas. Un documento dividido con \input o \include solo se compara en su nivel superior si no se añade --flatten. La finura del marcado dentro de las fórmulas en modo display se ajusta con --math-markup=level, y bajarlo es el remedio cuando las fórmulas salen destrozadas. Y el diff.tex generado no es el manuscrito: conserva su propio nombre y toda corrección vuelve al .tex original.
Comparar con una revisión de Git mediante latexdiff-vc
No hace falta exportar a mano la versión antigua. Basta pasar una revisión al latexdiff-vc incluido —por ejemplo --git -r HEAD~3— para que extraiga temporalmente esa versión, la compare y escriba el archivo de diferencias con el nombre main-diffHEAD~3.tex. Adivina el sistema de control de versiones si no se le indica, pero --git, --svn, --hg, --cvs o --rcs resulta más seguro. Añadir --pdf hace pasar dos veces pdflatex por el archivo de diferencias y produce también el PDF. Enseñar a un revisor qué cambió entre el envío y la versión corregida se reduce así a una sola orden. Conviene no olvidar poner el *-diff*.tex resultante en .gitignore.
latexdiff-vc --git -r HEAD~3 main.tex # writes main-diffHEAD~3.tex
latexdiff-vc --git --pdf -r v1.0 main.tex # ...and builds the PDF as well
# output of the run:
# Running: latexdiff "main-oldtmp-15378.tex" "main.tex" > "main-diffHEAD~3.tex"
# Generated difference file main-diffHEAD~3.textodonotes: notas al margen que desaparecen en la versión final
\todo{...} clava una nota adhesiva de color en el margen y \listoftodos las reúne todas en una sola lista. Es la herramienta más pequeña posible para dejar un «corregir luego» dentro de un manuscrito, y su única ventaja sobre un comentario % TODO es que resulta visible en la impresión. Esa visibilidad es justamente lo que impide olvidarlo. \todo[inline]{...} coloca la nota en pleno hilo del texto, \missingfigure{...} reserva el sitio de una figura aún no dibujada y \todototoc lleva la propia lista de TODO al índice. Para la versión final basta con pasar a \usepackage[disable]{todonotes}: todo desaparece de la página sin borrar ni una sola llamada a \todo. Con obeyFinal, en cambio, las notas siguen automáticamente la opción final de la clase de documento.
\usepackage{todonotes} % [disable] hides every note in the final build
...
\todo{Citation needed here}
\todo[inline]{Rewrite this paragraph before submission}
\missingfigure{Circuit diagram goes here}
\listoftodoschanges: marcado por autor y el error Undefined changes author
Cuando varias personas anotan un mismo manuscrito, el paquete changes es la herramienta indicada. \added{...}, \deleted{...}, \replaced{nuevo}{viejo}, \highlight{...} y \comment{...} explicitan la intención de cada edición, \listofchanges construye el índice de todas ellas, la opción draft muestra el marcado y pasar a final borra cualquier rastro. Hay, eso sí, un punto en el que todo el mundo tropieza la primera vez. Escribir \added[id=AB]{...} sin haber definido al autor AB detiene la compilación en ! Package changes Error: Undefined changes author: AB. Después llega una cascada de errores Undefined color procedentes de xcolor, pero todos tienen la misma causa. Una línea en el preámbulo —\definechangesauthor[name={...}, color=blue]{AB}— lo resuelve. Asignar un color distinto a cada autor es justamente la razón de ser del paquete: se añade una línea así cada vez que se suma un coautor.
\usepackage[draft]{changes} % swap draft for final to hide all markup
\definechangesauthor[name={Ada Byron}, color=blue]{AB}
\definechangesauthor[name={Bob Lane}, color=orange]{BL}
...
\added[id=AB]{A sentence the reviewer asked for.}
\replaced[id=AB]{new wording}{old wording}
\deleted[id=BL]{This clause has to go.}
\listofchangesCoautores en Overleaf y coautores que solo abren Word
No hace falta obligar a todo el mundo a usar Git. Overleaf permite editar en colaboración solo desde el navegador y trae su propio historial y su propia vista de seguimiento de cambios, a menudo la vía más rápida para un coautor poco familiarizado con LaTeX. Además, un proyecto de Overleaf puede tratarse como un repositorio Git: se puede clonar. Con ese puente tendido surge un reparto del trabajo: el coautor escribe en el navegador, uno hace git pull en local y produce el PDF de diferencias con latexdiff-vc. La única precaución es que todo lo dicho arriba sobre .gitignore sigue vigente; conviene revisar pronto la configuración de sincronización para que los PDF y registros generados en Overleaf no viajen junto con las fuentes.
El caso más peliagudo es el de un coautor que solo trabaja en Word. Lo sensato entonces es declarar autorizada la versión LaTeX. Se le envía un .docx producido a partir del .tex con pandoc, y el .docx anotado vuelve a pasar por pandoc al regresar. Su opción --track-changes admite accept (el valor por omisión), reject o all, y decide qué ocurre con el control de cambios de Word: accept aplica todas las inserciones y supresiones, reject las ignora y all conserva inserciones, supresiones y comentarios junto con el autor y la fecha de cada cambio, lo que permite quedarse solo con las correcciones de un revisor concreto. La opción afecta únicamente al lector de .docx. Cada ida y vuelta cuesta algo de formato, pero mientras la copia autorizada viva en LaTeX, lo que se pierde es el formato y no el manuscrito.
| Herramienta | Qué muestra | Cuándo recurrir a ella |
|---|---|---|
git diff | diferencias línea a línea en la fuente | hacen falta historial, ramas y fusión automática |
latexdiff | diferencias palabra a palabra compuestas en PDF | un coautor o revisor debe ver los cambios sobre la página |
todonotes | notas al margen y una lista reunida de TODO | algo está sin terminar y debe seguir a la vista |
changes | ediciones y comentarios coloreados por autor | varias personas anotan un mismo manuscrito |
pandoc | nada: convierte entre .tex y .docx | un coautor solo trabaja en Word |
Comprobaciones antes de enviar el PDF final
- Pasar
todonotesadisableychangesafinal, abrir después el PDF y comprobar que no ha sobrevivido ningún TODO ni marca. - Rastrear marcas de conflicto olvidadas con
git grep -n "^<<<<<<< " -- "*.tex": LaTeX no avisará de ellas. - Borrar los productos de compilación con
latexmk -Cy reconstruir desde cero, para demostrar que el documento no depende de un archivo intermedio caduco. - Mirar
git statusuna última vez por si un*-diff*.texdelatexdiffse ha colado en el directorio del manuscrito. - Al enviar a los coautores, separar el paquete de fuentes, el PDF de diferencias y el PDF final, y decir con claridad cuál debe revisarse.