Votre première contribution à gitrust

Objectifs

À la fin de ce tutoriel, vous saurez :

  • O1. Créer une branche de fonctionnalité depuis main et identifier un good first issue dans le backlog.
  • O2. Écrire un test unitaire qui échoue avant d'écrire le code de production (cycle TDD rouge-vert).
  • O3. Appliquer la chaîne QA complète (cargo fmt, cargo clippy, cargo test) et ouvrir une pull request sur la plateforme gitrust.

Pré-requis

  • Technique : Rust ≥ 1.77 installé, git ≥ 2.40, accès en lecture/écriture à une instance gitrust (locale ou demo.gitrust.eu), tutoriel 01-getting-started complété (environnement local qui compile).
  • Pédagogique : tutoriel 01-getting-started complété.
  • Temps estimé : ~90 minutes.

Vue d'ensemble

Contribuer à gitrust suit un cycle court et répétable : fork ou branche → test rouge → code minimal → tests verts → QA → PR.

sequenceDiagram
    participant Vous
    participant Git
    participant Cargo
    participant Plateforme

    Vous->>Git: git switch -c feat/mon-fix
    Vous->>Cargo: cargo test → ROUGE (test échoue)
    Vous->>Cargo: écrire le code minimal
    Vous->>Cargo: cargo test → VERT
    Vous->>Cargo: cargo fmt + cargo clippy
    Vous->>Git: git push origin feat/mon-fix
    Vous->>Plateforme: ouvrir Pull Request

Le cycle TDD (Test-Driven Development) est essentiel ici : en écrivant d'abord le test, vous définissez précisément ce que votre code doit faire avant de l'écrire. C'est ce que gitrust appelle rouge → vert → refactor.

Étape 1 : Identifier un good first issue

Rendez-vous sur la page issues du dépôt gitrust (ou sur votre instance locale). Filtrez par le label de classification good-first-issue.

Lisez l'issue attentivement. Pour ce tutoriel, nous allons simuler le cas d'un issue fictif :

Issue #42RepoSlug devrait rejeter les slugs commençant par un tiret.

Assignez-vous l'issue (bouton « Assignee » dans la sidebar) pour signaler que vous travaillez dessus.

Checkpoint : vous devriez voir votre nom dans la liste des assignés de l'issue.

Étape 2 : Créer une branche de travail

Depuis la racine du dépôt gitrust, créez une branche nommée selon la convention fix/<numéro>-description-courte ou feat/<numéro>-description-courte :

git switch main
git pull --rebase origin main
git switch -c fix/42-repo-slug-rejects-leading-dash

Sortie attendue :

Switched to a new branch 'fix/42-repo-slug-rejects-leading-dash'

Checkpoint : git branch doit afficher votre branche avec un * devant.

Étape 3 : Écrire un test qui échoue (rouge)

Avant d'écrire le moindre code de production, écrivez le test qui décrit le comportement attendu. Ouvrez le fichier contenant la validation des slugs :

crates/gitrust-core/src/models/repository.rs

Repérez le bloc #[cfg(test)] existant en bas du fichier (ou créez-le s'il est absent). Ajoutez ce test dans la section tests :

#[cfg(test)]
mod tests {
    use super::*;

    // Tests existants...

    #[test]
    fn repo_slug_rejects_leading_dash() {
        // Un slug qui commence par '-' doit être invalide.
        // Ce test DOIT échouer avant notre correctif.
        let result = RepoSlug::new("-invalid-slug");
        assert!(
            result.is_err(),
            "Un slug commençant par '-' devrait être rejeté"
        );
    }
}

Exécutez maintenant les tests pour confirmer que ce test échoue :

cargo test --package gitrust-core repo_slug_rejects_leading_dash

Sortie attendue (rouge) :

test models::repository::tests::repo_slug_rejects_leading_dash ... FAILED

failures:
    models::repository::tests::repo_slug_rejects_leading_dash

test result: FAILED. 0 passed; 1 failed

Checkpoint : le test échoue. C'est normal — c'est exactement l'état « rouge ».

Jargon : RepoSlug est un newtype — un type Rust qui enveloppe String et applique une validation à la construction via RepoSlug::new(). Cette technique garantit qu'un RepoSlug valide ne peut jamais exister sans passer par la validation.

Étape 4 : Écrire le code minimal (vert)

Localisez la fonction RepoSlug::new dans crates/gitrust-core/src/models/repository.rs. Elle ressemble à ceci (simplifié) :

impl RepoSlug {
    pub fn new(s: &str) -> Result<Self, ValidationError> {
        if s.is_empty() || s.len() > 64 {
            return Err(ValidationError::InvalidSlug("longueur invalide".into()));
        }
        if s.contains("..") || s.contains('/') {
            return Err(ValidationError::InvalidSlug("caractères interdits".into()));
        }
        // ... autres validations
        Ok(Self(s.to_lowercase()))
    }
}

