Files
sasedev-knowledge/docs/studies/004-V0_0_2_GIT_IMPLEMENTATION_STUDY.md
2026-09-28 15:11:03 +02:00

14 KiB

Étude 0.0.2 — implémentation Git générique

Objet

Cette étude ferme la décision technique exigée avant 0.0.2-alpha.1 : choisir le mécanisme qui implémentera le premier backend Git provider-neutral de Knowledge sans introduire de dépendance à une forge.

Le besoin réel est limité au chemin de lecture :

source Git locale ou remote
  -> copie locale contrôlée
  -> refresh idempotent
  -> résolution d'une référence vers un commit
  -> snapshot/worktree de lecture déterministe

L'écriture Git, les pull/merge requests, les webhooks de forge, PostgreSQL, le parsing final et la publication restent hors scope.

Contraintes déjà acquises

Les contrats de 0.0.1 restent autoritaires :

  • knowledge-domain-lib possède l'identité logique SourceId et la famille de contenu ;
  • knowledge-git-api possède le contrat provider-neutral GitRepository ;
  • knowledge-sync-api possède les signaux non autoritaires RepositorySyncSignal ;
  • le domaine ne connaît ni Gitea, ni GitHub, ni GitLab ;
  • une requête publique ne déclenche jamais de fetch distant ;
  • les tests de la voie générique doivent fonctionner avec des repositories temporaires locaux ;
  • les erreurs doivent rester récupérables et aucune donnée sensible ne doit être journalisée.

Sources techniques consultées

Consultation effectuée le 2026-09-28 :

Ces sources éclairent le choix technique ; elles ne remplacent pas les tests réels de Knowledge.

Option A — processus git contrôlé

Capacités

Le client Git officiel couvre directement le besoin :

  • git clone --mirror produit un repository bare qui mappe l'ensemble des refs et configure leur mise à jour depuis le remote ;
  • --no-local permet d'éviter l'optimisation locale par hardlinks lorsqu'une source est un chemin local ;
  • git remote update --prune rafraîchit le mirror sans créer une logique de forge ;
  • git rev-parse --verify --end-of-options <ref>^{commit} vérifie qu'une référence fournie résout effectivement un commit et évite qu'elle soit interprétée comme une option ;
  • git worktree add --detach <path> <revision> matérialise un arbre de lecture associé exactement à la révision résolue.

Sécurité du processus

Rust std::process::Command passe les arguments directement au programme sans shell. Le backend peut donc imposer :

  • Command::new(...) vers un exécutable Git réel, jamais sh -c, bash -c, cmd.exe ou un fichier .bat ;
  • un argument par arg/args, sans concaténation de commande ;
  • --end-of-options ou -- lorsque la commande Git le permet ;
  • GIT_TERMINAL_PROMPT=0 pour empêcher une attente interactive d'un serveur ;
  • un current_dir et des chemins appartenant au backend ;
  • retrait des variables d'environnement qui peuvent rediriger implicitement un repository (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES) ;
  • désactivation des hooks via un core.hooksPath vide contrôlé pour les opérations pouvant déclencher un hook ;
  • source locale/remote provenant uniquement de configuration de déploiement privilégiée, jamais d'un paramètre de requête publique ;
  • rejet des syntaxes de remote-helper exécutables telles que ext:: et absence de transport arbitraire découvert depuis une entrée utilisateur ;
  • absence de credentials dans les logs et dans les erreurs publiques.

Les mécanismes de credentials autorisés restent une responsabilité de déploiement : SSH agent, credential helper ou autre mécanisme Git préconfiguré. Les secrets ne sont pas copiés dans la configuration versionnée de Knowledge.

Portabilité et exploitation

Cette option ajoute un prérequis runtime explicite : un exécutable git compatible doit être présent. Elle évite en revanche une nouvelle pile Git native dans Cargo et utilise le comportement du client Git réellement déployé.

Le backend demeure synchrone au niveau du contrat actuel. Les appels processus sont bloquants et devront être isolés par la couche runtime lorsqu'ils sont déclenchés depuis Actix ; cette isolation ne justifie pas de rendre les crates de contrat async.

