DEV Community

Cover image for Metí el historial completo de git de Linux en una base de datos — y lo que encontré me pareció arqueología
Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

Metí el historial completo de git de Linux en una base de datos — y lo que encontré me pareció arqueología

Cometí un error que duré años sin darme cuenta que era un error: traté el historial de git como algo que se scrollea para arriba cuando algo se rompe, y después se cierra.

No lo cuento para flagelarme. Lo cuento porque la mayoría de los devs hace exactamente lo mismo. Y cuando finalmente lo tratás como datos — como filas en una tabla que podés consultar, filtrar, agregar — te encontrás mirando tu propio trabajo como si fuera de otra persona. Y eso es perturbador de la mejor manera posible.

Todo esto me lo disparó un post sobre pgit, un proyecto que mete el historial entero del kernel de Linux en PostgreSQL. El tipo consultó 1.2 millones de commits con SQL. Encontró patrones de authorship, velocidad de merges por subsistema, quién commitea a qué hora. Arqueología de software en tiempo real.

Yo leí eso un sábado a la tarde y tres horas después estaba haciendo lo mismo con mis propios repos.

Linux kernel git history pgit análisis: qué es y por qué importa

pgit es conceptualmente simple: toma el output de git log —con todos sus campos: autor, timestamp, archivos modificados, tamaño del diff, mensaje— y lo inserta en tablas relacionales. Después podés escribir SQL encima.

Lo que suena obvio cuando lo explicás así es revolucionario en la práctica. Porque git log --oneline te da una lista. PostgreSQL te da un modelo.

La diferencia es la diferencia entre leer un libro y poder hacer grep sobre todos los libros que leíste.

El kernel de Linux tiene datos desde 1991. Linus Torvalds tiene commits desde antes de que yo supiera que existían las computadoras. Hay commits de gente que ya murió. Hay decisiones técnicas que se pueden rastrear hasta conversaciones específicas de una semana específica de un año específico. Es arqueología digital con estratigrafía perfecta.

Pero el kernel es de otra persona. Lo mío me interesó más.

Cómo armé mi propia versión con repos personales

No usé pgit directamente — lo adapté. La idea es la misma: git log con formato personalizado, pipe a un script que parsea e inserta en PostgreSQL.

Este es el esquema que armé:

-- Tabla principal de commits
CREATE TABLE commits (
  hash        CHAR(40) PRIMARY KEY,
  repo        TEXT NOT NULL,           -- de qué repo viene
  autor       TEXT NOT NULL,
  email       TEXT NOT NULL,
  fecha       TIMESTAMPTZ NOT NULL,
  mensaje     TEXT NOT NULL,
  archivos    INTEGER DEFAULT 0,       -- cuántos archivos tocó
  inserciones INTEGER DEFAULT 0,
  eliminaciones INTEGER DEFAULT 0
);

-- Índices para las queries que voy a hacer seguido
CREATE INDEX idx_commits_fecha  ON commits(fecha);
CREATE INDEX idx_commits_repo   ON commits(repo);
CREATE INDEX idx_commits_autor  ON commits(autor);
Enter fullscreen mode Exit fullscreen mode

Y el script de ingesta:

#!/bin/bash
# ingestar_repo.sh — mete el historial de un repo en postgres

REPO_PATH=$1
REPO_NAME=$2
DB_URL=${DATABASE_URL:-"postgresql://localhost/gitarchivo"}

if [ -z "$REPO_PATH" ] || [ -z "$REPO_NAME" ]; then
  echo "Uso: ./ingestar_repo.sh /path/al/repo nombre-repo"
  exit 1
fi

cd "$REPO_PATH" || exit 1

# Formato: hash|autor|email|fecha-iso|archivos|inserciones|eliminaciones|mensaje
git log \
  --format="%H|%an|%ae|%aI|%x00" \
  --numstat \
  | awk '
    # Parsear el formato mixto de git log con --numstat
    /^[0-9a-f]{40}\|/ {
      if (hash != "") print hash"|"autor"|"email"|"fecha"|"arch"|"ins"|"del"|"msg
      split($0, a, "|")
      hash=a[1]; autor=a[2]; email=a[3]; fecha=a[4]
      arch=0; ins=0; del=0; msg=""
      next
    }
    /^[0-9]+\t[0-9]+\t/ {
      ins += $1; del += $2; arch++
      next
    }
  ' \
  | psql "$DB_URL" -c "
    COPY commits(hash,autor,email,fecha,repo,archivos,inserciones,eliminaciones,mensaje)
    FROM STDIN
    WITH (FORMAT CSV, DELIMITER '|')
  " --set repo="$REPO_NAME"

echo "Listo: $REPO_NAME ingestado."
Enter fullscreen mode Exit fullscreen mode

No es perfecto — los mensajes con pipes adentro lo rompen, lo sé. Pero para análisis exploratorio funciona.

Ingesté nueve repos míos. Proyectos freelance, experimentos, el monorepo del trabajo actual. Total: 4.847 commits entre 2020 y 2024.

Lo que encontré: las partes incómodas

Empecé con queries inocentes:

-- ¿A qué hora commiteo más?
SELECT
  EXTRACT(HOUR FROM fecha) AS hora,
  COUNT(*) AS cantidad,
  ROUND(AVG(inserciones + eliminaciones)) AS lineas_promedio
FROM commits
WHERE autor LIKE '%Torchia%'
GROUP BY hora
ORDER BY cantidad DESC;
Enter fullscreen mode Exit fullscreen mode

Resultado: mis picos son a las 11am y a las 10pm. Hasta ahí bien. Pero el promedio de líneas por commit a las 10pm es el doble que a las 11am. Commiteo más de noche y con cambios más grandes. Lo cual suena productivo hasta que mirás la calidad de esos mensajes:

-- Mensajes de commit por hora — los peores primero
SELECT
  EXTRACT(HOUR FROM fecha) AS hora,
  mensaje,
  inserciones + eliminaciones AS lineas_cambiadas
FROM commits
WHERE
  autor LIKE '%Torchia%'
  AND EXTRACT(HOUR FROM fecha) BETWEEN 21 AND 23
ORDER BY fecha DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

Los resultados me dieron vergüenza suficiente para no pegarlos acá. "arreglo", "wip", "no sé qué pasó pero funciona", "fix de antes". Commiteo más de noche, con más cambios, y con menos cuidado en comunicar qué hice. Correlación perfecta con mi peor versión como programador.

Después busqué patrones en los archivos:

-- ¿Qué extensiones toco más?
-- (Requiere una tabla de archivos separada, esto es aproximado)
SELECT
  CASE
    WHEN mensaje ILIKE '%.tsx%' OR mensaje ILIKE '%component%' THEN 'frontend'
    WHEN mensaje ILIKE '%.sql%' OR mensaje ILIKE '%migration%' THEN 'base de datos'
    WHEN mensaje ILIKE '%docker%' OR mensaje ILIKE '%deploy%' THEN 'infra'
    WHEN mensaje ILIKE '%test%' OR mensaje ILIKE '%.spec%' THEN 'tests'
    ELSE 'otro'
  END AS categoria,
  COUNT(*) AS commits,
  SUM(inserciones) AS lineas_agregadas
FROM commits
WHERE autor LIKE '%Torchia%'
GROUP BY categoria
ORDER BY commits DESC;
Enter fullscreen mode Exit fullscreen mode

Resultado: la categoría "tests" tiene un 3% del total. El equipo habla de testing en cada retrospectiva. Yo commiteo tests el 3% del tiempo. Los datos no mienten de la manera que uno quisiera.

La más reveladora fue esta:

-- Velocidad de commits por proyecto — ¿dónde perdí el ritmo?
SELECT
  repo,
  DATE_TRUNC('month', fecha) AS mes,
  COUNT(*) AS commits_ese_mes,
  MAX(fecha) - MIN(fecha) AS span_del_mes
FROM commits
GROUP BY repo, mes
ORDER BY repo, mes;
Enter fullscreen mode Exit fullscreen mode

Hay un proyecto donde commitié 180 veces en noviembre de 2022 y cero en diciembre. Literalmente cero. Lo que los datos no me dicen es por qué, pero yo lo sé: ese proyecto me quemó. Ver el corte tan nítido en una query SQL es diferente a recordarlo vagamente. Es como ver una cicatriz en una radiografía.

Los gotchas que no anticipé

El encoding va a romperte la ingesta. Repos viejos tienen mensajes en latin-1, UTF-8 mal declarado, caracteres raros en nombres de autores. Agregá iconv -f UTF-8 -t UTF-8 -c al pipeline para sanear antes de insertar.

Los merges inflan los números. Un merge commit puede tener miles de líneas cambiadas que en realidad son de otro branch. Filtrá con --no-merges si querés analizar trabajo real, o mantenelos separados con una columna es_merge BOOLEAN.

Los timestamps mienten si el equipo es remoto. Los commits tienen el timezone del committer. Alguien en UTC-3 que commitea a las 11pm aparece como 2am UTC. Para análisis de horarios, normalizá todo a un timezone antes de agregar.

La identidad de autor es un quilombo. Yo tengo commits como "Juan Torchia", "juanchi", "jtorchia", "Juan T.", y el email de trabajo versus el personal. Sin normalización, el SQL te va a decir que hay cuatro personas diferentes trabajando en el mismo repo. Armé una tabla de aliases:

-- Tabla para normalizar identidades
CREATE TABLE autor_aliases (
  email_original TEXT PRIMARY KEY,
  autor_canónico TEXT NOT NULL
);

INSERT INTO autor_aliases VALUES
  ('juanchi@gmail.com',      'Juan Torchia'),
  ('juan@trabajo.com',       'Juan Torchia'),
  ('jtorchia@cliente.com',   'Juan Torchia');

-- Query con join para normalizar
SELECT
  COALESCE(aa.autor_canónico, c.autor) AS autor_real,
  COUNT(*) AS total_commits
FROM commits c
LEFT JOIN autor_aliases aa ON c.email = aa.email_original
GROUP BY autor_real
ORDER BY total_commits DESC;
Enter fullscreen mode Exit fullscreen mode

Esta misma idea me hizo acordar al laburo de normalización que hice cuando migré el monorepo de npm a pnpm — la parte más tediosa siempre es limpiar los datos históricos, no la migración en sí. El install que bajó de 14 minutos a 90 segundos fue la parte sexy; las horas anteriores deduplicando dependencias no lo fueron.

FAQ — Preguntas frecuentes sobre analizar historial de git con SQL

¿Necesito pgit específicamente o puedo hacer esto con cualquier base de datos?

No necesitás pgit. Es una inspiración, no un requisito. Con git log --format personalizado y cualquier script de parseo podés llenar una tabla en PostgreSQL, SQLite, o incluso DuckDB (que es ideal para esto porque podés consultarlo directo sobre archivos CSV sin ni siquiera crear tablas). pgit es una implementación opinionada en Perl; la idea es portable.

¿Cuánto espacio ocupa el historial del kernel de Linux en PostgreSQL?

El historial completo del kernel con metadata básica (sin diffs completos) ronda los 2-4 GB. Si incluís el contenido de cada patch, hablamos de terabytes. Para repos personales normales — miles de commits, no millones — una base de datos de 50-200 MB es lo esperado. Totalmente manejable en cualquier instancia pequeña de Railway o Supabase.

¿Esto sirve para analizar el trabajo de mi equipo o es solo para proyectos personales?