Ajoutez uniquement la vérification manquante — ne modifiez rien d'autre :

impl RepoSlug {
    pub fn new(s: &str) -> Result<Self, ValidationError> {
        if s.is_empty() || s.len() > 64 {
            return Err(ValidationError::InvalidSlug("longueur invalide".into()));
        }
        // Nouveau : rejeter les slugs commençant par un tiret
        if s.starts_with('-') {
            return Err(ValidationError::InvalidSlug(
                "un slug ne peut pas commencer par un tiret".into(),
            ));
        }
        if s.contains("..") || s.contains('/') {
            return Err(ValidationError::InvalidSlug("caractères interdits".into()));
        }
        Ok(Self(s.to_lowercase()))
    }
}

Relancez le test :

cargo test --package gitrust-core repo_slug_rejects_leading_dash

Sortie attendue (vert) :

test models::repository::tests::repo_slug_rejects_leading_dash ... ok

test result: ok. 1 passed; 0 failed

Checkpoint : le test passe. Vous êtes en état « vert ».

Étape 5 : Vérifier que rien d'autre ne régresse

Un seul test qui passe ne suffit pas. Vérifiez l'ensemble de la suite du crate :

cargo test --package gitrust-core

Sortie attendue :

running XX tests
...
test result: ok. XX passed; 0 failed; 0 ignored

Checkpoint : aucun test ne régresse.

Étape 6 : Passer la chaîne QA

Gitrust impose trois gates obligatoires avant toute PR. Exécutez-les dans cet ordre :

Gate 1 — Formatage :

cargo fmt --all -- --check

Si des différences apparaissent, appliquez le format automatiquement :

cargo fmt --all

Gate 2 — Linting (zéro warning) :

cargo clippy --workspace -- -D warnings

Sortie attendue :

    Checking gitrust-core v0.1.0
    ...
    Finished `dev` profile

Gate 3 — Tests complets :

cargo test --workspace

Sortie attendue :

test result: ok. XXX passed; 0 failed; 0 ignored

Checkpoint : les trois commandes se terminent sans erreur ni warning.

Étape 7 : Committer selon la convention

Gitrust utilise des messages de commit conventionnels (feat:, fix:, test:, refactor:, docs:). Commitez votre travail :

git add crates/gitrust-core/src/models/repository.rs
git commit -m "fix: rejeter les slugs commençant par un tiret (issue #42)"

Ne committez jamais avec git add -A sans avoir inspecté git diff --staged au préalable. Vérifiez qu'aucun fichier .env, target/, ou fichier généré n'est inclus.

Checkpoint : git log --oneline -3 montre votre commit en tête.

Étape 8 : Pousser et ouvrir une PR

Poussez votre branche vers l'origine :

git push origin fix/42-repo-slug-rejects-leading-dash

Rendez-vous sur la plateforme gitrust. Un bandeau « Vous venez de pousser une branche — ouvrir une PR ? » devrait apparaître. Sinon, naviguez vers /{votre-username}/gitrust/pulls/new.

Remplissez le formulaire :

  • Titre : fix: rejeter les slugs commençant par un tiret
  • Corps : Renseignez le contexte, la cause du bug, la solution choisie, et ajoutez Closes #42 pour lier automatiquement l'issue.
  • Branche source : fix/42-repo-slug-rejects-leading-dash
  • Branche cible : main

Cliquez sur « Ouvrir la pull request ».

Checkpoint : la PR apparaît dans la liste /{owner}/gitrust/pulls avec le statut open.

Récapitulatif

  • O1 accompli en identifiant l'issue #42 et en créant la branche fix/42-repo-slug-rejects-leading-dash.
  • O2 accompli en écrivant repo_slug_rejects_leading_dash qui échouait avant l'ajout de s.starts_with('-').
  • O3 accompli en passant cargo fmt, cargo clippy -- -D warnings et cargo test --workspace sans erreur, puis en ouvrant la PR liée à l'issue.

Et si ça ne marche pas

Symptôme Cause probable Correction
cargo clippy signale needless_pass_by_value Votre nouvelle fonction prend une String alors qu'un &str suffit Changez le paramètre en &str
cargo test échoue sur un test non lié Votre modification a cassé un invariant adjacent Relisez les tests voisins ; revenez à un diff minimal
git push refusé avec remote: pre-receive hook declined La branche est protégée ou votre token SSH n'est pas enregistré Vérifiez /settings/keys sur la plateforme

Prochaine étape

Tutoriel 03 — Créer un worker async (capstone)