K. Code Archeologist ‐ Augmentation de l'API - uha-fr/endyear_2025_gr11_back GitHub Wiki

Prérequis

  1. Langage : Node.js
  2. Base de données : PostgreSQL
  3. Extensions de PostgreSQL :
  • pgvector
  • pgvectorscale
  • pgai
  1. Modèle d'embedding des commits (non implémenté ici) : Ollama - nomic-embed-text

Architecture

Le backend de code-archeologist tourne à l'aide d'un unique fichier App.jsx.

Le point focal de cette application s'organise en 3 temps :

  1. Lancement de l'analyse centralisée - route : POST /api/analyze

  2. Récupération et stockage des métriques du dépôt -

  • getTotalCommitCountAllBranches : recupérer le nombre de commits
  • fetchContributors : récupérer les contributeurs du dépôt
  • fetchCommitActivityAllBranches : récupérer le nombre de commit par jour pour chaque contributeur
  • fetchFileChangesAllBranches : récupérer le nombre de modification de chaque auteur pour chaque fichier
  • fetchIssues : récupérer les issues
  • fetchDependencies : récupérer les dépendances
  • fetchAndProcessCommits : récupérer chaque commit et ses informations
  • fetchBlameAllBranches : récupérer le nombre de ligne UNIQUE présente dans le dépôt au cours du temps pour chaque contributeur

Cet assemblage de métrique est stocké dans la table code_analysis de la base de données de code-acheologist.

  1. Envoie d'une partie des métriques de l'analyse vers un module exterieur -
  • route : GET /api/analysis-data : envoyer toutes les données de l'objet code_analysis
  • route : GET /api/analysis/:analysisId : envoyer l'objet code_analysis
  • route : GET /api/file-change-frequency : envoyer le nombre de modification de chaque auteur pour chaque fichier
  • route : GET /api/commit-activity-timeline : envoyer le nombre de commit par jour pour chaque contributeur
  • route : GET /api/contributor-statistics : envoyer le nombre de commit par jour pour chaque contributeur
  • route : GET /api/blame-evolution : envoyer le nombre de ligne UNIQUE présente dans le dépôt au cours du temps pour chaque contributeur
  • route : GET /api/global-stats : envoyer le nombre de commits, ajouts et suppressions par contributeur
  • route : GET /api/code-evolution : envoyer les commits et leurs informations

Autres routes non utilisées :

  • /api/codebase-heatmap
  • /api/dependency-graph
  • /api/linked-issues
  • /api/search-commits
  • /api/question-answering

A exploiter

  1. Responsabilités très bien segmentées
  • Compréhensible (bien que le fichier soit très long)
  • Maintenable
  • Modulable
  1. Données d'analyse persistantes
  • Pas besoin de relancer l'analyse au chargement de la page
  • Possibilité de réaliser les différentes parties de l'analyse en temps voulu
  1. Perspectives d'extension
  • L'embedding n'est réaliser que sur les informations des commits et non le code
  • Ne récupère pas l'intégralité des données disponibles à propos d'un dépôt

Limites

  1. Utilisation de l'API Github
  • Nombre de token limité comparé au volume à analyser dans ce contexte
  • Pouvoir travailler dans le train
  1. Analyse uniquement la branche par défaut
  • Ne reflète pas forcément les efforts fournis par les étudiants
  • Ne reflète pas forcément l'utilisation (souvent novice) des étudiants
  • Masque les aspects collaboratifs et les tentatives avortées
  1. Redondance
  • A cause du découplage des responsabilités
  • Certaines données peuvent découler d'autres au lieu d'aller chercher de nouveau dans le dépôt

Modifications

  1. Mode local
const branches = await octokit.repos.listBranches({ owner, repo }); //Octokit l'API de Github

Devient

const repoPath = `/app/clones/${repo}`;
const { stdout: branchListStdout } = await execPromise(`git -C ${repoPath} branch -r`); //Commande Git native  
const branches = branchListStdout
   .split('\n')
   .map(b => b.trim())
   .filter(b => b && !b.includes('HEAD')) 
  1. Explorer toutes les branches ET éviter la redondance
let totalCommits = 0;
const { stdout: commitsStdout } = await execPromise( //Branche par défaut
 `git -C ${repoPath} log ${branch} --since="${sinceDate}" --until="${untilDate}" --pretty=format:"%H|%aI|%ae"`
);
const lines = commitsStdout.split('\n').filter(line => line.length > 0);

