Skip to content

Commit 691f0d9

Browse files
authored
Merge pull request #16 from n0rfas/release-0_1_14
Release 0.1.14
2 parents c355708 + c38dd39 commit 691f0d9

File tree

6 files changed

+122
-4
lines changed

6 files changed

+122
-4
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)

git_analytics/static/index.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ <h2>Overview</h2>
9090
</div>
9191
</div>
9292
</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>
93108
</div>
94109
<div class="row mt-4">
95110
<div class="col-md-12">
@@ -320,7 +335,7 @@ <h2>Commits</h2>
320335
<!-- footer -->
321336
<footer class="bg-dark text-white py-3 mt-auto">
322337
<div class="container">
323-
<span>&copy; 2025 ver 0.1.14</span>
338+
<span>&copy; 2026 ver 0.1.14</span>
324339
</div>
325340
</footer>
326341

git_analytics/static/js/git-analytics.js

Lines changed: 19 additions & 0 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();

0 commit comments

Comments
 (0)