Clase 145 — Git y control de versiones para proyectos políglotas

Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

Hunt y Thomas, en The Pragmatic Programmer, cuentan que su primer consejo a cualquier equipo es poner todo bajo control de versiones —incluso las notas y la configuración— porque el sistema de versiones es la red de seguridad que permite experimentar sin miedo: si algo sale mal, siempre hay un estado bueno al que volver. Esta clase toma esa idea y la lleva al terreno políglota, donde un mismo repositorio alberga Python, Go, Java, Rust y más, cada uno con sus artefactos de compilación y su ruido particular. El reto no es usar Git —eso se aprende rápido—, sino usarlo bien en un monorepo con diez ecosistemas conviviendo.

El historial de Git es una secuencia de commits: instantáneas del proyecto, cada una con su mensaje, encadenadas de forma que puedes recorrer el pasado, ver quién cambió qué y volver atrás con precisión quirúrgica. El laboratorio destila esta estructura a su operación más elemental —contar los commits de un historial— porque contar es lo que hace por dentro cualquier herramienta que resume la actividad de un repositorio (git rev-list --count, las gráficas de contribuciones, los informes de release). Sobre ese esqueleto mínimo montaremos las decisiones que de verdad importan: qué versionar, qué ignorar y cómo dividir el trabajo en commits legibles.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Contar los commits de un historial dado.
  2. Explicar qué es un commit y por qué el historial es una red de seguridad.
  3. Diseñar un .gitignore que excluya los artefactos de build de varios lenguajes.
  4. Argumentar cuándo conviene un monorepo y cuándo submódulos.

🗺️ Temas

# Tema Por qué importa
1 Commit atómico Un cambio coherente por commit: historial legible
2 Historial como red de seguridad Volver atrás sin miedo a experimentar
3 .gitignore por lenguaje No versionar __pycache__, target/, node_modules/
4 Monorepo vs. submódulos Cómo organizar varios lenguajes en un repositorio

📖 Definiciones y características

Git. Un sistema de control de versiones distribuido: cada copia clonada contiene el historial completo, no un fragmento. Esto es lo que lo hace robusto —no hay un servidor central del que dependa todo— y lo que permite trabajar sin conexión y ramificar con coste casi nulo. Para Hunt y Thomas esta ubicuidad del historial es la esencia de la red de seguridad.

Commit atómico. Un commit registra una instantánea del proyecto con un mensaje. La virtud que se persigue es la atomicidad: cada commit debe capturar un único cambio coherente y funcional, de modo que su mensaje lo describa con una frase y pueda revertirse aislado. Un commit que mezcla un arreglo de un bug con un reformateo masivo es imposible de revisar y de deshacer limpiamente.

.gitignore. El archivo que le dice a Git qué no seguir. En un proyecto políglota es crítico: cada lenguaje genera artefactos que jamás deben versionarse (__pycache__/ y *.pyc en Python, target/ en Rust y Java, node_modules/ en JavaScript, binarios compilados en Go y C). Versionarlos ensucia el repositorio, provoca conflictos absurdos e infla el clon.

🧩 Situación

Trabajas en un monorepo que combina un backend en Go, scripts en Python y una interfaz en TypeScript. Un compañero, sin .gitignore adecuado, comitea por error su carpeta node_modules/ y los binarios de go build: el repositorio pasa de unos megabytes a cientos, cada git status se llena de ruido y los merges empiezan a chocar en archivos generados que a nadie le importan. La disciplina que evita este desastre tiene tres patas: un .gitignore con secciones por lenguaje, commits atómicos con mensajes claros, y la regla de oro de no versionar nunca lo que la build puede regenerar. Antes de todo eso, conviene entender la operación básica sobre el historial —contarlo— que implementa el laboratorio.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
fix add refactor commits=3
init commits=1
a b c d commits=4

📐 Algoritmo (pseudocódigo neutral)

LEER mensajes ; ESCRIBIR cantidad

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/. El contrato: leer mensajes de commit separados por espacio y escribir commits=<cantidad>. Con fix add refactor sale commits=3; con init, commits=1.

