Architecture Decision Records - ADR260201c

2026/02/01

Shell-out a git binary invece di libgit2

Autore: Marco Orlandin, Architect
Data: 01 Febbraio 2026
Status: Implementato
Progetto: git-side (open-source, sviluppato per Solexma LLC)
Constraint principali: Comportamento identico al git dell'utente, build semplice senza dipendenze native, distribuzione come singolo binario

Contesto e problema

git-side è scritto in Rust e ha bisogno di eseguire operazioni Git: init, add, commit, push, pull, log, rev-list, e altre. In Rust esistono diverse opzioni per interagire con Git programmaticamente, ognuna con trade-off significativi.

La scelta impatta direttamente su:

  • Compatibilità: il comportamento deve essere identico a quello che l'utente si aspetta dal proprio git.
  • Build: dipendenze native (come libgit2) complicano la compilazione cross-platform.
  • Manutenzione: più astrazione = più superficie da mantenere.

Requisiti non funzionali

  • Comportamento identico al git installato dall'utente
  • Build semplice, senza dipendenze native (C libraries)
  • Singolo binario distribuibile
  • Supporto per tutte le operazioni Git necessarie (porcelain e plumbing)
  • Gestione errori chiara quando git non è installato

Opzioni valutate

Opzione 1: git2-rs (binding Rust per libgit2)

  • Pro: API Rust-native, tipizzata, nessun parsing di output testuale. Ampiamente usata nell'ecosistema Rust.
  • Contro: Dipendenza nativa da libgit2 (C library). Build più complessa, specialmente in cross-compilation. Comportamento potenzialmente diverso dal git dell'utente (libgit2 non è git). Alcune operazioni avanzate non supportate o con semantica diversa.

Opzione 2: gitoxide (implementazione Git pura in Rust)

  • Pro: Puro Rust, nessuna dipendenza C. Prestazioni potenzialmente superiori.
  • Contro: Progetto ancora in evoluzione, non tutte le operazioni sono stabili. API in continuo cambiamento. Feature parity con git non garantita.

Opzione 3: Shell-out al binario git

  • Pro: Comportamento identico al git dell'utente, per definizione. Zero dipendenze native. Build banale. Supporta qualsiasi operazione git, incluse quelle plumbing.
  • Contro: Richiede git installato sulla macchina. Parsing dell'output testuale. Gestione errori meno tipizzata (exit code + stderr).

Decisione

Scelto lo shell-out al binario git tramite std::process::Command.

Motivazioni principali:

  1. Garanzia di comportamento: se git funziona per l'utente, funziona per git-side.
  2. Zero dipendenze native: il binario Rust compilato è autosufficiente.
  3. Accesso completo: qualsiasi comando git è disponibile, inclusi plumbing commands come hash-object e update-index.
  4. Semplicità: nessuna astrazione intermedia da mantenere.

Il presupposto è esplicito: git-side richiede git installato. Chi usa git-side ha già git, non è un vincolo reale.

Implementazione passo-passo

  1. Modulo git.rs: Wrapper sottili attorno a std::process::Command che impostano GIT_DIR, GIT_WORK_TREE e puliscono variabili d'ambiente problematiche.
  2. Gestione output: Cattura di stdout e stderr, check dell'exit code, conversione in tipo Result<String, Error>.
  3. Error handling: Enum custom via thiserror con varianti specifiche per i diversi tipi di errore git.
  4. Nessun parsing avanzato: L'output di git viene usato "as-is" dove possibile (log, status, diff passano direttamente all'utente).
  5. Plumbing diretto: Per operazioni come il self-versioning di .side-tracked, si usano comandi plumbing (hash-object, update-index) senza intermediari.

Conseguenze osservate

  • Build time del progetto ridotto rispetto a soluzioni con dipendenze native.
  • Nessun problema di compatibilità cross-platform (testato macOS e Linux).
  • Il parsing dell'output si è rivelato minimale: la maggior parte dei comandi non richiede interpretazione complessa.
  • Lezioni apprese:
    • Shell-out non è una scorciatoia, è la scelta giusta quando il comportamento "reale" di git è il requisito.
    • La pulizia delle variabili d'ambiente è cruciale: senza, i comandi git ereditano contesti sbagliati (specialmente nelle hook).
    • Wrapper sottili > wrapper profondi. Meno si astrae, meno si rompe.

Stack utilizzato

  • Linguaggio: Rust (Edition 2024, MSRV 1.85)
  • Interfaccia Git: std::process::Command
  • Error handling: crate thiserror
  • Linting: Clippy con all, pedantic, nursery

Quando considerare questo approccio

Se il tuo tool deve interagire con Git e:

  • Il comportamento deve essere identico al git dell'utente
  • Non vuoi dipendenze native
  • Hai bisogno di accesso a comandi plumbing
  • La build semplice è un requisito

...shell-out è quasi sempre la scelta giusta. Aggiungi libgit2 o gitoxide solo se hai bisogno di performance su operazioni massive (migliaia di commit, diff su grandi alberi).

Hai un caso simile? Contattami. Valutiamo insieme il trade-off per il tuo contesto.