Testabilité

Les tests d'intégration peuvent créer un repository temporaire local, y produire plusieurs commits puis exercer exactement la même implémentation que la production sans réseau ni forge publique.

Une prévalidation locale de cette étude a confirmé avec Git 2.47.3 la séquence :

clone --mirror --no-local
resolve refs/heads/main^{commit}
remote update --prune
resolve nouvelle révision
worktree add --detach
HEAD du snapshot = révision résolue
worktree propre

Cette prévalidation assistant ne constitue pas une gate opérateur.

Option B — gix

gix 0.88.0 fournit une abstraction Rust riche de repository Git. Sa feature matrix couvre notamment révisions, credentials, mutation de worktree et clients réseau bloquants ou async selon configuration.

Avantages :

  • API Rust et erreurs typées ;
  • pas de shell ni de dépendance à un exécutable Git pour les opérations couvertes ;
  • contrôle fin des features ;
  • bon candidat si Knowledge doit plus tard intégrer profondément des objets Git dans son processus.

Coûts pour 0.0.2 :

  • surface de features et API sensiblement plus large que le besoin actuel ;
  • choix supplémentaires pour le transport réseau et son modèle bloquant/async ;
  • davantage de code d'adaptation à maintenir pour reproduire et tester les détails de comportement attendus du client Git ;
  • aucune nécessité actuelle ne compense ce coût puisque le produit ne parse pas encore les objets Git lui-même.

gix reste donc une alternative future légitime, pas une option rejetée définitivement.

Option C — git2 / libgit2

git2 0.21.0 expose des bindings Rust vers libgit2. Le support réseau n'est pas activé par défaut ; HTTPS et SSH ajoutent des features et leurs dépendances associées. La crate dépend de libgit2-sys et peut construire ou lier libgit2 ; SSH/HTTPS peuvent également introduire libssh2/OpenSSL selon le choix de features.

Avantages :

  • API Rust mature et erreurs structurées ;
  • opérations locales, clone et fetch disponibles ;
  • pas d'exécutable Git requis à l'exécution lorsque toute la pile nécessaire est liée.

Coûts pour Knowledge :

  • dépendances natives supplémentaires et matrice de transport plus lourde ;
  • comportement libgit2 à valider séparément du client Git déployé ;
  • gain insuffisant pour une release qui n'a besoin que d'une matérialisation Git de lecture.

Cette option n'est pas retenue pour 0.0.2.

Option D — combinaison CLI + crate Git

Une combinaison pourrait utiliser le CLI pour clone/fetch et une crate pour lire les objets ou préparer les worktrees.

Elle n'apporte aujourd'hui aucune propriété nécessaire qui ne soit déjà couverte par le CLI. Elle doublerait au contraire les modèles d'erreur, les surfaces de compatibilité et les tests de cohérence.

Décision : ne pas introduire deux backends avant qu'un besoin concret le démontre.

Comparaison

Critère Processus git contrôlé gix git2
Fidélité au client Git déployé directe élevée mais à valider par opération libgit2, comportement distinct à valider
Source locale oui oui oui
Remote générique oui oui avec features réseau oui avec features réseau
mirror/fetch natif et direct disponible via API/features disponible via libgit2
worktree de lecture commande native API/features de mutation API libgit2 à composer
Erreurs structurées wrapper Knowledge nécessaire natives Rust natives Rust
Injection shell évitée avec Command direct sans shell sans shell
Dépendance runtime externe exécutable Git aucune pour les chemins couverts libgit2 et pile native liée/vendored
Async opérations bloquantes à isoler modes bloquant/async selon features principalement synchrone
Tests locaux reproductibles simples avec repositories temporaires possibles possibles
Coût d'intégration 0.0.2 faible à moyen moyen à élevé moyen à élevé

Décision

0.0.2 retient un processus git contrôlé comme première implémentation de knowledge-git-api.

La décision est locale à Knowledge et ne devient pas une règle PGR générale.

