git2report 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git2report/__init__.py +2 -0
- git2report/__main__.py +4 -0
- git2report/branches.py +34 -0
- git2report/cache.py +78 -0
- git2report/charts.py +155 -0
- git2report/chunker.py +47 -0
- git2report/cli.py +173 -0
- git2report/clustering.py +56 -0
- git2report/config.py +28 -0
- git2report/eval_tools.py +123 -0
- git2report/extractor.py +37 -0
- git2report/filtering.py +34 -0
- git2report/metrics.py +111 -0
- git2report/models.py +45 -0
- git2report/pipeline.py +188 -0
- git2report/report.py +884 -0
- git2report/rules.py +71 -0
- git2report/summarizer.py +359 -0
- git2report/timeutil.py +19 -0
- git2report/web.py +1207 -0
- git2report-0.1.0.dist-info/METADATA +142 -0
- git2report-0.1.0.dist-info/RECORD +26 -0
- git2report-0.1.0.dist-info/WHEEL +5 -0
- git2report-0.1.0.dist-info/entry_points.txt +4 -0
- git2report-0.1.0.dist-info/licenses/LICENSE +21 -0
- git2report-0.1.0.dist-info/top_level.txt +1 -0
git2report/report.py
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
"""STEP 7: combine sentences + numbers + commit IDs into Markdown, PDF, and interactive HTML."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from jinja2 import Template
|
|
11
|
+
from reportlab.lib import colors
|
|
12
|
+
from reportlab.lib.pagesizes import A4
|
|
13
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
14
|
+
from reportlab.lib.units import cm
|
|
15
|
+
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
|
16
|
+
|
|
17
|
+
from git2report.models import BranchInfo
|
|
18
|
+
|
|
19
|
+
MARKDOWN = Template("""\
|
|
20
|
+
# Weekly Progress Report
|
|
21
|
+
|
|
22
|
+
**Project:** {{ d.project }} | **Period:** {{ d.start }} to {{ d.end }} | **Audience:** {{ d.audience }}
|
|
23
|
+
{% if d.executive_summary %}
|
|
24
|
+
> **Executive Summary (TL;DR):** {{ d.executive_summary }}
|
|
25
|
+
{% endif %}
|
|
26
|
+
**Commits:** {{ d.metrics.commits }} | **Lines changed:** {{ d.metrics.lines_changed }} | \
|
|
27
|
+
**Most active:** {{ d.metrics.top_module }} | **Test-to-code ratio:** {{ d.metrics.test_to_code_ratio }}
|
|
28
|
+
|
|
29
|
+
**At a glance:** {{ d.glance_text }}
|
|
30
|
+
{% if d.metrics.tech_stack and d.metrics.tech_stack|length > 0 %}
|
|
31
|
+
### 💻 Tech Stack Breakdown
|
|
32
|
+
{% for item in d.metrics.tech_stack %}- **{{ item.tech }}**: {{ item.percentage }}%
|
|
33
|
+
{% endfor %}{% endif %}
|
|
34
|
+
{% if d.metrics.hotspots and d.metrics.hotspots|length > 0 %}
|
|
35
|
+
### 🔥 Codebase Hotspots
|
|
36
|
+
| File | Commits | Lines Modified |
|
|
37
|
+
|:---|:---:|:---:|
|
|
38
|
+
{% for h in d.metrics.hotspots %}| `{{ h.path }}` | {{ h.commits }} | {{ h.lines }} |
|
|
39
|
+
{% endfor %}{% endif %}
|
|
40
|
+
{% if d.metrics.contributors and d.metrics.contributors|length > 0 %}
|
|
41
|
+
### 👥 Contributor Leaderboard
|
|
42
|
+
| Contributor | Commits | Lines (+ / -) | Primary Focus |
|
|
43
|
+
|:---|:---:|:---:|:---|
|
|
44
|
+
{% for c in d.metrics.contributors %}| {{ c.name }} | {{ c.commits }} | +{{ c.added }} / -{{ c.deleted }} | {{ c.top_module }} |
|
|
45
|
+
{% endfor %}{% endif %}
|
|
46
|
+
{% for s in d.sections %}
|
|
47
|
+
## {{ s.module }}
|
|
48
|
+
{{ s.text }}
|
|
49
|
+
_Evidence: {{ s.hashes | join(', ') }}_
|
|
50
|
+
{% if s.note %}_Note: {{ s.note }}_
|
|
51
|
+
{% endif %}{% if s.commits and s.commits|length > 0 %}
|
|
52
|
+
| Type | Commit | Author | Lines |
|
|
53
|
+
|:---|:---|:---|:---:|
|
|
54
|
+
{% for cm in s.commits %}| `{{ cm.type | upper }}` | {% if d.remote_url %}[`{{ cm.hash }}`]({{ d.remote_url }}/commit/{{ cm.hash }}){% else %}`{{ cm.hash }}`{% endif %} {{ cm.subject }} | {{ cm.author }} | +{{ cm.added }}/-{{ cm.deleted }} |
|
|
55
|
+
{% endfor %}{% endif %}{% endfor %}{% if d.in_progress %}
|
|
56
|
+
## In progress (open branches)
|
|
57
|
+
{% for b in d.in_progress %}- `{{ b.name }}`: {{ b.ahead }} commit(s) not merged yet, last activity {{ b.last_date }} ("{{ b.subject }}")
|
|
58
|
+
{% endfor %}{% endif %}{% if d.planned %}
|
|
59
|
+
## Planned goals (written by the developer)
|
|
60
|
+
{% for p in d.planned %}- {{ p }}
|
|
61
|
+
{% endfor %}{% endif %}
|
|
62
|
+
---
|
|
63
|
+
_Generated by Git2Report on {{ d.generated }}. Summaries: {{ d.mode_label }}._
|
|
64
|
+
""", trim_blocks=False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CategorizedCommit:
|
|
69
|
+
hash: str
|
|
70
|
+
subject: str
|
|
71
|
+
author: str
|
|
72
|
+
date: str
|
|
73
|
+
type: str # feat, fix, perf, refactor, docs, chore, etc.
|
|
74
|
+
added: int = 0
|
|
75
|
+
deleted: int = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Section:
|
|
80
|
+
module: str
|
|
81
|
+
text: str
|
|
82
|
+
hashes: list[str]
|
|
83
|
+
mode: str = "offline"
|
|
84
|
+
note: str = ""
|
|
85
|
+
commits: list[CategorizedCommit] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class ReportData:
|
|
90
|
+
project: str
|
|
91
|
+
start: str
|
|
92
|
+
end: str
|
|
93
|
+
audience: str
|
|
94
|
+
metrics: dict
|
|
95
|
+
sections: list[Section] = field(default_factory=list)
|
|
96
|
+
mode_label: str = ""
|
|
97
|
+
glance_text: str = ""
|
|
98
|
+
in_progress: list[BranchInfo] = field(default_factory=list)
|
|
99
|
+
planned: list[str] = field(default_factory=list)
|
|
100
|
+
generated: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
|
|
101
|
+
remote_url: str | None = None
|
|
102
|
+
executive_summary: str = ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def render_markdown(data: ReportData) -> str:
|
|
106
|
+
return MARKDOWN.render(d=data).rstrip() + "\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _esc(t: str) -> str:
|
|
110
|
+
return t.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def render_pdf(data: ReportData, path: Path) -> None:
|
|
114
|
+
ss = getSampleStyleSheet()
|
|
115
|
+
navy = colors.HexColor("#0F172A")
|
|
116
|
+
blue = colors.HexColor("#2563EB")
|
|
117
|
+
slate = colors.HexColor("#334155")
|
|
118
|
+
gray_bg = colors.HexColor("#F8FAFC")
|
|
119
|
+
gray_border = colors.HexColor("#CBD5E1")
|
|
120
|
+
|
|
121
|
+
title = ParagraphStyle("t", parent=ss["Title"], fontName="Helvetica-Bold", fontSize=18, leading=22, textColor=navy, alignment=0, spaceAfter=4)
|
|
122
|
+
meta = ParagraphStyle("m", parent=ss["Normal"], fontSize=8.5, leading=11, textColor=colors.HexColor("#64748B"), spaceAfter=8)
|
|
123
|
+
h = ParagraphStyle("h", parent=ss["Heading2"], fontName="Helvetica-Bold", fontSize=11, leading=14, textColor=navy, spaceBefore=8, spaceAfter=3)
|
|
124
|
+
body = ParagraphStyle("b", parent=ss["Normal"], fontSize=9, leading=13, textColor=slate)
|
|
125
|
+
ev = ParagraphStyle("e", parent=ss["Normal"], fontName="Courier", fontSize=7.5, leading=10, textColor=colors.HexColor("#64748B"), spaceAfter=2)
|
|
126
|
+
note_style = ParagraphStyle("n", parent=ss["Normal"], fontSize=8, leading=11, textColor=colors.HexColor("#B45309"), spaceAfter=2)
|
|
127
|
+
|
|
128
|
+
cell = ParagraphStyle("c", parent=ss["Normal"], fontSize=8, leading=10.5, textColor=slate)
|
|
129
|
+
cell_bold = ParagraphStyle("cb", parent=cell, fontName="Helvetica-Bold", fontSize=10.5, leading=12.5, textColor=navy)
|
|
130
|
+
cellh = ParagraphStyle("ch", parent=cell, fontName="Helvetica-Bold", fontSize=8, leading=10.5, textColor=colors.white)
|
|
131
|
+
|
|
132
|
+
m = data.metrics
|
|
133
|
+
lines_add = m.get("lines_added", 0)
|
|
134
|
+
lines_del = m.get("lines_deleted", 0)
|
|
135
|
+
|
|
136
|
+
# 1. Header & Meta
|
|
137
|
+
story = [
|
|
138
|
+
Paragraph("Weekly Progress Report", title),
|
|
139
|
+
Paragraph(
|
|
140
|
+
f"Project: <b>{_esc(data.project)}</b> | Period: {data.start} to {data.end} | Audience: {data.audience.capitalize()}",
|
|
141
|
+
meta
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# 2. Executive Summary (TL;DR)
|
|
146
|
+
if getattr(data, "executive_summary", ""):
|
|
147
|
+
exec_title = ParagraphStyle("et", parent=ss["Normal"], fontName="Helvetica-Bold", fontSize=8.5, leading=11, textColor=colors.HexColor("#1D4ED8"), spaceAfter=2)
|
|
148
|
+
exec_body = ParagraphStyle("eb", parent=ss["Normal"], fontSize=8.5, leading=12.5, textColor=colors.HexColor("#1E293B"))
|
|
149
|
+
exec_box = Table([
|
|
150
|
+
[Paragraph("EXECUTIVE SUMMARY (TL;DR)", exec_title)],
|
|
151
|
+
[Paragraph(_esc(data.executive_summary), exec_body)],
|
|
152
|
+
], colWidths=[17.4*cm])
|
|
153
|
+
exec_box.setStyle(TableStyle([
|
|
154
|
+
("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#F8FAFC")),
|
|
155
|
+
("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#E2E8F0")),
|
|
156
|
+
("LINEBEFORE", (0, 0), (0, -1), 3.0, blue),
|
|
157
|
+
("TOPPADDING", (0, 0), (-1, -1), 4),
|
|
158
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
|
159
|
+
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
160
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
|
161
|
+
]))
|
|
162
|
+
story += [exec_box, Spacer(1, 4)]
|
|
163
|
+
|
|
164
|
+
# 3. KPI Grid (4 Cards)
|
|
165
|
+
kpi_rows = [
|
|
166
|
+
[
|
|
167
|
+
Paragraph("COMMITS", cell),
|
|
168
|
+
Paragraph("NET CODE CHURN", cell),
|
|
169
|
+
Paragraph("PRIMARY IMPACT", cell),
|
|
170
|
+
Paragraph("TEST RATIO", cell),
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
Paragraph(f"<b>{m['commits']}</b> <font size=7 color='#64748B'>(of {m['commits_seen_before_filter']})</font>", cell_bold),
|
|
174
|
+
Paragraph(f"<b>{m['lines_changed']}</b> <font size=7 color='#059669'>+{lines_add}</font> <font size=7 color='#DC2626'>-{lines_del}</font>", cell_bold),
|
|
175
|
+
Paragraph(f"<b>{_esc(str(m['top_module']))}</b>", cell_bold),
|
|
176
|
+
Paragraph(f"<b>{m['test_to_code_ratio']}</b>", cell_bold),
|
|
177
|
+
],
|
|
178
|
+
]
|
|
179
|
+
tbl = Table(kpi_rows, colWidths=[4.35*cm, 4.45*cm, 4.4*cm, 4.2*cm])
|
|
180
|
+
tbl.setStyle(TableStyle([
|
|
181
|
+
("BACKGROUND", (0, 0), (-1, -1), gray_bg),
|
|
182
|
+
("BOX", (0, 0), (-1, -1), 0.5, gray_border),
|
|
183
|
+
("INNERGRID", (0, 0), (-1, -1), 0.5, gray_border),
|
|
184
|
+
("TOPPADDING", (0, 0), (-1, -1), 5),
|
|
185
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
|
186
|
+
("LEFTPADDING", (0, 0), (-1, -1), 7),
|
|
187
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 7),
|
|
188
|
+
]))
|
|
189
|
+
story += [tbl, Spacer(1, 4)]
|
|
190
|
+
|
|
191
|
+
# 4. At-a-glance banner
|
|
192
|
+
glance_box = Table([[Paragraph(f"<b>At a glance:</b> {_esc(data.glance_text)}", body)]], colWidths=[17.4*cm])
|
|
193
|
+
glance_box.setStyle(TableStyle([
|
|
194
|
+
("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#EFF6FF")),
|
|
195
|
+
("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#BFDBFE")),
|
|
196
|
+
("TOPPADDING", (0, 0), (-1, -1), 4),
|
|
197
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
|
198
|
+
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
199
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
|
200
|
+
]))
|
|
201
|
+
story += [glance_box, Spacer(1, 4)]
|
|
202
|
+
|
|
203
|
+
# 4b. Visual Analytics Chart: Commit Type Distribution
|
|
204
|
+
commits_by_type = m.get("commits_by_type", {})
|
|
205
|
+
if commits_by_type:
|
|
206
|
+
from git2report.charts import build_type_breakdown_bar
|
|
207
|
+
type_chart = build_type_breakdown_bar(commits_by_type, width=490, height=30)
|
|
208
|
+
story += [
|
|
209
|
+
Paragraph("Commit Type Distribution", h),
|
|
210
|
+
type_chart,
|
|
211
|
+
Spacer(1, 3)
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
# 4c. Visual Analytics: Module Code Churn Chart (+Lines Added / -Lines Deleted)
|
|
215
|
+
if data.sections:
|
|
216
|
+
from git2report.charts import build_module_churn_chart
|
|
217
|
+
churn_chart = build_module_churn_chart(data.sections, width=490, height=60)
|
|
218
|
+
story += [
|
|
219
|
+
Paragraph("Code Churn by Module (+Lines Added / -Lines Deleted)", h),
|
|
220
|
+
churn_chart,
|
|
221
|
+
Spacer(1, 4)
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
# 5. Two-column Analytics: Tech Stack + Codebase Hotspots
|
|
225
|
+
tech_stack = m.get("tech_stack", [])
|
|
226
|
+
hotspots = m.get("hotspots", [])
|
|
227
|
+
if tech_stack or hotspots:
|
|
228
|
+
# Tech stack table
|
|
229
|
+
ts_rows = [[Paragraph("Technology", cellh), Paragraph("Share", cellh)]]
|
|
230
|
+
if isinstance(tech_stack, list):
|
|
231
|
+
for item in tech_stack[:5]:
|
|
232
|
+
t_name = item.get("tech", "Other") if isinstance(item, dict) else str(item)
|
|
233
|
+
t_pct = item.get("percentage", 0) if isinstance(item, dict) else 0
|
|
234
|
+
ts_rows.append([
|
|
235
|
+
Paragraph(_esc(str(t_name)), cell),
|
|
236
|
+
Paragraph(f"<b>{t_pct}%</b>", cell),
|
|
237
|
+
])
|
|
238
|
+
elif isinstance(tech_stack, dict):
|
|
239
|
+
for tech, pct in list(tech_stack.items())[:5]:
|
|
240
|
+
ts_rows.append([
|
|
241
|
+
Paragraph(_esc(str(tech)), cell),
|
|
242
|
+
Paragraph(f"<b>{pct}%</b>", cell),
|
|
243
|
+
])
|
|
244
|
+
if len(ts_rows) == 1:
|
|
245
|
+
ts_rows.append([Paragraph("Not detected", cell), Paragraph("-", cell)])
|
|
246
|
+
ts_tbl = Table(ts_rows, colWidths=[5.4*cm, 2.6*cm])
|
|
247
|
+
ts_tbl.setStyle(TableStyle([
|
|
248
|
+
("BACKGROUND", (0, 0), (-1, 0), navy),
|
|
249
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, gray_bg]),
|
|
250
|
+
("GRID", (0, 0), (-1, -1), 0.5, gray_border),
|
|
251
|
+
("TOPPADDING", (0, 0), (-1, -1), 2.5),
|
|
252
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 2.5),
|
|
253
|
+
("LEFTPADDING", (0, 0), (-1, -1), 5),
|
|
254
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
|
255
|
+
]))
|
|
256
|
+
|
|
257
|
+
# Hotspots table
|
|
258
|
+
hs_rows = [[Paragraph("Active File", cellh), Paragraph("Commits", cellh), Paragraph("Lines", cellh)]]
|
|
259
|
+
if isinstance(hotspots, list):
|
|
260
|
+
for hs in hotspots[:5]:
|
|
261
|
+
if isinstance(hs, dict):
|
|
262
|
+
fname = hs.get("path") or hs.get("file", "")
|
|
263
|
+
n_commits = hs.get("commits", 0)
|
|
264
|
+
n_lines = hs.get("lines", 0)
|
|
265
|
+
else:
|
|
266
|
+
fname = str(hs)
|
|
267
|
+
n_commits = 0
|
|
268
|
+
n_lines = 0
|
|
269
|
+
if len(fname) > 26:
|
|
270
|
+
fname = "..." + fname[-23:]
|
|
271
|
+
hs_rows.append([
|
|
272
|
+
Paragraph(f"<font face='Courier' size=7>{_esc(fname)}</font>", cell),
|
|
273
|
+
Paragraph(str(n_commits), cell),
|
|
274
|
+
Paragraph(str(n_lines), cell),
|
|
275
|
+
])
|
|
276
|
+
if len(hs_rows) == 1:
|
|
277
|
+
hs_rows.append([Paragraph("None", cell), Paragraph("-", cell), Paragraph("-", cell)])
|
|
278
|
+
hs_tbl = Table(hs_rows, colWidths=[5.4*cm, 1.8*cm, 1.8*cm])
|
|
279
|
+
hs_tbl.setStyle(TableStyle([
|
|
280
|
+
("BACKGROUND", (0, 0), (-1, 0), navy),
|
|
281
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, gray_bg]),
|
|
282
|
+
("GRID", (0, 0), (-1, -1), 0.5, gray_border),
|
|
283
|
+
("TOPPADDING", (0, 0), (-1, -1), 2.5),
|
|
284
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 2.5),
|
|
285
|
+
("LEFTPADDING", (0, 0), (-1, -1), 5),
|
|
286
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
|
287
|
+
]))
|
|
288
|
+
|
|
289
|
+
analytics_tbl = Table([
|
|
290
|
+
[Paragraph("Tech Stack Breakdown", h), Paragraph("Codebase Hotspots", h)],
|
|
291
|
+
[ts_tbl, hs_tbl]
|
|
292
|
+
], colWidths=[8.4*cm, 9.0*cm])
|
|
293
|
+
analytics_tbl.setStyle(TableStyle([
|
|
294
|
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
295
|
+
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
|
296
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
|
297
|
+
("TOPPADDING", (0, 0), (-1, -1), 1),
|
|
298
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
|
|
299
|
+
]))
|
|
300
|
+
story += [analytics_tbl, Spacer(1, 4)]
|
|
301
|
+
|
|
302
|
+
# 6. Team Contributor Leaderboard
|
|
303
|
+
contributors = m.get("contributors", [])
|
|
304
|
+
if contributors and len(contributors) > 0:
|
|
305
|
+
c_rows = [[
|
|
306
|
+
Paragraph("Contributor", cellh),
|
|
307
|
+
Paragraph("Commits", cellh),
|
|
308
|
+
Paragraph("Lines (+ / -)", cellh),
|
|
309
|
+
Paragraph("Primary Focus", cellh),
|
|
310
|
+
]]
|
|
311
|
+
for c_ in contributors[:6]:
|
|
312
|
+
c_rows.append([
|
|
313
|
+
Paragraph(_esc(c_["name"]), cell),
|
|
314
|
+
Paragraph(str(c_["commits"]), cell),
|
|
315
|
+
Paragraph(f"<font color='#059669'>+{c_['added']}</font> / <font color='#DC2626'>-{c_['deleted']}</font>", cell),
|
|
316
|
+
Paragraph(_esc(c_["top_module"]), cell),
|
|
317
|
+
])
|
|
318
|
+
c_tbl = Table(c_rows, colWidths=[5.2*cm, 2.6*cm, 4.6*cm, 5.0*cm])
|
|
319
|
+
c_tbl.setStyle(TableStyle([
|
|
320
|
+
("BACKGROUND", (0, 0), (-1, 0), navy),
|
|
321
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, gray_bg]),
|
|
322
|
+
("GRID", (0, 0), (-1, -1), 0.5, gray_border),
|
|
323
|
+
("TOPPADDING", (0, 0), (-1, -1), 3),
|
|
324
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
|
325
|
+
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
|
326
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
|
327
|
+
]))
|
|
328
|
+
story += [Paragraph("Team Contributors", h), c_tbl, Spacer(1, 4)]
|
|
329
|
+
|
|
330
|
+
bullet = ParagraphStyle("bl", parent=body, leftIndent=12, bulletIndent=2, fontSize=8.5, leading=12)
|
|
331
|
+
|
|
332
|
+
# 7. Module Deliverables & Categorized Badges
|
|
333
|
+
story.append(Paragraph("Module Deliverables", h))
|
|
334
|
+
for s in data.sections:
|
|
335
|
+
story.append(Paragraph(f"<b>{_esc(s.module)}</b>", h))
|
|
336
|
+
s_box = Table([[Paragraph(_esc(s.text), body)]], colWidths=[17.4*cm])
|
|
337
|
+
s_box.setStyle(TableStyle([
|
|
338
|
+
("BACKGROUND", (0, 0), (-1, -1), gray_bg),
|
|
339
|
+
("BOX", (0, 0), (-1, -1), 0.5, gray_border),
|
|
340
|
+
("LINEBEFORE", (0, 0), (0, -1), 2.5, blue),
|
|
341
|
+
("TOPPADDING", (0, 0), (-1, -1), 4),
|
|
342
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
|
343
|
+
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
344
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
|
345
|
+
]))
|
|
346
|
+
story.append(s_box)
|
|
347
|
+
|
|
348
|
+
# Categorized commits with distinct badge colors
|
|
349
|
+
if s.commits:
|
|
350
|
+
badge_colors = {
|
|
351
|
+
"FEAT": "#059669",
|
|
352
|
+
"FIX": "#DC2626",
|
|
353
|
+
"PERF": "#7C3AED",
|
|
354
|
+
"REFACTOR": "#D97706",
|
|
355
|
+
"DOCS": "#0284C7",
|
|
356
|
+
"STYLE": "#64748B",
|
|
357
|
+
"TEST": "#0D9488",
|
|
358
|
+
"CHORE": "#64748B",
|
|
359
|
+
}
|
|
360
|
+
commit_items = []
|
|
361
|
+
for cm_ in s.commits[:5]:
|
|
362
|
+
t_label = cm_.type.upper() if cm_.type else "OTHER"
|
|
363
|
+
b_color = badge_colors.get(t_label, "#2563EB")
|
|
364
|
+
commit_items.append(
|
|
365
|
+
Paragraph(
|
|
366
|
+
f"<font color='{b_color}'><b>[{t_label}]</b></font> {_esc(cm_.subject)} <font color='#64748B'>({_esc(cm_.author)}, +{cm_.added}/-{cm_.deleted})</font> <font face='Courier' color='#2563EB'>{cm_.hash}</font>",
|
|
367
|
+
bullet,
|
|
368
|
+
bulletText="\u2022"
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
story += commit_items
|
|
372
|
+
if len(s.commits) > 5:
|
|
373
|
+
story.append(Paragraph(f"<i>... and {len(s.commits) - 5} more commits</i>", bullet))
|
|
374
|
+
|
|
375
|
+
story.append(Paragraph("Evidence: " + ", ".join(s.hashes), ev))
|
|
376
|
+
if s.note:
|
|
377
|
+
story.append(Paragraph("Note: " + _esc(s.note), note_style))
|
|
378
|
+
story.append(Spacer(1, 3))
|
|
379
|
+
|
|
380
|
+
# 8. In progress branches
|
|
381
|
+
if data.in_progress:
|
|
382
|
+
story.append(Paragraph("In progress (open branches)", h))
|
|
383
|
+
for b in data.in_progress:
|
|
384
|
+
story.append(Paragraph(f"<b>{_esc(b.name)}</b>: {b.ahead} commit(s) not merged yet, last activity {b.last_date} ({_esc(b.subject)})",
|
|
385
|
+
bullet, bulletText="\u2022"))
|
|
386
|
+
|
|
387
|
+
# 9. Planned goals
|
|
388
|
+
if data.planned:
|
|
389
|
+
story.append(Paragraph("Planned goals (written by the developer)", h))
|
|
390
|
+
for g_ in data.planned:
|
|
391
|
+
story.append(Paragraph(_esc(g_), bullet, bulletText="\u2022"))
|
|
392
|
+
|
|
393
|
+
story += [Spacer(1, 8), Paragraph(f"Generated by Git2Report on {data.generated}. Summaries: {_esc(data.mode_label)}.", meta)]
|
|
394
|
+
|
|
395
|
+
def header_footer(c, doc):
|
|
396
|
+
c.saveState()
|
|
397
|
+
c.setFont("Helvetica", 7.5)
|
|
398
|
+
c.setFillColor(colors.HexColor("#64748B"))
|
|
399
|
+
# Running header (pages > 1)
|
|
400
|
+
if doc.page > 1:
|
|
401
|
+
c.drawString(1.8*cm, A4[1] - 1.2*cm, f"{data.project} — Weekly Progress Report")
|
|
402
|
+
c.drawRightString(A4[0] - 1.8*cm, A4[1] - 1.2*cm, f"Generated: {data.generated}")
|
|
403
|
+
c.setStrokeColor(gray_border)
|
|
404
|
+
c.setLineWidth(0.5)
|
|
405
|
+
c.line(1.8*cm, A4[1] - 1.35*cm, A4[0] - 1.8*cm, A4[1] - 1.35*cm)
|
|
406
|
+
|
|
407
|
+
# Running footer
|
|
408
|
+
c.drawString(1.8*cm, 1.1*cm, "Git2Report | Engineering Delivery Report")
|
|
409
|
+
c.drawRightString(A4[0] - 1.8*cm, 1.1*cm, f"Page {doc.page}")
|
|
410
|
+
c.setStrokeColor(gray_border)
|
|
411
|
+
c.setLineWidth(0.5)
|
|
412
|
+
c.line(1.8*cm, 1.3*cm, A4[0] - 1.8*cm, 1.3*cm)
|
|
413
|
+
c.restoreState()
|
|
414
|
+
|
|
415
|
+
SimpleDocTemplate(
|
|
416
|
+
str(path), pagesize=A4,
|
|
417
|
+
leftMargin=1.8*cm, rightMargin=1.8*cm,
|
|
418
|
+
topMargin=1.6*cm, bottomMargin=1.6*cm,
|
|
419
|
+
title=f"{data.project} Progress Report"
|
|
420
|
+
).build(story, onFirstPage=header_footer, onLaterPages=header_footer)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
HTML_TEMPLATE = """\
|
|
424
|
+
<!DOCTYPE html>
|
|
425
|
+
<html lang="en">
|
|
426
|
+
<head>
|
|
427
|
+
<meta charset="UTF-8">
|
|
428
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
429
|
+
<title>Progress Report - {{ d.project }}</title>
|
|
430
|
+
<style>
|
|
431
|
+
:root {
|
|
432
|
+
--bg: #0f172a;
|
|
433
|
+
--card: #1e293b;
|
|
434
|
+
--card-border: #334155;
|
|
435
|
+
--text: #f8fafc;
|
|
436
|
+
--text-muted: #94a3b8;
|
|
437
|
+
--primary: #3b82f6;
|
|
438
|
+
--primary-hover: #60a5fa;
|
|
439
|
+
--accent: #8b5cf6;
|
|
440
|
+
--success: #10b981;
|
|
441
|
+
--danger: #ef4444;
|
|
442
|
+
--warning: #f59e0b;
|
|
443
|
+
--info: #06b6d4;
|
|
444
|
+
--chip-bg: rgba(255, 255, 255, 0.06);
|
|
445
|
+
}
|
|
446
|
+
.light {
|
|
447
|
+
--bg: #f8fafc;
|
|
448
|
+
--card: #ffffff;
|
|
449
|
+
--card-border: #e2e8f0;
|
|
450
|
+
--text: #0f172a;
|
|
451
|
+
--text-muted: #64748b;
|
|
452
|
+
--chip-bg: rgba(0, 0, 0, 0.04);
|
|
453
|
+
}
|
|
454
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
455
|
+
body {
|
|
456
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
457
|
+
background-color: var(--bg);
|
|
458
|
+
color: var(--text);
|
|
459
|
+
line-height: 1.5;
|
|
460
|
+
padding: 24px 16px;
|
|
461
|
+
transition: background-color 0.2s, color 0.2s;
|
|
462
|
+
}
|
|
463
|
+
.container { max-width: 1100px; margin: 0 auto; }
|
|
464
|
+
header {
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-wrap: wrap;
|
|
467
|
+
justify-content: space-between;
|
|
468
|
+
align-items: center;
|
|
469
|
+
gap: 16px;
|
|
470
|
+
padding-bottom: 24px;
|
|
471
|
+
border-bottom: 1px solid var(--card-border);
|
|
472
|
+
margin-bottom: 24px;
|
|
473
|
+
}
|
|
474
|
+
.title-group h1 { font-size: 1.85rem; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
|
475
|
+
.badge {
|
|
476
|
+
display: inline-flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
font-size: 0.75rem;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
padding: 3px 8px;
|
|
481
|
+
border-radius: 9999px;
|
|
482
|
+
text-transform: uppercase;
|
|
483
|
+
letter-spacing: 0.05em;
|
|
484
|
+
}
|
|
485
|
+
.badge-primary { background: rgba(59, 130, 246, 0.15); color: var(--primary); }
|
|
486
|
+
.badge-success { background: rgba(16, 185, 129, 0.15); color: var(--success); }
|
|
487
|
+
.badge-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
|
|
488
|
+
.badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
|
|
489
|
+
.badge-accent { background: rgba(139, 92, 246, 0.15); color: var(--accent); }
|
|
490
|
+
.meta-text { color: var(--text-muted); font-size: 0.9rem; margin-top: 4px; }
|
|
491
|
+
.actions { display: flex; gap: 10px; align-items: center; }
|
|
492
|
+
button, .btn {
|
|
493
|
+
background: var(--card);
|
|
494
|
+
border: 1px solid var(--card-border);
|
|
495
|
+
color: var(--text);
|
|
496
|
+
padding: 8px 14px;
|
|
497
|
+
border-radius: 8px;
|
|
498
|
+
cursor: pointer;
|
|
499
|
+
font-weight: 500;
|
|
500
|
+
font-size: 0.85rem;
|
|
501
|
+
display: inline-flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
gap: 6px;
|
|
504
|
+
text-decoration: none;
|
|
505
|
+
transition: all 0.2s;
|
|
506
|
+
}
|
|
507
|
+
button:hover, .btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
508
|
+
.kpi-grid {
|
|
509
|
+
display: grid;
|
|
510
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
511
|
+
gap: 16px;
|
|
512
|
+
margin-bottom: 24px;
|
|
513
|
+
}
|
|
514
|
+
.kpi-card {
|
|
515
|
+
background: var(--card);
|
|
516
|
+
border: 1px solid var(--card-border);
|
|
517
|
+
border-radius: 12px;
|
|
518
|
+
padding: 18px;
|
|
519
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
520
|
+
}
|
|
521
|
+
.kpi-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; }
|
|
522
|
+
.kpi-value { font-size: 1.75rem; font-weight: 700; margin: 4px 0; }
|
|
523
|
+
.kpi-sub { font-size: 0.8rem; color: var(--text-muted); }
|
|
524
|
+
.glance-banner {
|
|
525
|
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
|
|
526
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
527
|
+
padding: 14px 18px;
|
|
528
|
+
border-radius: 10px;
|
|
529
|
+
margin-bottom: 24px;
|
|
530
|
+
display: flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
gap: 10px;
|
|
533
|
+
font-size: 0.95rem;
|
|
534
|
+
}
|
|
535
|
+
.progress-bar-container {
|
|
536
|
+
background: var(--card);
|
|
537
|
+
border: 1px solid var(--card-border);
|
|
538
|
+
border-radius: 12px;
|
|
539
|
+
padding: 18px;
|
|
540
|
+
margin-bottom: 24px;
|
|
541
|
+
}
|
|
542
|
+
.progress-bar {
|
|
543
|
+
display: flex;
|
|
544
|
+
height: 12px;
|
|
545
|
+
border-radius: 6px;
|
|
546
|
+
overflow: hidden;
|
|
547
|
+
margin: 12px 0 8px 0;
|
|
548
|
+
background: rgba(255, 255, 255, 0.05);
|
|
549
|
+
}
|
|
550
|
+
.progress-segment { height: 100%; transition: width 0.3s; }
|
|
551
|
+
.progress-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 0.8rem; color: var(--text-muted); }
|
|
552
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
553
|
+
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
554
|
+
|
|
555
|
+
.leaderboard {
|
|
556
|
+
background: var(--card);
|
|
557
|
+
border: 1px solid var(--card-border);
|
|
558
|
+
border-radius: 12px;
|
|
559
|
+
padding: 18px;
|
|
560
|
+
margin-bottom: 24px;
|
|
561
|
+
}
|
|
562
|
+
table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 0.9rem; }
|
|
563
|
+
th { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--card-border); color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
|
|
564
|
+
td { padding: 10px 12px; border-bottom: 1px solid var(--chip-bg); }
|
|
565
|
+
tr:last-child td { border-bottom: none; }
|
|
566
|
+
|
|
567
|
+
.module-card {
|
|
568
|
+
background: var(--card);
|
|
569
|
+
border: 1px solid var(--card-border);
|
|
570
|
+
border-radius: 12px;
|
|
571
|
+
padding: 20px;
|
|
572
|
+
margin-bottom: 20px;
|
|
573
|
+
}
|
|
574
|
+
.module-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
575
|
+
.module-title { font-size: 1.25rem; font-weight: 600; }
|
|
576
|
+
.summary-box {
|
|
577
|
+
background: var(--chip-bg);
|
|
578
|
+
border-left: 3px solid var(--primary);
|
|
579
|
+
padding: 10px 14px;
|
|
580
|
+
border-radius: 4px;
|
|
581
|
+
font-size: 0.95rem;
|
|
582
|
+
margin-bottom: 14px;
|
|
583
|
+
}
|
|
584
|
+
.commit-table { width: 100%; }
|
|
585
|
+
.commit-table td { padding: 8px 10px; font-size: 0.85rem; }
|
|
586
|
+
.commit-hash {
|
|
587
|
+
font-family: monospace;
|
|
588
|
+
font-size: 0.8rem;
|
|
589
|
+
background: var(--chip-bg);
|
|
590
|
+
padding: 2px 6px;
|
|
591
|
+
border-radius: 4px;
|
|
592
|
+
text-decoration: none;
|
|
593
|
+
color: var(--primary);
|
|
594
|
+
}
|
|
595
|
+
.commit-hash:hover { text-decoration: underline; }
|
|
596
|
+
.filter-bar {
|
|
597
|
+
display: flex;
|
|
598
|
+
flex-wrap: wrap;
|
|
599
|
+
justify-content: space-between;
|
|
600
|
+
gap: 12px;
|
|
601
|
+
margin-bottom: 20px;
|
|
602
|
+
}
|
|
603
|
+
.search-input {
|
|
604
|
+
background: var(--card);
|
|
605
|
+
border: 1px solid var(--card-border);
|
|
606
|
+
color: var(--text);
|
|
607
|
+
padding: 8px 14px;
|
|
608
|
+
border-radius: 8px;
|
|
609
|
+
font-size: 0.85rem;
|
|
610
|
+
min-width: 250px;
|
|
611
|
+
}
|
|
612
|
+
.search-input:focus { outline: none; border-color: var(--primary); }
|
|
613
|
+
.section-title { font-size: 1.2rem; font-weight: 600; margin: 24px 0 12px 0; }
|
|
614
|
+
.tag-feat { color: #10b981; }
|
|
615
|
+
.tag-fix { color: #ef4444; }
|
|
616
|
+
.tag-perf, .tag-refactor { color: #8b5cf6; }
|
|
617
|
+
.tag-other { color: #94a3b8; }
|
|
618
|
+
footer { text-align: center; color: var(--text-muted); font-size: 0.8rem; margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--card-border); }
|
|
619
|
+
@media print {
|
|
620
|
+
body { background: #fff !important; color: #000 !important; }
|
|
621
|
+
.actions, .filter-bar, button { display: none !important; }
|
|
622
|
+
.kpi-card, .module-card, .leaderboard, .progress-bar-container { border: 1px solid #ccc !important; box-shadow: none !important; }
|
|
623
|
+
}
|
|
624
|
+
</style>
|
|
625
|
+
</head>
|
|
626
|
+
<body>
|
|
627
|
+
<div class="container">
|
|
628
|
+
<header>
|
|
629
|
+
<div class="title-group">
|
|
630
|
+
<h1>📊 {{ d.project }} <span class="badge badge-primary">{{ d.audience }}</span></h1>
|
|
631
|
+
<div class="meta-text">
|
|
632
|
+
Period: <strong>{{ d.start }}</strong> to <strong>{{ d.end }}</strong> • Generated: {{ d.generated }}
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="actions">
|
|
636
|
+
{% if d.remote_url %}
|
|
637
|
+
<a href="{{ d.remote_url }}" target="_blank" class="btn">🔗 GitHub</a>
|
|
638
|
+
{% endif %}
|
|
639
|
+
<button onclick="window.print()">🖨️ Print / PDF</button>
|
|
640
|
+
<button onclick="toggleTheme()" id="themeBtn">🌓 Theme</button>
|
|
641
|
+
</div>
|
|
642
|
+
</header>
|
|
643
|
+
|
|
644
|
+
<!-- KPI Cards -->
|
|
645
|
+
<div class="kpi-grid">
|
|
646
|
+
<div class="kpi-card">
|
|
647
|
+
<div class="kpi-label">Commits</div>
|
|
648
|
+
<div class="kpi-value">{{ d.metrics.commits }}</div>
|
|
649
|
+
<div class="kpi-sub">{{ d.metrics.commits_seen_before_filter }} in period</div>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="kpi-card">
|
|
652
|
+
<div class="kpi-label">Code Churn</div>
|
|
653
|
+
<div class="kpi-value" style="color: var(--success);">+{{ d.metrics.lines_added }}</div>
|
|
654
|
+
<div class="kpi-sub" style="color: var(--danger);">-{{ d.metrics.lines_deleted }} lines ({{ d.metrics.lines_changed }} total)</div>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="kpi-card">
|
|
657
|
+
<div class="kpi-label">Top Module</div>
|
|
658
|
+
<div class="kpi-value" style="font-size: 1.35rem; color: var(--primary);">{{ d.metrics.top_module }}</div>
|
|
659
|
+
<div class="kpi-sub">{{ d.metrics.modules }} active module(s)</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="kpi-card">
|
|
662
|
+
<div class="kpi-label">Test-to-Code</div>
|
|
663
|
+
<div class="kpi-value">{{ d.metrics.test_to_code_ratio }}</div>
|
|
664
|
+
<div class="kpi-sub">Test lines ratio</div>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="kpi-card">
|
|
667
|
+
<div class="kpi-label">Contributors</div>
|
|
668
|
+
<div class="kpi-value">{{ d.metrics.authors }}</div>
|
|
669
|
+
<div class="kpi-sub">Active developers</div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<!-- At a Glance -->
|
|
674
|
+
<div class="glance-banner">
|
|
675
|
+
<span>✨ <strong>At a glance:</strong> {{ d.glance_text }}</span>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<!-- Module Distribution Progress Bar -->
|
|
679
|
+
{% if d.metrics.module_distribution %}
|
|
680
|
+
<div class="progress-bar-container">
|
|
681
|
+
<div style="font-size: 0.85rem; font-weight: 600; text-transform: uppercase; color: var(--text-muted);">
|
|
682
|
+
Module Work Distribution
|
|
683
|
+
</div>
|
|
684
|
+
<div class="progress-bar">
|
|
685
|
+
{% set colors_list = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#06b6d4', '#ec4899', '#64748b'] %}
|
|
686
|
+
{% for mod, pct in d.metrics.module_distribution.items() %}
|
|
687
|
+
<div class="progress-segment" style="width: {{ pct }}%; background-color: {{ colors_list[loop.index0 % 7] }};" title="{{ mod }}: {{ pct }}%"></div>
|
|
688
|
+
{% endfor %}
|
|
689
|
+
</div>
|
|
690
|
+
<div class="progress-legend">
|
|
691
|
+
{% for mod, pct in d.metrics.module_distribution.items() %}
|
|
692
|
+
<div class="legend-item">
|
|
693
|
+
<div class="legend-dot" style="background-color: {{ colors_list[loop.index0 % 7] }};"></div>
|
|
694
|
+
<span>{{ mod }} ({{ pct }}%)</span>
|
|
695
|
+
</div>
|
|
696
|
+
{% endfor %}
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
{% endif %}
|
|
700
|
+
|
|
701
|
+
<!-- Module Code Churn Visual Chart -->
|
|
702
|
+
{% if d.sections and d.sections|length > 0 %}
|
|
703
|
+
<div class="progress-bar-container">
|
|
704
|
+
<div style="font-size: 0.85rem; font-weight: 600; text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px; display: flex; justify-content: space-between;">
|
|
705
|
+
<span>📊 Code Churn by Module</span>
|
|
706
|
+
<span style="font-size: 0.8rem; font-weight: normal;"><font color="var(--success)">+Lines Added</font> • <font color="var(--danger)">-Lines Deleted</font></span>
|
|
707
|
+
</div>
|
|
708
|
+
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
709
|
+
{% for s in d.sections %}
|
|
710
|
+
{% set s_add = s.commits | sum(attribute='added') %}
|
|
711
|
+
{% set s_del = s.commits | sum(attribute='deleted') %}
|
|
712
|
+
<div style="display: grid; grid-template-columns: 140px 1fr 110px; align-items: center; gap: 12px; font-size: 0.85rem;">
|
|
713
|
+
<div style="font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ s.module }}</div>
|
|
714
|
+
<div style="display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: rgba(255,255,255,0.05);">
|
|
715
|
+
{% set total_churn = s_add + s_del %}
|
|
716
|
+
{% if total_churn > 0 %}
|
|
717
|
+
<div style="width: {{ (s_add / total_churn) * 100 }}%; background: var(--success);" title="+{{ s_add }} lines added"></div>
|
|
718
|
+
<div style="width: {{ (s_del / total_churn) * 100 }}%; background: var(--danger);" title="-{{ s_del }} lines deleted"></div>
|
|
719
|
+
{% endif %}
|
|
720
|
+
</div>
|
|
721
|
+
<div style="text-align: right; font-family: monospace; font-size: 0.8rem;">
|
|
722
|
+
<span style="color: var(--success);">+{{ s_add }}</span> / <span style="color: var(--danger);">-{{ s_del }}</span>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
{% endfor %}
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
{% endif %}
|
|
729
|
+
|
|
730
|
+
<!-- Contributor Leaderboard -->
|
|
731
|
+
{% if d.metrics.contributors and d.metrics.contributors|length > 0 %}
|
|
732
|
+
<div class="leaderboard">
|
|
733
|
+
<div style="font-size: 0.9rem; font-weight: 600; text-transform: uppercase; color: var(--text-muted);">
|
|
734
|
+
👥 Team Contributions
|
|
735
|
+
</div>
|
|
736
|
+
<table>
|
|
737
|
+
<thead>
|
|
738
|
+
<tr>
|
|
739
|
+
<th>Developer</th>
|
|
740
|
+
<th style="text-align: center;">Commits</th>
|
|
741
|
+
<th>Lines Added</th>
|
|
742
|
+
<th>Lines Deleted</th>
|
|
743
|
+
<th>Primary Module</th>
|
|
744
|
+
</tr>
|
|
745
|
+
</thead>
|
|
746
|
+
<tbody>
|
|
747
|
+
{% for c in d.metrics.contributors %}
|
|
748
|
+
<tr>
|
|
749
|
+
<td><strong>{{ c.name }}</strong></td>
|
|
750
|
+
<td style="text-align: center;"><span class="badge badge-primary">{{ c.commits }}</span></td>
|
|
751
|
+
<td style="color: var(--success);">+{{ c.added }}</td>
|
|
752
|
+
<td style="color: var(--danger);">-{{ c.deleted }}</td>
|
|
753
|
+
<td><span class="badge badge-accent">{{ c.top_module }}</span></td>
|
|
754
|
+
</tr>
|
|
755
|
+
{% endfor %}
|
|
756
|
+
</tbody>
|
|
757
|
+
</table>
|
|
758
|
+
</div>
|
|
759
|
+
{% endif %}
|
|
760
|
+
|
|
761
|
+
<!-- Filter Bar -->
|
|
762
|
+
<div class="filter-bar">
|
|
763
|
+
<div style="font-size: 1.1rem; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
|
764
|
+
📦 Module Breakdown ({{ d.sections|length }})
|
|
765
|
+
</div>
|
|
766
|
+
<input type="text" id="commitSearch" class="search-input" placeholder="🔍 Search commits, authors, hashes..." onkeyup="filterCommits()">
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<!-- Modules List -->
|
|
770
|
+
<div id="modulesContainer">
|
|
771
|
+
{% for s in d.sections %}
|
|
772
|
+
<div class="module-card" data-module="{{ s.module|lower }}">
|
|
773
|
+
<div class="module-header">
|
|
774
|
+
<div class="module-title">{{ s.module }}</div>
|
|
775
|
+
<span class="badge badge-primary">{{ s.hashes|length }} commit(s)</span>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="summary-box">
|
|
778
|
+
{{ s.text }}
|
|
779
|
+
</div>
|
|
780
|
+
{% if s.note %}
|
|
781
|
+
<div style="font-size: 0.8rem; color: var(--warning); margin-bottom: 10px;">⚠️ {{ s.note }}</div>
|
|
782
|
+
{% endif %}
|
|
783
|
+
|
|
784
|
+
{% if s.commits %}
|
|
785
|
+
<table class="commit-table">
|
|
786
|
+
<tbody>
|
|
787
|
+
{% for cm in s.commits %}
|
|
788
|
+
<tr class="commit-row" data-search="{{ cm.subject|lower }} {{ cm.author|lower }} {{ cm.hash|lower }}">
|
|
789
|
+
<td style="width: 80px;">
|
|
790
|
+
{% if cm.type == 'feat' %}<span class="badge badge-success">FEAT</span>
|
|
791
|
+
{% elif cm.type == 'fix' %}<span class="badge badge-danger">FIX</span>
|
|
792
|
+
{% elif cm.type in ['perf', 'refactor'] %}<span class="badge badge-accent">{{ cm.type|upper }}</span>
|
|
793
|
+
{% else %}<span class="badge" style="background: var(--chip-bg); color: var(--text-muted);">{{ cm.type|upper }}</span>{% endif %}
|
|
794
|
+
</td>
|
|
795
|
+
<td style="width: 90px;">
|
|
796
|
+
{% if d.remote_url %}
|
|
797
|
+
<a href="{{ d.remote_url }}/commit/{{ cm.hash }}" target="_blank" class="commit-hash">{{ cm.hash }}</a>
|
|
798
|
+
{% else %}
|
|
799
|
+
<span class="commit-hash">{{ cm.hash }}</span>
|
|
800
|
+
{% endif %}
|
|
801
|
+
</td>
|
|
802
|
+
<td>{{ cm.subject }}</td>
|
|
803
|
+
<td style="width: 140px; color: var(--text-muted); font-size: 0.8rem;">{{ cm.author }}</td>
|
|
804
|
+
<td style="width: 100px; text-align: right; font-size: 0.8rem;">
|
|
805
|
+
<span style="color: var(--success);">+{{ cm.added }}</span> / <span style="color: var(--danger);">-{{ cm.deleted }}</span>
|
|
806
|
+
</td>
|
|
807
|
+
</tr>
|
|
808
|
+
{% endfor %}
|
|
809
|
+
</tbody>
|
|
810
|
+
</table>
|
|
811
|
+
{% endif %}
|
|
812
|
+
</div>
|
|
813
|
+
{% endfor %}
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<!-- In Progress (Open Branches) -->
|
|
817
|
+
{% if d.in_progress %}
|
|
818
|
+
<div class="module-card">
|
|
819
|
+
<div class="module-title" style="margin-bottom: 12px;">🌿 In Progress (Open Branches)</div>
|
|
820
|
+
<table>
|
|
821
|
+
<thead>
|
|
822
|
+
<tr>
|
|
823
|
+
<th>Branch</th>
|
|
824
|
+
<th>Commits Ahead</th>
|
|
825
|
+
<th>Last Updated</th>
|
|
826
|
+
<th>Latest Subject</th>
|
|
827
|
+
</tr>
|
|
828
|
+
</thead>
|
|
829
|
+
<tbody>
|
|
830
|
+
{% for b in d.in_progress %}
|
|
831
|
+
<tr>
|
|
832
|
+
<td><code>{{ b.name }}</code></td>
|
|
833
|
+
<td><span class="badge badge-warning">{{ b.ahead }}</span></td>
|
|
834
|
+
<td>{{ b.last_date }}</td>
|
|
835
|
+
<td>{{ b.subject }}</td>
|
|
836
|
+
</tr>
|
|
837
|
+
{% endfor %}
|
|
838
|
+
</tbody>
|
|
839
|
+
</table>
|
|
840
|
+
</div>
|
|
841
|
+
{% endif %}
|
|
842
|
+
|
|
843
|
+
<!-- Planned Goals -->
|
|
844
|
+
{% if d.planned %}
|
|
845
|
+
<div class="module-card">
|
|
846
|
+
<div class="module-title" style="margin-bottom: 12px;">🎯 Planned Goals for Next Period</div>
|
|
847
|
+
<ul style="padding-left: 20px; line-height: 1.8;">
|
|
848
|
+
{% for p in d.planned %}
|
|
849
|
+
<li>{{ p }}</li>
|
|
850
|
+
{% endfor %}
|
|
851
|
+
</ul>
|
|
852
|
+
</div>
|
|
853
|
+
{% endif %}
|
|
854
|
+
|
|
855
|
+
<footer>
|
|
856
|
+
Generated by <strong>Git2Report</strong> on {{ d.generated }} • Mode: {{ d.mode_label }}
|
|
857
|
+
</footer>
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
<script>
|
|
861
|
+
function toggleTheme() {
|
|
862
|
+
document.body.classList.toggle('light');
|
|
863
|
+
localStorage.setItem('g2r-theme', document.body.classList.contains('light') ? 'light' : 'dark');
|
|
864
|
+
}
|
|
865
|
+
if (localStorage.getItem('g2r-theme') === 'light') {
|
|
866
|
+
document.body.classList.add('light');
|
|
867
|
+
}
|
|
868
|
+
function filterCommits() {
|
|
869
|
+
const q = document.getElementById('commitSearch').value.toLowerCase();
|
|
870
|
+
const rows = document.querySelectorAll('.commit-row');
|
|
871
|
+
rows.forEach(r => {
|
|
872
|
+
const text = r.getAttribute('data-search') || '';
|
|
873
|
+
r.style.display = text.includes(q) ? '' : 'none';
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
</script>
|
|
877
|
+
</body>
|
|
878
|
+
</html>
|
|
879
|
+
"""
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def render_html(data: ReportData) -> str:
|
|
883
|
+
template = Template(HTML_TEMPLATE)
|
|
884
|
+
return template.render(d=data)
|