for (const line of lines) { //Pour tous les commits
  const [hash, dateStr, email] = line.split('|');
  totalCommits += 1;

Devient

let totalCommits = 0;
const seenCommits = new Set(); //Pour ne pas explorer plusieurs fois le même commit

for (const branch of branches) { //Pour toutes les branches 
  const { stdout: commitsStdout } = await execPromise(
     `git -C ${repoPath} log ${branch} --since="${sinceDate}" --until="${untilDate}" --pretty=format:"%H|%aI|%ae"`
   );
   const lines = commitsStdout.split('\n').filter(line => line.length > 0);

   for (const line of lines) { //Pour tous les commits de toutes les branches
      const [hash, dateStr, email] = line.split('|');
      if (seenCommits.has(hash)) continue;
      seenCommits.add(hash);  //On stocke le hashcode du commit 
      totalCommits += 1;
   }
}
  1. Ajout de fetchBlameAllBranches et fetchProcessCommits

Nativement, code-archeologist procède à une analyse (POST /api/analyze) pour réunir un premier lot d'informations sur le dépôt et demande une "confirmation" sous la forme d'un second envoi de requête avant de procéder aux plus lourdes opérations sur les commits (POST /api/process/commits). C'est suite à cette réponse que l'embedding des commits est réalisé (fonctionnalité non implémentée dans notre application)

La méthode fetchProcessCommits passe outre cette mécanique et est appelée dans POST /api/analyze.

Pour récupérer le nombre de lignes écrites par chaque contributeur présentent dans le code au cours du temps à partir d'une liste de commits :

//On regarde pour chaque branche
//Pour chaque jour où il y a eu au moins un commit
const { stdout: shaOut } = await execPromise(
   `git -C ${repoPath} rev-list -1 --before="${dateStr} 23:59:59" ${branch}` //Extrait un seul commit par jour
);

await execPromise(`git -C ${repoPath} checkout -f ${commitSha}`); //Retrouver l'état du dossier de fichier au moment de ce commit
const { stdout: fileList } = await execPromise(`git -C ${repoPath} ls-files`); //En extraire les fichiers

   //Pour chaque fichier
   if (/\.(svg|png|jpg|jpeg|gif|ico|pdf|exe|bin|sql)$/i.test(file)) { //Filtre sur certaines extensions
      continue; 
   }
   const { stdout: blameOutput } = await execPromise(
      `git -C ${repoPath} blame --line-porcelain ${file}` //Récupérer les informations sur chaque ligne présente dans le fichier (dont son auteur)
   );


Jeu de données

  1. POST /api/analyze : faire l'analyse (retour : response)
  2. GET /api/analyze/analysisId=response.analysisId
    "result": {
        "status": "success",
        "data": {
            "id": "1",
            "repo_url": "https://github.com/uha-fr/archiweb_2025_projets_gr02",
            "status": "completed",
            "created_at": "2025-06-09T06:44:36.428Z",
            "codeEvolution": [
                {
                    "sha": "c8f63d59dff374fa6cbbf25c86bd8f1b6d145a74",
                    "stats": {
                        "additions": 151,
                        "deletions": 75
                    },
                    "author": {
                        "date": "Sun Apr 20 16:32:49 2025 +0200",
                        "name": "sheraDev",
                        "email": "[email protected]"
                    },
                    "message": "Merge branch 'main' of https://github.com/uha-fr/archiweb_2025_projets_gr02",
                    "parents": [
                        "0549c3b80d5a9b4044df82ecbfa0b339ba766b0c",
                        "b14c6fc46bc9241bc509db4863840b91fc0b77f7"
                    ]
                },
                {
                    "sha": "4b7ae19c64c36f6dc5273f4fb462e869fb96ac32",
                    "stats": {
                        "additions": 0,
                        "deletions": 0
                    },
                    "author": {
                        "date": "Sat Feb 22 12:45:11 2025 +0100",
                        "name": "sheraDev",
                        "email": "[email protected]"
                    },
                    "message": "first commit",
                    "parents": []
                }
            ],
            "file_changes": {
                "artisan": {
                    "contributors": {
                        "[email protected]": 1
                    },
                    "totalChanges": 1
                },
                "README.md": {
                    "contributors": {
                        "[email protected]": 1,
                        "[email protected]": 2
                    },
                    "totalChanges": 3
                },
                "public/css/app.css": {
                    "contributors": {
                        "[email protected]": 1,
                        "[email protected]": 1
                    },
                    "totalChanges": 2
                },
                "app/Models/User.php": {
                    "contributors": {
                        "[email protected]": 2,
                        "[email protected]": 3,
                        "[email protected]": 1
                    },
                    "totalChanges": 6
                },
                "database/migrations/2025_04_12_225649_add_phone_address_company_fields_to_users_table.php": {
                    "contributors": {
                        "[email protected]": 1
                    },
                    "totalChanges": 1
                }
            },
            "commit_activity": {
                "2025-02-22": {
                    "[email protected]": 1,
                    "[email protected]": 3
                },
                
                "2025-04-20": {
                    "[email protected]": 2
                }
            },
            "blame_by_day": {
                "2025-02-22": {
                    "NFSTOURE": 10403,
                    "sheraDev": 1
                },
                "2025-04-09": {
                    "NFSTOURE": 10271,
                    "sheraDev": 1,
                    "Abdou Samatte Diop": 12972
                },
                "2025-04-11": {
                    "NFSTOURE": 10119,
                    "sheraDev": 291,
                    "Abdou Samatte Diop": 13410
                },
                "2025-04-20": {
                    "NFSTOURE": 10302,
                    "sheraDev": 677,
                    "Abdou Samatte Diop": 13648
                }
            },
            "contributors": [
                {
                    "id": 147320827,
                    "url": "https://api.github.com/users/sheraDev",
                    "type": "User",
                    "login": "sheraDev",
                    "node_id": "U_kgDOCMfv-w",
                    "html_url": "https://github.com/sheraDev",
                    "gists_url": "https://api.github.com/users/sheraDev/gists{/gist_id}",
                    "repos_url": "https://api.github.com/users/sheraDev/repos",
                    "avatar_url": "https://avatars.githubusercontent.com/u/147320827?v=4",
                    "events_url": "https://api.github.com/users/sheraDev/events{/privacy}",
                    "site_admin": false,
                    "gravatar_id": "",
                    "starred_url": "https://api.github.com/users/sheraDev/starred{/owner}{/repo}",
                    "contributions": 15,
                    "followers_url": "https://api.github.com/users/sheraDev/followers",
                    "following_url": "https://api.github.com/users/sheraDev/following{/other_user}",
                    "user_view_type": "public",
                    "organizations_url": "https://api.github.com/users/sheraDev/orgs",
                    "subscriptions_url": "https://api.github.com/users/sheraDev/subscriptions",
                    "received_events_url": "https://api.github.com/users/sheraDev/received_events"
                }     
            ],
            "dependencies": {
                "axios": "^0.21",
                "lodash": "^4.17.19",
                "@tailwindcss/forms": "^0.5.10"
            },
            "issues": []
        }
    }
}