Le backend sera matérialisé dans :

crates/knowledge-git-lib/

Cette crate possédera :

  • la configuration physique d'une source Git locale ou remote ;
  • la racine d'état Git généré ;
  • l'exécution contrôlée de Git ;
  • le mirror local ;
  • refresh/résolution ;
  • la préparation et validation d'un snapshot de lecture.

knowledge-domain-lib conserve l'identité logique. knowledge-git-api conserve le contrat. La composition applicative associera un SourceId à une configuration concrète sans faire remonter les chemins ou URLs dans le domaine.

Racine d'état et layout retenus

Les copies Git générées ne résident jamais dans le dépôt source sasedev-knowledge ni dans un dépôt éditorial.

Le backend reçoit une racine d'état explicite, sans défaut implicite dans le workspace :

<git-state-root>/
  hooks-disabled/
  repositories/
    <source-key>.git/
  snapshots/
    <source-key>/
      <resolved-revision>/

La future composition runtime pourra alimenter cette racine via une configuration de déploiement telle que KNOWLEDGE_GIT_STATE_ROOT, mais knowledge-git-lib reçoit un PathBuf et ne dépend pas directement d'une variable d'environnement.

Règles :

  • source-key est dérivé de manière sûre par l'implémentation ; ni SourceId brut ni URL remote ne sont interpolés comme chemin ;
  • la racine d'état appartient exclusivement à une instance de backend/processus dans 0.0.2 ; le partage concurrent entre plusieurs processus reste hors contrat tant qu'un mécanisme de verrouillage inter-processus n'est pas matérialisé ;
  • un snapshot est adressé par la révision réellement résolue par le backend ;
  • un snapshot existant n'est réutilisé qu'après vérification que son HEAD correspond à cette révision et que le worktree est propre ; une divergence est une erreur, pas une suppression silencieuse ;
  • les credentials ne sont jamais dérivés dans les noms de chemins.

Politique de source locale et remote

Une source locale et une source remote convergent vers le même mirror contrôlé.

Pour une source locale, l'initialisation utilisera clone --mirror --no-local afin d'éviter de rendre la copie contrôlée dépendante des hardlinks/optimisations du repository source.

Après initialisation, le refresh utilise la configuration du remote du mirror et une mise à jour avec pruning. Le backend ne dépend donc pas d'une API de forge.

Résolution de révision

Une référence configurée reste opaque dans knowledge-git-api, mais l'implémentation doit produire une révision qui désigne un commit réel :

git --git-dir=<mirror> rev-parse --verify --end-of-options <reference>^{commit}

La sortie validée devient GitRevision. Aucun nom de branche ou tag non résolu n'est utilisé comme identité de snapshot.

Ownership de synchronisation

RepositoryEventSource reste inchangé : il ne fait qu'émettre des signaux.

Le cas d'usage partagé qui enchaîne refresh, résolution, comparaison de la précédente révision et préparation du snapshot appartient à la couche application. La candidate existante knowledge-app-lib devient donc justifiée lorsque cette orchestration est implémentée ; elle ne sera pas créée en alpha.1 uniquement comme placeholder.

En l'absence de PostgreSQL dans 0.0.2, la dernière révision observée est fournie par l'appelant au cas d'usage puis retournée dans son résultat. Cette release ne crée pas de pseudo-base locale cachée.

La cause Periodic reste supportée par le contrat de signal, mais un scheduler concret est différé tant que son owner runtime/lifecycle n'est pas démontré. Une future source périodique devra appeler le même cas d'usage que la synchronisation explicite.

Conséquences pour les prochaines tranches

alpha.1 ne modifie aucun contrat Rust fonctionnel : elle ferme les décisions nécessaires et crée le plan.

La première extension de knowledge-git-api sera limitée au besoin démontré de matérialisation d'un snapshot de lecture. Les APIs status/diff/branch/commit/merge/push restent hors scope.

Le changement de backend reste possible ultérieurement derrière le même contrat si les mesures de maintenance, portabilité ou performance justifient gix ou une autre solution.