Skip to content

Commit 85ca1fd

Browse files
authored
merge: merge pull request #17 from n0rfas/dev - release 0.1.14
2 parents ef3fffc + 691f0d9 commit 85ca1fd

File tree

9 files changed

+145
-18
lines changed

9 files changed

+145
-18
lines changed

git_analytics/analyzers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .authors_statistics import AuthorsStatisticsAnalyzer
2+
from .bus_factor_post import bus_factor_post
23
from .commit_type import CommitTypeAnalyzer
34
from .commits_summary import CommitsSummaryAnalyzer
45
from .historical_statistics import HistoricalStatisticsAnalyzer
@@ -7,6 +8,7 @@
78

89
__all__ = [
910
"AuthorsStatisticsAnalyzer",
11+
"bus_factor_post",
1012
"CommitTypeAnalyzer",
1113
"CommitsSummaryAnalyzer",
1214
"HistoricalStatisticsAnalyzer",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Any, Dict
2+
3+
4+
def _bus_factor(contributions: Dict[str, int], threshold: float = 0.5) -> int:
5+
total = sum(contributions.values())
6+
acc = 0
7+
for _, lines in sorted(contributions.items(), key=lambda x: x[1], reverse=True):
8+
acc += lines
9+
if acc / total >= threshold:
10+
return len([a for a in contributions if contributions[a] >= lines])
11+
return 1
12+
13+
14+
def bus_factor_post(data: Dict[str, Any]) -> int:
15+
author_lines = dict(data.get("authors_statistics", {})).get("authors")
16+
17+
if author_lines:
18+
author_data = {}
19+
for author, item in author_lines.items():
20+
author_data[author] = item.get("insertions", 0) + item.get("deletions", 0)
21+
22+
if len(author_data) < 2:
23+
return 1
24+
25+
return _bus_factor(author_data)
26+
27+
return 1

git_analytics/engine.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
1+
import os
12
from datetime import date, timezone
2-
from typing import Any, Dict, Optional
3+
from pathlib import Path
4+
from typing import Any, Dict, Optional, Tuple
35

6+
from git_analytics.analyzers import bus_factor_post
47
from git_analytics.entities import AnalyticsResult
58
from git_analytics.interfaces import CommitSource
69

710

11+
class FileAnalyticsEngine:
12+
def __init__(self, repo_path: str = "."):
13+
self.repo_path = repo_path
14+
15+
def run(self) -> Dict[str, Tuple[int, int]]:
16+
stats: Dict[str, Tuple[int, int]] = {}
17+
18+
repo_path = Path(self.repo_path).resolve()
19+
20+
ignore_dirs = {
21+
".git",
22+
"__pycache__",
23+
"node_modules",
24+
".venv",
25+
"venv",
26+
"htmlcov",
27+
".pytest_cache",
28+
".mypy_cache",
29+
"dist",
30+
"build",
31+
}
32+
33+
for root, dirs, files in os.walk(repo_path):
34+
dirs[:] = [d for d in dirs if d not in ignore_dirs]
35+
36+
for file in files:
37+
file_path = Path(root) / file
38+
39+
extension = file_path.suffix if file_path.suffix else "(no extension)"
40+
41+
try:
42+
with open(file_path, "r", encoding="utf-8") as f:
43+
line_count = sum(1 for _ in f)
44+
except (UnicodeDecodeError, PermissionError, OSError):
45+
line_count = 0
46+
47+
if extension in stats:
48+
file_count, total_lines = stats[extension]
49+
stats[extension] = (file_count + 1, total_lines + line_count)
50+
else:
51+
stats[extension] = (1, line_count)
52+
53+
return dict(sorted(stats.items()))
54+
55+
856
class CommitAnalyticsEngine:
957
def __init__(
1058
self,
@@ -39,6 +87,11 @@ def run(
3987
analyzer.process(commit)
4088

4189
result = {analyzer.name: analyzer.result() for analyzer in analyzers}
90+
91+
# post-process
92+
result["post_data"] = {}
93+
result["post_data"]["bus_factor"] = bus_factor_post(result)
94+
4295
if self._additional_data:
4396
result["additional_data"] = self._additional_data
4497
return result

git_analytics/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
LanguageAnalyzer,
1212
LinesAnalyzer,
1313
)
14-
from git_analytics.engine import CommitAnalyticsEngine
14+
from git_analytics.engine import CommitAnalyticsEngine, FileAnalyticsEngine
1515
from git_analytics.sources import GitCommitSource
1616
from git_analytics.web_app import create_web_app
1717

@@ -36,10 +36,12 @@ def run():
3636
print("Error: Current directory is not a git repository.")
3737
return
3838

39+
extension_stats = FileAnalyticsEngine().run()
40+
3941
engine = CommitAnalyticsEngine(
4042
source=GitCommitSource(repo),
4143
analyzers_factory=make_analyzers,
42-
additional_data={"name_branch": name_branch},
44+
additional_data={"name_branch": name_branch, "extension_stats": extension_stats},
4345
)
4446

4547
web_app = create_web_app(engine=engine)
528 Bytes
Loading
1.06 KB
Loading

git_analytics/static/index.html

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<title>Git Analytics</title>
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
8+
<title>Git-Analytics</title>
9+
<meta name="description" content="Advanced analytics for Git repositories — commits, authors, code churn, lines of code, trends, and visual dashboards.">
10+
11+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
12+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
713
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
14+
815
<link href="css/bootstrap.min.css" rel="stylesheet">
916
<link href="css/bootstrap-icons.min.css" rel="stylesheet">
17+
18+
<script src="js/bootstrap.bundle.min.js" defer></script>
19+
<script src="js/chart.umd.min.js" defer></script>
20+
<script src="js/git-analytics.js" defer></script>
1021
</head>
1122
<body class="d-flex flex-column min-vh-100">
1223
<!-- top menu -->
@@ -79,6 +90,21 @@ <h2>Overview</h2>
7990
</div>
8091
</div>
8192
</div>
93+
<div class="col-md-2">
94+
<div class="card h-100 w-100">
95+
<div class="card-header">
96+
<span>Bus Factor</span>
97+
<i class="bi bi-info-circle"
98+
data-bs-toggle="tooltip"
99+
data-bs-placement="right"
100+
title="The bus factor is a measurement of the risk resulting from information and capabilities not being shared among team members. It indicates the minimum number of developers that have to leave a project before the project becomes incapacitated.">
101+
</i>
102+
</div>
103+
<div class="card-body">
104+
<span id="busFactor"></span>
105+
</div>
106+
</div>
107+
</div>
82108
</div>
83109
<div class="row mt-4">
84110
<div class="col-md-12">
@@ -263,7 +289,7 @@ <h2>Commits</h2>
263289
</i>
264290
</div>
265291
<div class="card-body">
266-
<canvas id="chartDay"></canvas>
292+
<canvas id="chartCommitsByHour"></canvas>
267293
</div>
268294
</div>
269295
</div>
@@ -280,7 +306,7 @@ <h2>Commits</h2>
280306
</i>
281307
</div>
282308
<div class="card-body">
283-
<canvas id="chartWeek"></canvas>
309+
<canvas id="chartCommitsByWeekday"></canvas>
284310
</div>
285311
</div>
286312
</div>
@@ -297,7 +323,7 @@ <h2>Commits</h2>
297323
</i>
298324
</div>
299325
<div class="card-body">
300-
<canvas id="chartMonth"></canvas>
326+
<canvas id="chartCommitsByDayOfMonth"></canvas>
301327
</div>
302328
</div>
303329
</div>
@@ -309,12 +335,10 @@ <h2>Commits</h2>
309335
<!-- footer -->
310336
<footer class="bg-dark text-white py-3 mt-auto">
311337
<div class="container">
312-
<span>&copy; 2025 ver 0.1.13</span>
338+
<span>&copy; 2026 ver 0.1.14</span>
313339
</div>
314340
</footer>
315-
<script src="js/bootstrap.bundle.min.js"></script>
316-
<script src="js/chart.umd.min.js"></script>
317-
<script src="js/git-analytics.js"></script>
341+
318342
<script>
319343
const views = document.querySelectorAll('.view');
320344
const nav = document.getElementById('sidebarNav');

git_analytics/static/js/git-analytics.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ async function loadAndRender(type, value, timeIntervalLabel) {
6464

6565
// render stats
6666
renderGeneralStatistics(stats, timeIntervalLabel);
67+
renderBusFactor(stats);
6768
renderWeeklyCommitTypes(stats.commit_type.commit_type_by_week);
6869

6970
renderInsDelLinesByAuthors(stats.authors_statistics.authors);
@@ -158,6 +159,24 @@ function renderGeneralStatistics(stats, rangeLabel) {
158159
`;
159160
}
160161

162+
function renderBusFactor(stats) {
163+
const busFactorEl = document.getElementById("busFactor");
164+
busFactorVal= stats.post_data.bus_factor;
165+
166+
let colorClass = "text-success";
167+
if (busFactorVal === 1) {
168+
colorClass = "text-danger";
169+
} else if (busFactorVal === 2) {
170+
colorClass = "text-warning";
171+
}
172+
173+
busFactorEl.innerHTML = `
174+
<div class="d-flex align-items-center justify-content-center h-100">
175+
<strong class="h1 ${colorClass}">${busFactorVal}</strong>
176+
</div>
177+
`;
178+
}
179+
161180
function renderChart(id, config) {
162181
if (CHARTS[id]) {
163182
CHARTS[id].destroy();
@@ -284,7 +303,7 @@ function buildHourByAuthorChart(hourOfDayData) {
284303
return Object.values(byAuthor).reduce((s, v) => s + v, 0);
285304
});
286305

287-
renderChart("chartDay", {
306+
renderChart("chartCommitsByHour", {
288307
type: "bar",
289308
data: { labels: HOUR_LABELS, datasets: [{
290309
label: "Total",
@@ -315,7 +334,7 @@ function buildHourByAuthorChart(hourOfDayData) {
315334
backgroundColor: getAuthorColor(author),
316335
}));
317336

318-
renderChart("chartDay", {
337+
renderChart("chartCommitsByHour", {
319338
type: "bar",
320339
data: { labels: HOUR_LABELS, datasets },
321340
options: {
@@ -378,7 +397,7 @@ function buildWeekByAuthorChart(dayOfWeekData) {
378397
Object.values(dayOfWeekData[d] || {}).reduce((s, v) => s + v, 0)
379398
);
380399

381-
renderChart("chartWeek", {
400+
renderChart("chartCommitsByWeekday", {
382401
type: "bar",
383402
data: {
384403
labels: WEEK_LABELS,
@@ -412,7 +431,7 @@ function buildWeekByAuthorChart(dayOfWeekData) {
412431
backgroundColor: getAuthorColor(author),
413432
}));
414433

415-
renderChart("chartWeek", {
434+
renderChart("chartCommitsByWeekday", {
416435
type: "bar",
417436
data: { labels: WEEK_LABELS, datasets },
418437
options: {
@@ -446,7 +465,7 @@ function buildDayOfMonthByAuthorChart(dayOfMonthData) {
446465
Object.values(dayOfMonthData[d] || {}).reduce((s, v) => s + v, 0)
447466
);
448467

449-
renderChart("chartMonth", {
468+
renderChart("chartCommitsByDayOfMonth", {
450469
type: "bar",
451470
data: {
452471
labels: DAY_LABELS,
@@ -480,7 +499,7 @@ function buildDayOfMonthByAuthorChart(dayOfMonthData) {
480499
backgroundColor: getAuthorColor(author),
481500
}));
482501

483-
renderChart("chartMonth", {
502+
renderChart("chartCommitsByDayOfMonth", {
484503
type: "bar",
485504
data: { labels: DAY_LABELS, datasets },
486505
options: {

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "git-analytics"
3-
version = "0.1.13"
3+
version = "0.1.14"
44
description = "Advanced analytics for Git repositories — commits, authors, code churn, lines of code, trends, and visual dashboards."
55
authors = ["n0rfas <n0rfas@protonmail.com>"]
66
license = "MIT"

0 commit comments

Comments
 (0)