Python · python/main.py · python main.py

import sys

msgs = sys.stdin.read().split()
print(f"commits={len(msgs)}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

La solución de Python es casi telegráfica y por eso ilustra bien la operación. sys.stdin.read().split() lee toda la entrada y la trocea por espacios en una lista de tokens; len(...) cuenta cuántos hay. Cada token representa un commit, así que su longitud es el número de commits del historial —exactamente lo que devuelve git rev-list --count HEAD sobre un repositorio real. Que .split() sin argumentos colapse múltiples espacios es una comodidad deliberada: hace que un historial con separación irregular se cuente igual de bien, sin tokens vacíos que inflen el resultado.

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const msgs = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`commits=${msgs.length}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const msgs: string[] = readFileSync(0, "utf8").trim().split(/\s+/);
console.log(`commits=${msgs.length}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] msgs = br.readLine().trim().split("\\s+");
        System.out.println("commits=" + msgs.length);
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

string[] msgs = Console.In.ReadToEnd()
    .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine($"commits={msgs.Length}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    msgs := strings.Fields(line)
    fmt.Printf("commits=%d\n", len(msgs))
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n = s.split_whitespace().count();
    println!("commits={n}");
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

int main(void) {
    char tok[256];
    int c = 0;
    while (scanf("%255s", tok) == 1) c++;
    printf("commits=%d\n", c);
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

El contraste entre C y SQL revela dos formas de «contar». C no construye ninguna colección: lee token a token con scanf("%255s", ...) —el límite 255 es una defensa consciente contra el desbordamiento del buffer, tan característica del rigor de Kernighan y Ritchie— e incrementa un contador. SQL, en cambio, modela los commits como filas de una tabla y aplica count(*): la operación de contar es primitiva en el modelo relacional que describe Date en SQL and Relational Theory, porque una relación es un conjunto de tuplas y su cardinalidad es un dato de primera clase. Ambos llegan al mismo número por caminos conceptualmente opuestos: uno imperativo y byte a byte, otro declarativo y sobre conjuntos.

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL: cuenta las filas (commits).
WITH commits(msg) AS (VALUES ('fix'), ('add'), ('refactor'))
SELECT printf('commits=%d', count(*)) AS resultado FROM commits;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
$msgs = preg_split('/\s+/', trim(fgets(STDIN)));
echo "commits=" . count($msgs) . "\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔬 Comparación

El conteo es idéntico; lo que cambia entre lenguajes es qué artefactos de build hay que excluir del control de versiones. Un .gitignore de monorepo políglota necesita una sección por ecosistema.

Lenguaje Qué NO versionar (ejemplos)
Python __pycache__/, *.pyc, .venv/, dist/, *.egg-info/
JavaScript / TypeScript node_modules/, dist/, *.tsbuildinfo
Java target/, build/, *.class
C# bin/, obj/
Go binarios compilados, *.exe
Rust target/
C *.o, *.a, binarios
SQL dumps y .db locales
PHP vendor/

Nota transversal: lo que sí se versiona en todos es el lockfile (clase 143), porque forma parte del código fuente reproducible; lo que nunca se versiona es la carpeta de dependencias descargadas (node_modules/, vendor/, .venv/), porque se regenera desde el lock. Distinguir ambas cosas es la decisión más frecuente y más mal resuelta en repositorios políglotas.

🧬 El concepto en la familia

Git domina, pero el modelo de instantáneas versionadas encadenadas por un hash lo comparten Mercurial y, en otro nivel, sistemas como Fossil o Pijul. La gran decisión de arquitectura en proyectos con varios lenguajes es monorepo vs. submódulos: el monorepo mantiene todo en un solo historial —commits atómicos que cruzan lenguajes, un único punto de verdad— a costa de un repositorio grande; los submódulos (o git subtree) enlazan repositorios independientes, útil cuando cada componente tiene su propio ciclo de vida pero más frágil a la hora de coordinar cambios. Este curso mismo es un monorepo políglota, y su .gitignore es exactamente del tipo que describe la tabla anterior.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 145

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 144 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 146 ⏭️