Skip to content

Le staging par hunk n'est pas représentable dans le contrat de revue #163

Closed

Description

@benfavre

Suivi séparé de l'item de #128 — « combined staged/unstaged/untracked review with file/hunk staging and conflict state ». La moitié fichier est livrée (PR #162, adossée à bext-stack/automonique #225) ; la moitié hunk ne l'est pas, et ne peut pas l'être en l'état.

Pourquoi ce n'est pas un manque d'implémentation

Le contrat ne peut pas l'exprimer :

  • ReviewProposal porte des identifiants de fichiers ;
  • une action de staging nomme une proposition, pas un fragment ;
  • l'identifiant de hunk de ReviewAnchor ne sert qu'aux commentaires.

Aucun chemin ne permet donc de désigner un hunk comme cible d'écriture, et en inventer un côté client produirait une cible que le serveur ne peut ni valider ni clôturer.

Ce qu'il manque vraiment

Deux choses, en amont dans bext-stack/automonique :

  1. Une proposition ou une action cadrée au hunk, pour que la cible soit nommable sur le fil.
  2. Une identité de hunk assez stable pour clôturer une écriture dessus. C'est le point difficile, et c'est le même raisonnement qui a conduit #225 à faire porter à une projection l'oid de HEAD et l'état de l'index : un digest doit s'engager sur ce qu'un préflight peut réellement prouver. Un numéro de hunk est une position dans un diff, pas une identité — il se déplace dès que le fichier bouge. Sans identité stable, un digest de staging par hunk ressemblerait à une barrière sans rien retenir.

Recommandation

Chiffrer l'identité de hunk avant de construire quoi que ce soit — c'est la question qui décide si la fonctionnalité est raisonnable, pas un détail d'implémentation. Le staging fichier reste utilisable entre-temps.

