Analyse du mécanisme de fusion trois-voies dans Git

La fusion trois-voies de Git repose sur une chaîne de traitements répartis entre plusieurs modules du code source. L'objectif est d'identifier un ancêtre commun, de mesurer les écarts entre deux branches et de réconcilier les modificatiosn au niveau fichier. Cette description se concentre sur les étapes essentielles et propose une relecture des structures clés sous un angle différent.

1. Recherche de l'ancêtre commun

Git parcourt le graphe orienté acyclique des commits pour trouver le point de divergence le plus récent entre deux branches. La fonction centrale exploite les parents de chaque commit et gère le cas des ancêtres multiples.

Fichier source : merge-base.c

struct commit *trouver_ancetre_commun(struct commit *branche_a,
                                      struct commit *branche_b)
{
    struct commit_list *candidates;
    struct commit *ancetre;

    /* Extraction des ancêtres communs potentiels */
    candidates = get_merge_bases(branche_a, branche_b);
    ancetre = pop_commit(&candidates);

    /* En cas de fusion octopus, on vide la liste pour la suite */
    while (candidates) {
        liberer_liste_commits(candidates);
        candidates = get_merge_bases(branche_a, branche_b);
    }

    return ancetre;
}

2. Calcul des différences

Une fois l'ancêtre identifié, Git compare son arbre avec ceux des deux révisions à fusionner. Deux séries de différences sont produites : l'une pour la branche locale, l'autre pour la branche distante.

Fichier source : diff.c

void calculer_diffs(const struct tree *base,
                    const struct tree *revis_a,
                    const struct tree *revis_b)
{
    struct diff_options options_diff;

    /* Écarts entre l'ancêtre et la première branche */
    comparer_arbres(base, revis_a, &options_diff);

    /* Écarts entre l'ancêtre et la seconde branche */
    comparer_arbres(base, revis_b, &options_diff);

    /* Normalisation et filtrage des résultats */
    normaliser_resultats_diff(&options_diff);
}

3. Logique de fusion trois-voies

Le cœur de l'algorithme traverse récursivement les arbres et détermine, pour chaque entrée, si elle peut être fusionnée automatiquement ou si elle doit être marquée comme conflit.

Fichier source : merge-recursive.c

int fusionner_arbres(const struct tree *base,
                     const struct tree *revis_a,
                     const struct tree *revis_b,
                     struct merge_options *opt)
{
    struct traverse_info contexte;
    int statut;

    memset(opt, 0, sizeof(*opt));
    opt->ours = revis_a;
    opt->theirs = revis_b;

    statut = parcourir_hierarchie(base, revis_a, revis_b,
                                 &contexte, opt);

    if (statut < 0) {
        error("Conflit de fusion détecté");
        return -1;
    }

    return 0;
}

Parcours récursif des entrées

int parcourir_hierarchie(const struct tree *base,
                         const struct tree *revis_a,
                         const struct tree *revis_b,
                         struct traverse_info *ctx,
                         struct merge_options *opt)
{
    /* Si les deux branches modifient la même entrée de manière incompatible */
    if (modifications_en_conflit(revis_a, revis_b)) {
        enregistrer_conflit(ctx, revis_a, revis_b);
        return -1;
    }

    /* Application de la fusion au niveau fichier */
    fusionner_fichier(ctx, base, revis_a, revis_b, opt);
    return 0;
}

Fusion au niveau fichier

void fusionner_fichier(struct traverse_info *ctx,
                       const struct tree *base,
                       const struct tree *revis_a,
                       const struct tree *revis_b,
                       struct merge_options *opt)
{
    struct contenu_fichier contenu;

    contenu.origine = lire_contenu(base);
    contenu.local   = lire_contenu(revis_a);
    contenu.distant = lire_contenu(revis_b);

    if (!appliquer_fusion_3voies(&contenu, opt)) {
        fprintf(stderr, "Conflit : résolution manuelle requise pour %s\n",
                ctx->path);
    }
}

4. Gestion des conflits

Lorsque les modifications ne peuvent pas être fusionnées automatiquement, Git insère des marqueurs dans le fichier pour matérialiser les zones litigieuses.

<<<<<<< M_BRANCHE
modification locale
=======
modification distante
>>>>>>> AUTRE_BRANCHE

Fichier source : merge-recursive.c

int appliquer_fusion_3voies(struct contenu_fichier *contenu,
                            struct merge_options *opt)
{
    struct diff_resultat resultat;

    resultat = diff_fichiers(contenu->origine,
                             contenu->local,
                             contenu->distant);

    if (resultat.conflit) {
        ecrire_marqueurs_conflit(contenu, opt);
        return -1;
    }

    ecrire_fichier_fusion(contenu, opt);
    return 0;
}

5. Création du commit de fusion

Quand tous les fichiers sont résolus, Git crée un commit possédant deux parents : le pointeur de la branche courante et celui de la branche fusionnée.

Fichier source : commit.c

int creer_commit_fusion(const char *message,
                        struct commit *tete,
                        struct commit *autre)
{
    struct commit_list *parents = NULL;

    commit_list_append(tete, &parents);
    commit_list_append(autre, &parents);

    return inserer_commit(message, parents);
}

Cartographie des responsabilités

  • Recherche de l'ancêtre : merge-base.c
  • Calcul des différences : diff.c
  • Algorithme de fusion : merge-recursive.c
  • Insertion des marqueurs de conflit : merge-recursive.c
  • Création du commit : commit.c

Référence : code source disponible sur https://github.com/git/git.

Étiquettes: Git merge-recursive three-way-merge diff-algorithm merge-base

Publié le 18 juin à 21h19