Usando Gemini 3 Pro para analizar git diffs entre branches

Estuve trabajando en un dashboard de deployments que muestra el estado de nuestras ramas deploy-* en múltiples repositorios. Para dar contexto, usamos un branch deploy-[nombre del ambiente] por cada ambiente y main es nuestro ambiente de dev. La idea es simple: ver qué ramas están atrasadas respecto a un ambiente anterior, cuántos commits, y qué cambian esos commits.
El problema es que leer git diffs es tedioso, especialmente cuando estás comparando ramas que pueden tener decenas de commits de diferencia. Necesitaba algo que pudiera resumir qué cambió sin que el equipo tenga que revisar cientos de líneas de diff ni estar revisando cada commit/PR que fue mergeado a ese branch.
Hay que tener en claro que el objetivo final no es revisar el código en si (que ya fue previamente revisada en los PRs), sino que la idea es poder entender que estamos mandando al ambiente, que impacto van a tener los cambios, que riesgos hay al deployar estos cambios y si tenemos pasos previos a realizar (ajustes en recursos, cambios en bases de datos, etc.)
Con todo eso en mente, se me ocurrió probar Gemini 3 Pro, aprovechando sus capacidades mejoradas para lo que es desarrollo, para que revise el diferencial entre los branches y haga ese trabajo por nosotros, pasando de revisar diff a revisar realmente lo que necesitamos revisar.
¿Por qué Gemini 3 Pro y no Claude o GPT-4? Tres razones: contexto de 1M de tokens (suficiente para diffs masivos), costos significativamente menores (centavos por análisis vs dólares), y las mejoras específicas en code reasoning que tiene Gemini 3 con el thinking mode. Para este caso de uso, la relación costo-beneficio-performance es imbatible.
El Problema
Cuando manejás múltiples ambientes de deployment (staging, qa, producción, etc.), necesitás saber:
- Qué features hay en cada rama
- Si hay breaking changes que todavía no deployaste
- Qué cambios de infraestructura tenés que hacer antes de mergear (nuevas variables de entorno, migraciones de DB, recursos de cloud)
La vista de comparación de branches que tiene GitHub (https://github.com/${repoFullName}/compare/${base}...${head}) muestra el diff completo, pero sigue siendo crudo. Para cambios pequeños puede ser manejable, pero para una semana de commits de multiples devs en el equipo y con múltiples archivos modificados, el contexto se pierde en el ruido.
Los Detalles Técnicos
Conseguir el Diff
Primero, necesitás el diff de GitHub. La librería Octokit hace esto bastante simple:
export async function getComparisonDiff(
owner: string,
repo: string,
base: string,
head: string
): Promise<string | null> {
try {
const { data } = await octokit.repos.compareCommits({
owner,
repo,
base,
head,
mediaType: {
format: 'diff', // Este es el punto clave
},
});
return data as unknown as string;
} catch (error: unknown) {
if (
error &&
typeof error === 'object' &&
'status' in error &&
error.status === 404
) {
console.warn(
`Branch not found when getting diff for ${owner}/${repo}: ${base}...${head}`
);
return null;
}
throw error;
}
}
El truco está en mediaType: { format: 'diff' }. Sin eso, obtenés JSON con metadata de commits. Con eso, obtenés el diff en formato unificado.
Mandarlo a Gemini
Una vez que tenés el diff, lo mandás a Gemini para que lo analice. Pero acá está el tema: los diffs pueden ser gigantes. Gemini 3 Pro tiene límites de tokens altos (1M tokens de input), pero igual tenés que ser práctico.
Primero creamos el cliente, para eso usamos el paquete @google/genai (el SDK oficial de Google para Gemini, no confundir con @google/generative-ai que está siendo deprecado) y instanciamos GoogleGenAI. Para generar la API KEY navegamos a https://aistudio.google.com/app/apikey y seguimos los pasos. Lo ideal es guardar esta key en un secret manager pero aquí para simplificar lo dejo como variable de entorno.
import { GoogleGenAI } from '@google/genai';
let genAI: GoogleGenAI | null = null;
export function getGeminiClient(): GoogleGenAI {
if (!genAI) {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error('GEMINI_API_KEY is not configured');
}
genAI = new GoogleGenAI({ apiKey });
}
return genAI;
}
Y luego procedemos al analisis de estos cambios. Para limitar los costos y los tiempos nos quedamos solo con una parte considerable del diff (es raro que el diff tenga mas de 200 mil caracteres, dado que intentamos hacer salidas a los diferentes ambientes de forma frecuente). Para finalizar, hacemos el pedido usando el modelo gemini-3-pro-preview mandando el prompt y el diff (podriamos mejorar esto pero para una prueba de concepto rápida es una muy buena opción).
export async function analyzeBranchChanges(
diff: string
): Promise<BranchAnalysis> {
try {
const ai = getGeminiClient();
// Limitar el tamaño del diff para controlar costos y latencia
const maxDiffSize = 200000;
const truncatedDiff =
diff.length > maxDiffSize
? diff.substring(0, maxDiffSize) +
'\n\n[... diff truncated due to size ...]'
: diff;
const model = 'gemini-3-pro-preview';
// Convert Zod schema to JSON schema for structured output
const responseJsonSchema = z.toJSONSchema(branchAnalysisSchema);
const config = {
thinkingConfig: {
thinkingLevel: ThinkingLevel.HIGH,
},
tools: [{ googleSearch: {} }],
responseMimeType: 'application/json' as const,
responseJsonSchema,
};
const response = await ai.models.generateContentStream({
model,
config,
contents: [
{
role: 'user',
parts: [{ text: ANALYSIS_PROMPT + truncatedDiff }],
},
],
});
// Manejar el streaming
let fullText = '';
for await (const chunk of response) {
if (chunk.text) {
fullText += chunk.text;
}
}
// Parsear y validar la respuesta JSON
const analysis: BranchAnalysis = branchAnalysisSchema.parse(
JSON.parse(fullText)
);
return analysis;
} catch (error) {
// Siempre devolver algo, aunque sea vacío
return {
summary: [],
validationChecklist: [],
breakingChanges: [],
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
Sobre el Thinking Mode
Gemini 3 tiene nuevos parametros diseñados para dar mas control en costos, latencia y fidelidad multimodal. Uno de ellos es el thinking level, que podés configurarlo en LOW, MEDIUM o HIGH. Yo fui por HIGH porque code review no es algo que querés apurar. Actualmente este es el modo defecto y el MEDIUM todavía no está soportado.
thinkingConfig: {
thinkingLevel: ThinkingLevel.HIGH,
}
¿Hace diferencia? Sí. El modelo realmente razona el diff en vez de solo hacer pattern matching. Atrapa cosas como "ah, esto agrega una nueva variable de entorno S3_BUCKET, mejor mencionarlo en el checklist."
Costos y Límites
Gemini 3 Pro tiene un contexto de entrada de 1 millón de tokens, lo cual es suficiente para diffs masivos. Para dar contexto, un diff de 200k caracteres representa aproximadamente 50k tokens, quedando muy por debajo del límite.
En cuanto a costos, Gemini es considerablemente más económico que otros LLMs del mercado. Para este tipo de análisis, estamos hablando de centavos por análisis (literalmente, menos de 5 centavos por request típico). Si tenés 50 análisis por día, son menos de $2.50 al día. Nada comparado con el tiempo que ahorrás al equipo.
El Prompt
Como prompt, la idea fue mantenerlo simple pero que quede claro que necesitamos que resuelva.
const ANALYSIS_PROMPT = `You are a code review assistant analyzing git diffs.
Analyze the following diff and identify:
1. New features or changes implemented
2. Things that need validation before merging (database migrations, environment variables, cloud resources, third-party integrations, infrastructure changes)
3. Any breaking changes that could affect existing functionality
If there are no items for a category, provide an empty array.
If the diff is too large or cannot be analyzed, set the error field.
Here's the diff to analyze:
`;
Notar que dentro del promp, no estamos aclarando el tipo de output ni el formato que necesito, pero claramente voy a necesitar un formato estructurado en JSON. Para eso la api tiene el concepto de Structured outputs.
Structured outputs
Defini con zod el formato del output de la siguiente forma. Para convertir el schema de Zod a JSON Schema (que es lo que Gemini espera), vamos a usar la función z.toJSONSchema (feature nativa de Zod v4 - si usás versiones anteriores, necesitás el paquete zod-to-json-schema):
// Define Zod schema for structured output
const branchAnalysisSchema = z.object({
summary: z
.array(z.string())
.describe(
'A concise bullet-point list of new features/changes implemented (3-5 bullets max)'
),
validationChecklist: z
.array(z.string())
.describe(
'Things that need to be checked/validated before merging, such as database migrations, environment variables, cloud resources, third-party integrations, or infrastructure changes'
),
breakingChanges: z
.array(z.string())
.describe('Any breaking changes that could affect existing functionality'),
error: z
.string()
.optional()
.describe('Error message if the diff cannot be analyzed'),
});
Luego al momento de procesar el request se le agrega como parte de la configuración responseMimeType: 'application/json' y responseJsonSchema: z.toJSONSchema((branchAnalysisSchema). Nuestra configuración completa queda de la siguiente forma.
// Convert Zod schema to JSON schema for structured output
const responseJsonSchema = z.toJSONSchema(branchAnalysisSchema);
const config = {
thinkingConfig: {
thinkingLevel: ThinkingLevel.HIGH,
},
tools: [{ googleSearch: {} }],
responseMimeType: 'application/json' as const,
responseJsonSchema,
};
Algunas consideraciones generales
No Bloquees la UI
Este análisis puede tomar unos cuantos segundos (especialmente dependiendo de la cantidad de cambios que existan). No hagas que los usuarios esperen. Cargá primero los datos de comparación de ramas (commits adelante/atrás), después traé el análisis de AI de forma asíncrona o, mejor aún, bajo demanda.
¿Si el análisis falla o se toma demasiado tiempo? Mostrá la comparación normal. El análisis de AI es un nice-to-have, no un requirement.
Fallar con Gracia
Mirá ese bloque catch de nuevo:
catch (error) {
return {
summary: [],
validationChecklist: [],
breakingChanges: [],
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
Evitá mostrar errores directamente al usuario. Si Gemini está caído o se alcanza el rate limit, devolvé arrays vacíos. La UI puede manejar eso de forma elegante. Un modal de error para un feature secundario degrada innecesariamente la experiencia de usuario.
Sé Específico Sobre Lo Que Importa
No le pido a Gemini feedback general sobre calidad de código o sugerencias de mejora. Le pido tres cosas específicas:
- Qué cambió (resumen)
- Qué necesitás hacer antes de mergear (checklist)
- Qué puede romperse (breaking changes)
Eso es todo. Más que eso, y estás saturando a la gente. Los developers no queremos un ensayo ni tener que revisar algo mas largo que el propio diff, queremos información accionable y simple.
Estrategia de Truncado
Aunque Gemini 3 Pro tiene 1M de tokens de contexto, truncar en 200k caracteres, que son aproximadamente 50k tokens, tiene sentido por tres razones: es más rápido, más barato, y reduce el ruido. Si un diff es tan grande, probablemente las ramas han divergido significativamente y los primeros 200k chars te dan suficiente contexto.
Podrías hacerlo configurable o ser más inteligente sobre qué partes incluir (priorizar archivos cambiados sobre archivos eliminados/agregados, por ejemplo).
Parsear JSON Requiere Manejo Defensivo
Incluso cuando pedís JSON explícitamente, a veces la respuesta puede no coincidir con el schema configurado, por lo que siempre está bueno validar el output. Hacer esto con zod es muy simple y ayuda a no tener mayores problemas a nivel del front.
Con simplemente hacer un const analysis = branchAnalysisSchema.parse(JSON.parse(fullText)); ya validamos la respuesta del modelo y podemos trabajar mas tranquilos en el front.
Ejemplo del Mundo Real
Supongamos que estás comparando deploy-staging con main, y main está 10 commits adelante. El diff incluye:
- Una nueva integración con Stripe
- Nuevas variables de entorno para API keys
- Una migración de base de datos para agregar una tabla
subscription - Algo de refactoring en el módulo de auth
Gemini devolvería algo así (ejemplo simplificado para ilustrar el formato):
{
"summary": [
"Add Stripe payment integration for subscription management",
"Create subscription database schema with migration",
"Refactor authentication module to support subscription-based access",
"Add webhook endpoints for Stripe events"
],
"validationChecklist": [
"Set STRIPE_API_KEY and STRIPE_WEBHOOK_SECRET environment variables",
"Run database migration to create subscription table",
"Configure Stripe webhook URL in Stripe dashboard",
"Test payment flow in staging environment before deploying to production"
],
"breakingChanges": [
"Auth middleware now checks subscription status, may block users without active subscriptions"
]
}
Eso es útil. Sabés exactamente qué hacer antes de mergear y a quien deberías avisar por los breaking changes.
¿Lo Usaría en Producción?
Sí, definitivamente. Pero con algunas consideraciones importantes:
- Cachear los resultados: El mismo diff debería devolver el mismo análisis. Cachealo por commit SHAs.
- Rate limiting: Si tenés 50 repos con 5 ramas cada uno, son 250 llamadas a la API. Sé inteligente sobre cuándo analizás.
- Monitoreo de costos: Gemini es barato comparado con otros LLMs, pero no es gratis. Monitoreá tu uso.
- Controles de usuario: Dejá que los usuarios lo deshabiliten si no les resulta útil.
Reflexiones Finales
Por mas que es un ejemplo simple de las multiples utilidades que se le puede dar a un modelo como Gemini 3 Pro, realmente nos resultó muy util para una problematica que estabamos teniendo en nuestro día a día. Con muy poco código pudimos simplificar nuestras salidas a los diferentes ambientes con un buen nivel de seguridad, ahorrandonos muchos dolores de cabeza y tiempos.
Obviamente, como este ejemplo hay miles de otras tareas donde podemos aprovechar este tipo de modelos, espero que este articulo sirva para inspirar.
Y siempre, siempre diseñá para el fallo. Si la AI no funciona, tu app debería seguir funcionando. Esa es la diferencia entre un buen feature y un producto roto.
Si querés probar esto vos mismo, el código es directo. Necesitás:
- Un token de GitHub con acceso a repos
- Una API key de Gemini (generala en https://aistudio.google.com/app/apikey)
- Los paquetes:
@google/genai,@octokit/restyzod
La implementación total es menos de 150 líneas de código. La mayor parte del trabajo está en diseñar el prompt y manejar edge cases apropiadamente.