Activity

  1. benfavre commented on Aug 30, 2026

    @benfavre
    OwnerAuthor

    Verdict

    Non. Il n'existe pas d'identité de hunk assez stable pour clôturer une écriture, et ce n'est pas un champ qui manque. Trois constats, chacun suffisant à lui seul :

    1. un hunk n'est pas une propriété du dépôt, mais d'un rendu : il n'y a donc rien de stable à adresser ;
    2. le hunk qui circule sur le fil n'est pas une observation du worktree que l'écriture toucherait : les deux ne se rencontrent jamais ;
    3. git n'offre aucun mécanisme clôturable : le seul qui existe, git apply --cached, écrit avec succès dans une région que personne n'a relue.

    Le staging par hunk ne doit donc pas être rendu représentable sous ce nom. Ce qui est représentable, et que je décris en fin de commentaire, est une écriture d'index partielle scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. Ce n'est pas une identité de hunk, c'est son contournement, et cela coûte une famille en amont plus l'inversion d'un invariant de #225. Je ne recommande pas de le construire aujourd'hui.

    Tout ce qui suit est mesuré, avec de quoi rejouer.


    1. Un hunk n'est pas une propriété du dépôt

    1.1 Adresser le contenu ne suffit pas : deux hunks peuvent être rigoureusement identiques

    Un fichier contenant deux fois le même bloc de 7 lignes, séparés par 30 lignes de remplissage, la même ligne éditée dans les deux :

    @@ -1,7 +1,7 @@          @@ -38,7 +38,7 @@ filler30
     aaa                      aaa
     bbb                      bbb
     ccc                      ccc
    -TARGET                  -TARGET
    +TARGET-EDITED           +TARGET-EDITED
     ddd                      ddd
     eee                      eee
     fff                      fff
    

    Corps des deux hunks, en-tête exclu, hachés séparément :

    132c1c63dd5f89d13608ba7b62f6baf307f07f01d40ad43dd1236f5fd756f793  hunk1.body
    132c1c63dd5f89d13608ba7b62f6baf307f07f01d40ad43dd1236f5fd756f793  hunk2.body
    

    Identiques. Le même pré-image, le même fichier, deux hunks distincts, une seule empreinte. Élargir le contexte ne corrige rien : il suffit de répéter un bloc plus large. Une adresse de contenu seule est ambiguë à l'intérieur d'un seul fichier.

    Reste la position pour désambiguïser. Mais :

    1.2 La position dépend de paramètres que rien n'observe

    Trois éditions aux lignes 10, 16 et 40 d'un fichier de 60 lignes, mêmes deux blobs, seul le contexte change :

    -U0  -> 3 hunks : @@ -10 +10 @@   @@ -16 +16 @@   @@ -40 +40 @@
    -U2  -> 3 hunks : @@ -8,5 +8,5 @@ @@ -14,5 +14,5 @@ @@ -38,5 +38,5 @@
    -U3  -> 2 hunks : @@ -7,13 +7,13 @@              @@ -37,7 +37,7 @@
    -U10 -> 2 hunks : @@ -1,26 +1,26 @@             @@ -30,21 +30,21 @@
    

    « Hunk 2 » désigne l'édition de la ligne 16 à -U2 et celle de la ligne 40 à -U3. Le nombre même de hunks change.

    diff.indentHeuristic, réglage utilisateur actif par défaut depuis git 2.14, déplace la frontière et le corps :

    indentHeuristic=false        indentHeuristic=true
    @@ -5,2 +5,6 @@              @@ -4,2 +4,6 @@
     {                            (ligne vide)
    +  new();                    +{
    +}                           +  new();
    +                            +}
    +{                           +
       b();                       {
    

    Départ ligne 5 contre ligne 4, et des lignes ajoutées différentes. Une empreinte de contenu calculée sous une configuration n'existe pas sous l'autre.

    Un pilote textconv déclaré dans .gitattributes supprime le diff entièrement :

    c.txt diff=lower   +   git config diff.lower.textconv 'tr A-Z a-z <'
    diff par défaut : 1 hunk (-beta / +BETA)
    avec le pilote  : diff VIDE, zéro hunk
    --no-textconv   : le hunk revient
    

    Enfin, le rendu que l'humain lit et celui que le serveur dériverait doivent être le même diff. Sur un fichier où la première modification n'est que de l'espacement :

    diff par défaut -> 2 hunks : @@ -2,7 +2,7 @@   @@ -12,7 +12,7 @@
    diff -w         -> 1 hunk  :                   @@ -12,7 +12,7 @@
    

    Si le relecteur lit le diff -w, « hunk 1 » vaut ligne 15. Si le serveur dérive le diff par défaut, « hunk 1 » vaut ligne 5.

    Un hunk est fonction de (blob A, blob B, algorithme, largeur de contexte, heuristique d'indentation, attributs/textconv, version de git). Aucun de ces paramètres de dérivation n'est observé ni porté nulle part dans le contrat. J'ai relu GitWorktreeObservation::digest (rust/crates/automonique-daemon/src/platform_v2_git_worktree_adapter.rs, rev 524073c3) : il scelle l'étiquette de version, la famille, le commit HEAD, la branche, le digest d'index, le côté du conflit, le (mode, objet) résolu, puis par fichier le chemin, l'objet dans HEAD, l'objet dans l'index, les étages de conflit, l'empreinte stat et les drapeaux staged/unstaged/untracked, puis les staged_paths. Rien sur le rendu d'un diff.

    Conclusion partielle. La position n'est pas une identité en général. Elle en devient une relativement à un couple de blobs épinglé et à des paramètres de dérivation figés. C'est le seul point de sauvetage possible, et il ne survit pas au constat suivant.


    2. Le hunk du fil n'est pas une observation du worktree

    C'est le constat le plus structurel, et il ne dépend d'aucune configuration.

    Ce que le contrat porte réellement (rust/crates/automonique-protocol/src/platform_v2_review.rs) :

    pub struct DiffHunk {
        id: ReviewHunkId,
        old_start: u32, old_lines: u32,
        new_start: u32, new_lines: u32,
        preview: ReviewHunkPreview,   // BoundedString<512>
    }
    pub struct ReviewAnchor { file_id: ReviewFileId, hunk_id: ReviewHunkId, side: DiffSide, line: u32 }

    Le hunk transporte des positions et un aperçu de 512 octets. Pas son contenu. Le document de conception du contrat (docs/product-plan/platform-v2-review-contract.md) le dit dans ses propres termes : la projection « file and hunk » se termine par « bounded sanitized hunk preview », la proposition porte « an exact bounded ordered file set », et « Binary bytes [...] have no representation ». Un client ne peut donc ni calculer une adresse de contenu de hunk à partir de ce qu'il reçoit, ni en émettre une.

    Et surtout, à la révision épinglée 524073c3, sur l'arbre entier :

    DiffHunk::new   -> 1 seul site hors tests : platform_v2_review_api.rs:274  (le décodeur)
    ReviewFile::new -> 1 seul site hors tests : platform_v2_review_api.rs:310  (le décodeur)
    

    Aucun code de production de bext-stack/automonique ne construit un hunk à partir d'un dépôt git. La liste de hunks est décodée d'un document, pas observée.

    Symétriquement, l'adaptateur que #225 a ajouté pour observer et écrire le dépôt, platform_v2_git_worktree_adapter.rs (1990 lignes), ne contient aucune occurrence de « hunk ». Il observe par chemin :

    pub struct GitFileObservation {
        path: RepositoryFile,
        head_object: Option<ObjectId>,
        index_object: Option<ObjectId>,
        conflict: Option<ConflictStages>,
        worktree: Option<FileStamp>,
        staged: bool, unstaged: bool, untracked: bool,
    }

    Les deux moitiés ne se rencontrent jamais. Le magasin le formule mieux que moi, dans le commentaire de ReviewPlan::git_staging (rust/crates/automonique-store/src/review_store.rs) :

    Everything sealed here is either operator-owned (the canonical root), server-owned (the proposal, its files, the commit subject the snapshot carried) or observed (the head, the index, the observation digest). No client string is admitted [...]

    Un chemin est à la fois server-owned et observed : l'adaptateur peut aller le chercher dans le dépôt et prouver ce qu'il y trouve. Un identifiant de hunk ne peut être que le premier. Il n'existe rien dans le dépôt que l'adaptateur puisse consulter par identifiant de hunk pour confirmer que ce hunk existe. C'est exactement la distinction que l'issue appelle « méritée, pas déduite », dans le vocabulaire du code lui-même.

    Bornes annexes, qui achèvent le tableau : MAX_REVIEW_HUNKS_PER_FILE = 128 et MAX_REVIEW_HUNKS = 512. Un jeu de modifications plus large n'est pas représentable du tout, même en lecture.

    Côté ShellDeck, c'est cohérent

    ReviewHunkId n'apparaît que dans deux endroits hors tests, tous deux à l'intérieur de ReviewAction::AddComment → ReviewAnchor : crates/shelldeck-core/src/config/platform_review.rs:149 et crates/shelldeck-core/src/config/platform_review/custody.rs:500. Dans l'interface, le seul élément conscient d'un hunk est review_anchor_element_id (crates/shelldeck-ui/src/fleet_view.rs:219), le bouton d'ancre de commentaire. Aucun contrôle par hunk n'existe.

    Un piège trouvé au passage, et c'est la raison de la PR jointe. crates/shelldeck-core/src/workspace_review.rs modélise déjà ReviewMutationKind::StageHunks / UnstageHunks, autorisés par AuthorityScope::Repository { checkout, stage_hunks } et scellés par MutationTargetFence::LocalReview { checkout, review_revision }, en #[allow(dead_code)], à côté du commentaire « adapter wiring is a later integration milestone ». Rien hors tests ne produit de ReviewHunk, donc ces identifiants sont des UUID v4 qui n'étiquettent rien. Et review_revision épingle l'instantané de ce crate, pas le worktree : il ne bouge pas quand un autre processus tournant sous le même uid déplace HEAD, l'index ou le fichier. Câbler cela à un adaptateur git installerait très exactement la barrière creuse que cette issue existe pour empêcher.


    3. git apply --cached n'est pas une barrière

    3.1 C'est pourtant le seul mécanisme que git propose

    GIT_TRACE=2 git add -p :

    run_command: git diff-files --no-color --ignore-submodules=dirty -p -- ':(,prefix:0)f.txt'
    (2/2) Stage this hunk [y,n,q,...]?
    run_command: git apply --cached
    

    git add -p rend le diff, prend la sélection, et applique un patch reconstruit. Il n'existe aucune plomberie qui indexe un fragment par chemin.

    3.2 Il applique à n'importe quel décalage, et le dit avec un code de retour 0

    Patch frappé quand l'index tenait le blob b5c3d22, ciblant @@ -17,7 +17,7 @@. Un autre processus déplace ensuite l'index (10 lignes insérées en tête, blob 550e70a, la cible d'édition est passée en ligne 30) :

    git apply --cached                       -> EXIT 0  APPLIQUÉ, l'édition atterrit ligne 30 (le patch disait 20)
    git apply --cached -C1                   -> EXIT 0  ligne 30
    git apply --cached --recount             -> EXIT 0  ligne 30
    git apply --cached --ignore-whitespace   -> EXIT 0  ligne 30
    git apply --cached --3way                -> EXIT 0  ligne 30
    

    La ligne index b5c3d22..65da919 que porte le patch est ignorée. --3way n'est pas une contrainte, c'est un repli quand l'application directe échoue : ici elle réussit, donc le repli ne sert jamais. Il n'existe aucune option de git apply qui refuse tant que le pré-image n'est pas exactement un oid donné.

    3.3 Et il écrit dans une région que personne n'a relue

    Le cas qui décide. Fichier avec deux occurrences du même bloc (lignes 11-17 et 26-32). Le relecteur édite la première, le patch dit @@ -11,7 +11,7 @@. Entre le préflight et l'exécution, un autre processus retire la première occurrence de l'index. Le patch est alors appliqué :

    ### apply the reviewer's patch to that index
    EXIT 0 -- APPLIQUÉ
    
    ### résultat
        19  aaa
        20  bbb
        21  ccc
        22  TARGET-EDITED     <-- la SECONDE occurrence, jamais relue, jamais approuvée
        23  ddd
    

    Code de retour 0, aucune erreur, aucun avertissement, et l'écriture atterrit dans une région différente de celle qui a été revue. C'est la définition même du mode de défaillance que l'issue décrit : une règle qui a l'air de garder quelque chose et ne garde rien.

    (Note pour qui rejouerait : un hunk qui commence à la ligne 1 arme match_beginning dans apply.c et refuse. Il faut un hunk en milieu de fichier pour observer le comportement ci-dessus. Ma première tentative a été un faux négatif pour cette raison.)

    3.4 L'autre voie casse l'invariant central de #225

    La seule alternative à git apply est de construire le blob résultant et de l'écrire avec update-index --cacheinfo. Or #225 a bâti tout son module sur l'inverse, et le dit :

    // Git writes the working-tree bytes, from the stage it is already
    // holding. This process never opens the file, so it can neither choose
    // the content nor follow a symlink out of the worktree.

    Les trois écritures respectent cette règle : git add -- <pathspec>, git restore --staged --source=<oid HEAD observé> -- <pathspec>, et checkout-index --force --stage=N suivi de update-index --add --cacheinfo <mode>,<objet observé>,<chemin>. Chacune nomme un chemin et laisse git fournir le contenu depuis un endroit que le processus ne choisit pas.

    Une écriture par hunk doit ouvrir le fichier, choisir les octets et écrire un nouvel objet. Ce n'est pas un champ en plus, c'est une posture de sécurité différente, qui mérite son propre argument et sa propre autorisation.


    4. L'observation par fichier est déjà trop faible pour dériver

    GitFileObservation épingle head_object et index_object comme oids de blob, donc deux engagements de contenu réels. Mais le côté worktree n'est qu'une empreinte stat, et #225 admet lui-même le résidu :

    Its one residual is git's: a rewrite that preserves size, inode and both timestamps to the nanosecond is not distinguished.

    Mesuré :

    contenu=AAAA  stat=5 1577836800 46667191  oid=b194361...
    contenu=BBBB  stat=5 1577836800 46667191  oid=a07e243...
    

    Même taille, même mtime, même inode, contenu différent.

    Pour le staging fichier, ce résidu est borné : git add indexe ce qui est sur le disque et la vérification post-écriture ne contrôle que « diffère de HEAD ». Pour le staging hunk, il ne l'est pas : le contenu décide quels hunks existent. Une réécriture racée change silencieusement la décomposition contre laquelle la sélection est résolue. Le staging par hunk exigerait donc un engagement de contenu que le staging par fichier peut se permettre de ne pas avoir.

    Corollaire à noter : git diff-files --raw affiche des zéros du côté worktree. Seul git hash-object donne cet oid. Bonne nouvelle au passage, git hash-object sans -w calcule sans écrire (vérifié : l'objet n'est pas dans la base ensuite), donc un préflight resterait sans mutation.


    5. Ce qui serait représentable, si un jour on le veut

    Pas du staging par hunk. Une écriture d'index partielle, scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. La question de l'identité de hunk se dissout : on n'a pas besoin d'identifier un hunk, on a besoin d'identifier un résultat, et un oid de blob est l'objet le plus vérifiable que git possède.

    Forme, si la décision amont était prise :

    • famille PartialIndexWrite, toujours cadrée au fichier, la proposition nommant les fichiers exactement comme aujourd'hui ;
    • l'observation par chemin gagne un engagement de contenu pour le côté worktree (git hash-object, sans -w), de sorte que le couple (index_object, worktree_object) soit intégralement épinglé ;
    • la sélection voyage comme un jeu de sélecteurs que le serveur re-dérive : (début pré-image, longueur pré-image, début post-image, longueur post-image, sha256 du corps du hunk). Ce n'est pas un identifiant : c'est un prédicat, qui n'a de sens que résolu contre un couple de blobs épinglé. Refus sur 0 correspondance et refus sur 2 ou plus, parce que le point 1.1 rend le cas « 2 ou plus » réel ;
    • les paramètres de dérivation sont figés et entrent dans le digest : -U<n>, algorithme, heuristique d'indentation explicitement posée, --no-textconv --no-ext-diff, et aucun drapeau ignorant l'espacement. Le diff que l'humain a lu et celui que le serveur dérive doivent être le même ;
    • le collage se fait dans l'espace des blobs, pas sur les octets du disque. Avec * text=auto eol=crlf, les octets du disque et ceux du blob diffèrent (mesuré : \r\n sur le disque, \n dans le blob, hash-object applique le filtre, hash-object --no-filters non), donc coller des lignes brutes du worktree dans le blob d'index produirait le mauvais objet ;
    • et ce que le digest clôture réellement est l'oid du blob résultant B', calculé au préflight. Vérifié :
    blob d'index épinglé A = b5c3d22...   blob worktree épinglé W = c15b41d...
    hunks dérivés du couple : @@ -7,7 +7,7 @@   @@ -27,7 +27,7 @@
    B' calculé trois fois   : ef909cc4b24b760b14430b2cb25f04dbd6bef528 (identique)
    update-index --cacheinfo -> index = ef909cc4...
    diff indexé   : -line10 / +line10-EDIT      (exactement le hunk 1, rien d'autre)
    diff non indexé : -line30 / +line30-EDIT    (exactement le reste)
    

    Aucun git apply, aucune recherche de contexte, aucun décalage possible. Un sélecteur mal résolu ne peut pas produire une écriture différente, parce qu'une écriture différente est un B' différent et fait échouer le digest.

    Pourquoi je ne recommande quand même pas de le construire

    • Coût amont : nouvelle famille, nouveau champ d'observation, nouveaux champs de digest, dans un dépôt qui n'est pas le nôtre.
    • Inversion d'invariant : cela retire à #225 son « ce processus n'ouvre jamais le fichier ». Cela mérite sa propre autorisation, pas un élargissement d'index_write.
    • Sérialisation, et c'est le point qui tue l'intérêt du produit. Comme la confirmation scelle le digest de l'index entier, chaque écriture par hunk invalide toutes les autres confirmations en cours du dépôt. Mesuré : après avoir indexé le seul hunk 1, le blob d'index passe de b5c3d22 à ef909cc4 et l'empreinte de git diff-index --cached HEAD de e3b0c442 à 35f4cb01. Une interface qui propose « indexer les hunks 1, 2, 3 » doit donc relire les capacités entre chaque, un aller-retour par hunk, chacun refusable. C'est une interaction nettement moins bonne que le contrôle par fichier déjà livré, pour une fonctionnalité dont tout l'attrait est la rapidité.

    À noter aussi : la direction désindexation est en meilleure posture que l'indexation, parce que ses deux extrémités (HEAD → index) sont déjà des oids de blob observés, sans nouveau champ. Si l'on devait un jour ouvrir cette porte, c'est par là qu'elle serait la moins chère et la mieux prouvée.


    6. Ce que je recommande pour cette issue

    1. Requalifier Le staging par hunk n'est pas représentable dans le contrat de revue #163 : le titre est juste, la conclusion est que le staging par hunk n'est pas représentable et n'a pas à le devenir sous ce nom. Ce qui reste ouvert, si quelqu'un en veut un jour, est une écriture d'index partielle scellée sur un oid de blob, qui est une décision amont.
    2. Garder l'issue comme trace de conception plutôt que comme dette. Le point de Add workspace review, agent attention, and delivery workflows #128 est clos pour la moitié fichier ; la moitié hunk n'est pas « à faire plus tard », elle est répondue.
    3. Ne pas câbler le réducteur dormant. C'est le seul risque concret que l'enquête a fait apparaître.
    4. Le staging fichier reste utilisable et inchangé.

    Ce qui est livré

    PR #165, documentation uniquement : le commentaire au site sur StageHunks / UnstageHunks dans workspace_review.rs, avec les trois raisons, plus une entrée de journal dans docs/testing/USE_CASES.md à côté de celle du staging fichier. Pas de SDUC ni de SDTEST : rien ne change de comportement, et un test ici affirmerait qu'une variante non câblée est toujours non câblée, ce que l'absence d'appelant garantit déjà.

  2. benfavre commented on Aug 30, 2026

    @benfavre
    OwnerAuthor

    Fermé comme décision consignée, pas comme dette

    La question que ce ticket demandait de chiffrer avant de construire quoi que ce soit a reçu sa réponse : il n'existe pas d'identité de hunk capable de clôturer honnêtement une écriture. Ce n'est pas un champ manquant, et aucune version du contrat ne le rendrait représentable.

    Trois constats, chacun suffisant seul, détaillés dans le commentaire d'analyse et consignés par la PR #165 :

    1. Un hunk est une propriété d'un rendu, pas du dépôt. Mesuré sur la même paire de blobs : trois hunks en -U2 et deux en -U3 ; diff.indentHeuristic (config utilisateur, active par défaut depuis git 2.14) déplace la frontière ; -w donne un autre découpage, donc un relecteur et un serveur qui ne rendent pas le même diff ne parlent pas du même « hunk 1 » ; un pilote textconv fait produire zéro hunk à la même paire. Et l'adressage par contenu ne sauve rien : deux hunks d'un même fichier, contexte complet inclus, ont été construits avec un sha256 identique.
    2. Le hunk sur le fil n'est pas une observation de l'arbre de travail. DiffHunk::new et ReviewFile::new n'ont qu'un seul appelant non-test : le décodeur. Rien, côté Rust ou SDK TypeScript, ne construit un hunk à partir d'un dépôt. Ce que porte DiffHunk, la doc l'appelle un aperçu.
    3. git apply --cached n'est pas une barrière, et c'est le seul mécanisme dont git dispose. Il applique à n'importe quel décalage : sortie 0 contre un blob d'index dont le patch n'est pas issu, hunk écrit dix lignes plus loin, idem avec -C1, --recount, --ignore-whitespace, --3way. Pire cas mesuré : région relue retirée de l'index, sortie 0 et écriture dans une autre région semblable que personne n'avait relue.

    Ce qui reste possible, et pourquoi ce n'est pas mieux

    Une écriture d'index partielle mais clôturée sur l'oid du blob résultant, la sélection étant re-dérivée côté serveur depuis une paire de blobs épinglée : le fichier reste l'unité que le serveur clôture, l'identité de hunk n'est jamais requise. Spécifié et vérifié de bout en bout. Mais cela coûte une famille amont, inverse l'invariant central de #225 (« ce processus n'ouvre jamais le fichier, il ne peut donc pas choisir le contenu »), et surtout sérialise : la confirmation scelle l'empreinte d'index entière, donc chaque écriture de hunk invalide toutes les autres confirmations en cours. L'interaction serait moins bonne que le contrôle par fichier déjà livré.

    ⚠️ Un piège laissé en place volontairement

    crates/shelldeck-core/src/workspace_review.rs modélise déjà StageHunks / UnstageHunks en #[allow(dead_code)], derrière MutationTargetFence::LocalReview { checkout, review_revision }, à côté d'un commentaire disant que le câblage viendra plus tard. review_revision épingle l'instantané de ShellDeck, pas l'arbre de travail : les câbler installerait exactement la barrière creuse que ce ticket existe pour empêcher. La PR #165 pose les raisons à cet endroit-là, pour que la prochaine personne les lise avant de tirer le fil.

    Le staging par fichier reste livré et utilisable. Fermé.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions