K. Code Archeologist ‐ Augmentation de l'API - uha-fr/endyear_2025_gr11_back GitHub Wiki
Prérequis
- Langage : Node.js
- Base de données : PostgreSQL
- Extensions de PostgreSQL :
- pgvector
- pgvectorscale
- pgai
- 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 :
-
Lancement de l'analyse centralisée - route : POST /api/analyze
-
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.
- 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
- Responsabilités très bien segmentées
- Compréhensible (bien que le fichier soit très long)
- Maintenable
- Modulable
- 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
- 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
- Utilisation de l'API Github
- Nombre de token limité comparé au volume à analyser dans ce contexte
- Pouvoir travailler dans le train
- 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
- 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
- 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'))
- 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;
}
}
- 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
- POST /api/analyze : faire l'analyse (retour : response)
- 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": []
}
}
}