Sirve perfecto para equipos, pero hay que tener cuidado con el contexto. Un commit count bajo no significa que alguien labure menos — puede significar que hace commits más grandes, que trabaja en branches de larga duración, o que está en un rol que no requiere commits frecuentes (code review, arquitectura, documentación). Los datos son datos; la interpretación requiere contexto humano. Usarlo para métricas de performance individuales sin ese contexto es una pésima idea y una forma segura de destruir la confianza del equipo.

¿Qué pasa con el contenido de los commits, no solo la metadata?

Si querés analizar el contenido real de los diffs — qué cambió dentro de los archivos — el volumen explota rápidamente. Lo más práctico es guardar solo la metadata en SQL y usar git show <hash> bajo demanda para recuperar el diff cuando lo necesitás. Alternativamente, podés guardar el diff completo en una columna TEXT o en un campo JSONB, pero para repos grandes va a ser lento y caro en storage. Para búsqueda full-text sobre diffs, algo como Elasticsearch o incluso FTS de PostgreSQL puede ayudar.

¿Hay herramientas ya hechas para esto sin tener que armar el pipeline a mano?

Sí. git-quick-stats te da análisis rápido sin base de datos. Hercules es más sofisticado y analiza burndown de código por autor. gitinspector es otro clásico. La diferencia con hacer tu propio pipeline a PostgreSQL es la flexibilidad: con SQL podés responder cualquier pregunta que se te ocurra, no solo las que el tool contempló. Si estás explorando, SQL gana. Si querés un reporte estándar, usá las herramientas.

¿Esto tiene algo que ver con cómo los LLMs analizan código?

Conceptualmente sí, y es un área interesante. Algunos pipelines de orquestación de agentes como los que vimos con Scion usan el historial de git como contexto para que los agentes entiendan cómo evolucionó un codebase. Cuando un agente puede consultar "qué archivos se modificaron junto con este módulo históricamente" está haciendo arqueología de git de la misma manera. La diferencia es que en vez de SQL están usando embeddings y búsqueda semántica, pero la fuente de datos es la misma: el historial de git tratado como datos.

Lo que me llevé de esta tarde

Hay algo perturbador en consultarte a vos mismo con SQL. No en el sentido ansioso — en el sentido de que te da información que tu memoria no te da. Yo recordaba el proyecto que me quemó. No recordaba la precisión quirúrgica con la que dejé de commitear. Eso está en los datos. Los datos no tienen sesgos de memoria.

Lo mismo que aplico cuando analizo accesibilidad real versus el score de Lighthouse aplica acá: los números te dicen algo, pero no todo. Un commit count bajo puede ser disciplina o puede ser bloqueo creativo. Eso lo sabés vos, no la query.

Lo que sí te dice el historial con certeza es dónde pusiste tu atención. Y eso, con el tiempo suficiente, es un retrato bastante honesto de quién sos como programador.

Mis repos me dijeron que commiteo mejor a la mañana, que testeo poco, y que cuando un proyecto me emociona el ritmo es imposible de ignorar en los datos. Tres cosas que "ya sabía" pero que ver en un GROUP BY las hace difíciles de racionalizar.

Si tenés repos con historia, aunque sea dos o tres años, te recomiendo gastarte una tarde en esto. No para optimizarte. Para entenderte.

El pipeline que armé está en GitHub — si querés el script completo, mandame un mensaje. Y si hacés esto con tus propios repos y encontrás algo interesante (o incómodo), me interesa saber qué apareció.


Si te quedaste con ganas de más arqueología de software: el mismo espíritu explorador lo apliqué cuando armé la extensión de visor de certificados x509 — en vez de seguir parseando output de openssl a mano, lo traté como un problema de datos. Y cuando me harté de esperar que alguien maintuviera la extensión de HAProxy, la historia de cómo llegué ahí también tiene commits con mensajes vergonzosos a las 11pm. Los datos no mienten.


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)