diff --git a/.gitignore b/.gitignore index 075cbda..1a37262 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ docs/build memory/ TASKS.md dashboard.html +.gstack/ diff --git a/CLAUDE.md b/CLAUDE.md index 8ece4c2..4a2b03a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,3 +39,15 @@ Xianpeng Shen (shenxianpeng), DevOps engineer. Maintains the gitstats open sourc - Branch for PRs: `main` - Always run `nox -s lint` before committing - **Language**: Always reply in the same language the user writes in. If the user writes in Chinese, respond in Chinese. + +## Design System +Always read `DESIGN.md` before making any visual or UI decisions. +All font choices, colors, spacing, and aesthetic direction are defined there. +Do not deviate without explicit user approval. +In QA mode, flag any code that doesn't match `DESIGN.md`. + +Key rules (never violate without checking DESIGN.md first): +- `font-family` must be `var(--font-mono)` on body — no fallback to sans-serif +- Accent color is warm amber (`#D97706` light / `#F59E0B` dark) — not steel blue +- Border-radius: zero everywhere +- No cards, no box-shadow elevation, no grain textures diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..91dd711 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,304 @@ +# Design System — GitStats + +## Product Context +- **What this is:** A Python CLI tool that generates self-contained HTML/JSON reports with statistics from Git repositories +- **Who it's for:** Developers and DevOps engineers analyzing their own repositories +- **Space/industry:** Developer tooling / repository analytics +- **Project type:** Data report — static HTML output, not a web app or SaaS product + +## Aesthetic Direction +- **Direction:** Terminal-Native / Developer-First +- **Decoration level:** Minimal — typography, spacing, and contrast do all the work +- **Mood:** Feels like high-quality CLI tool output rendered as HTML. Not a SaaS product trying to appeal to developers — a developer tool that reflects developer values. The user should think "this tool knows what it is." +- **Key differentiator:** Warm grayscale palette + zero border-radius + full monospace typeface. Most git analytics tools (GitHub, GitLab, Sourcegraph) use cool-gray, rounded-corner SaaS aesthetics. GitStats owns the developer-native space. + +## Typography +- **All text — IBM Plex Mono** (one typeface for everything, including body) + - This is a deliberate commitment, not an omission. The gitstats user is a developer. There are no non-technical users to protect from monospace body text. + - `font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace` +- **Loading:** Google Fonts CDN (`https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap`) +- **Type scale:** + +| Role | Size | Weight | Notes | +|------|------|--------|-------| +| Hero / page title | 22–28px | 600 | letter-spacing: -0.03em | +| Section heading h2 | 18px | 500 | prefixed with `// ` in amber | +| Sub-heading h3 | 15px | 500 | — | +| Body / paragraphs | 13px | 400 | line-height: 1.7 | +| UI labels / table headers | 11px | 500 | uppercase, letter-spacing: 0.05em | +| Tabular data | 12px | 400 | font-feature-settings: "tnum" | +| Code / commands | 12px | 400 | background surface, border | + +## Color + +### Light mode +| Token | Value | Usage | +|-------|-------|-------| +| `--bg` | `#ffffff` | Page background | +| `--surface` | `#f5f5f4` | Table headers, code blocks, surface elements | +| `--surface-2` | `#ebeaea` | Hover states | +| `--text` | `#211e1e` | Primary text (warm near-black) | +| `--text-2` | `#4b4646` | Secondary text, nav links | +| `--text-muted` | `#9e9a9a` | Placeholder, metadata | +| `--border` | `#cfcecd` | Default borders | +| `--border-strong` | `#9e9a9a` | Table borders, strong dividers | +| `--amber` | `#D97706` | **Accent** — links, bar charts, interactive hover, `//` heading prefix | +| `--amber-hover` | `#b45309` | Amber on hover | +| `--amber-light` | `#fef3c7` | Alert/badge backgrounds | +| `--green` | `#2d6a3f` | Health score, success states | +| `--red` | `#8b2323` | Danger/error states | +| `--yellow` | `#d4a017` | Warning states | + +### Dark mode +| Token | Value | Usage | +|-------|-------|-------| +| `--bg` | `#211e1e` | Page background (warm near-black) | +| `--surface` | `#2d2929` | Surface elements | +| `--surface-2` | `#352f2f` | Hover states | +| `--text` | `#f1ecec` | Primary text (warm near-white) | +| `--text-2` | `#b7b1b1` | Secondary text | +| `--text-muted` | `#656363` | Muted text | +| `--border` | `#3d3838` | Default borders | +| `--border-strong` | `#656363` | Strong borders | +| `--amber` | `#F59E0B` | Accent (one stop brighter for dark contrast) | +| `--amber-hover` | `#fbbf24` | Amber on hover | +| `--green` | `#4ade80` | Health/success | +| `--red` | `#f87171` | Danger/error | +| `--yellow` | `#e3b341` | Warning | + +### Color role rules +- **Amber** = interaction, state emphasis, links, bar charts, active indicators +- **Green** = heatmap density (GitHub green scale), health scores, success states — never use for interaction +- These two color families must not overlap. Amber is for "you can click/interact with this." Green is for "this is a data density or health signal." + +### Heatmap scale +GitHub green — colorblind-safe, universally recognizable for contribution graphs: +- Level 0: `#ebedf0` (light) / `#1c2128` (dark) +- Level 1: `#9be9a8` / `#0e4429` +- Level 2: `#40c463` / `#006d32` +- Level 3: `#30a14e` / `#26a641` +- Level 4: `#216e39` / `#39d353` + +## Spacing +- **Base unit:** 4px +- **Scale:** `--sp-1: 4px` `--sp-2: 8px` `--sp-3: 12px` `--sp-4: 16px` `--sp-5: 24px` `--sp-6: 32px` `--sp-7: 48px` `--sp-8: 64px` +- **Density:** Compact-to-comfortable. Table cell padding: 8px 12px. Reports are data-dense — don't waste vertical space. +- **Max content width:** 1280px + +## Layout +- **Approach:** Section-based, not card-based. Sections separated by `

` headings and `
` dividers. Tables, chart regions, and stat blocks are the primitives — not cards. +- **Border radius:** Zero everywhere (`border-radius: 0`). This is the terminal/angular identity. Never introduce rounded corners. +- **Grid:** No explicit CSS grid. Section content flows naturally with max-width constraint. +- **Nav:** Sticky top bar, min-height 44px (WCAG 2.5.5 touch target). + +### Section heading convention +All `h2` elements get a `//` prefix in amber, applied via CSS: +```css +h2::before { content: '// '; color: var(--amber); } +``` + +### Generation command block +On the general/index page, directly below `

`, show the command used to generate the report: +``` +$ gitstats /path/to/repo ./output ▊ +``` +Styled with surface background, 1px border, amber prompt character, blinking cursor. This is the single strongest "this is a CLI tool" signal and requires ~4 lines of Python to implement. + +## Motion + +### Tokens +| Token | Duration | Easing | Usage | +|-------|----------|--------|-------| +| `--transition-fast` | `120ms ease-out` | ease-out | hover color/background shifts | +| `--transition-base` | `150ms ease-out` | ease-out | border-color, nav state | +| `--transition-theme` | `200ms ease` | ease | theme toggle — bg + color on body | +| `--transition-cursor` | `1.2s step-end infinite` | step-end | blinking cursor (blink keyframe) | + +### Rules +- **Standard:** `transition: background-color 120ms ease-out, color 120ms ease-out, border-color 120ms ease-out` +- **Theme switch:** `transition: background-color 0.2s ease, color 0.2s ease` on `body` +- **Never animate layout properties** (`width`, `height`, `top`, `left`) — transform + opacity only +- **No animations on data load** — reports are static; entrance animations would feel wrong +- **`prefers-reduced-motion`:** Cursor blink animation (`@keyframes blink`) is disabled via `@media (prefers-reduced-motion: reduce)`. All other transitions are safe (sub-200ms, no layout shifts). + +## Interactive States + +### Focus +- **`:focus-visible`:** `outline: 2px solid var(--amber); outline-offset: 2px` +- Never use `outline: none` without a visible replacement +- All nav links, the theme toggle, and table header sort anchors must show focus ring on keyboard nav + +### Hover +- **Links:** color shifts to `var(--amber)`, underline appears +- **Nav links:** color → `var(--amber)`, background → `var(--surface-color)` +- **Table rows:** `tr:hover td` → background `var(--table-hover-bg)` +- **Bar charts:** `.bar` opacity 0.85 → 1.0 on parent hover +- **Theme toggle / nav-github:** color → `var(--amber)` or `var(--text-color)` + +### Active / Current page +- Nav link for the current page should visually indicate active state (bold weight or amber color) +- (not yet implemented — future enhancement) + +## Responsive Design + +### Strategy +GitStats reports are primarily desktop tools (run via CLI, opened in a browser). Mobile is secondary but should not be broken. The goal is "readable and usable on mobile" not "designed for mobile." + +### Breakpoints +| Name | Width | Behavior | +|------|-------|----------| +| desktop | ≥ 768px | Full layout, sticky nav, side-by-side columns | +| mobile | < 768px | Stacked layout, scrollable tables, wrapped nav | + +### Nav on mobile +**Current state:** Nav items wrap into multiple rows (deferred — FINDING-003). +**Target:** Implement horizontal scrollable nav with `overflow-x: auto; white-space: nowrap` on `.nav ul`. No hamburger menu — developer tool users expect dense, direct access. A horizontally scrolling list of 6 links is appropriate for this audience. + +**Implementation plan (when prioritized):** +```css +@media (max-width: 768px) { + .nav ul { + overflow-x: auto; + white-space: nowrap; + flex-wrap: nowrap; + -webkit-overflow-scrolling: touch; + } + .nav li a { + min-height: 44px; /* already set */ + } +} +``` + +### Tables on mobile +Tables use `display: block; overflow-x: auto` on all viewports (implemented). Wide tables scroll horizontally within the viewport — correct behavior for data reports. + +### Charts on mobile +Chart.js canvases are responsive by default (`responsive: true`). Charts reflow to container width automatically. + +## Component Library + +### Navigation (`.nav`) +``` +[GitStats] [General | Activity | Authors | Files | Lines | Tags] [GitHub ⬡] [🌙] +``` +- Sticky top bar, `min-height: 44px`, `z-index: 100` +- Brand: `16px 700 monospace`, left +- Links: `12px 500 monospace`, separated by `1px border-left`, vertical dividers +- Right side: GitHub icon link + theme toggle button, separated from links by `border-left` +- Active hover: color → amber, background → surface +- All interactive elements: `min-height: 44px`, `min-width: 44px` (WCAG 2.5.5) + +### Generation Command Block (`.cmd-block`) +``` +$ gitstats . ./output ▊ +``` +- Surface background, `1px border`, monospace 12px, amber `$` prompt, blinking cursor `▊` +- `@media (prefers-reduced-motion: reduce)` disables blink +- Placed directly below `

` on the index/general page only + +### Tables +- `border-collapse: collapse`, `font-size: 12px` +- `th`: surface background, uppercase, `11px`, `letter-spacing: 0.08em`, strong border +- `td`: white/table-bg background, `8px 12px` padding, light border +- `tr:hover`: table-hover-bg background +- `table.noborders`: bar chart tables — no borders, transparent background +- Responsive: `display: block; overflow-x: auto` on all viewports + +### Bar Charts (`.bar` + `.noborders`) +- Plain `` with `
` cells containing a `
` +- `.bar` background: `var(--bar-color)` = `var(--amber)`, opacity 0.85 +- On row hover: opacity → 1.0 +- Bar height/width set inline by Python based on value percentage + +### Heatmap (`.heat0`–`.heat4`) +- Used on the Activity page for commit frequency calendars +- GitHub green scale (5 levels, colorblind-safe) +- Level 0 = no activity, Level 4 = peak activity +- Text color inverts to white at levels 3–4 for contrast +- See Color section for exact hex values + +### Section Headings (`h2`) +```html +

Section Name

+``` +- CSS `h2::before { content: '// '; color: var(--amber); }` +- `border-left: 2px solid var(--amber)`, surface background, 18px 500 +- Hover reveals `#` anchor link via `h2:hover a::after` + +### AI Summary (`.ai-summary`) +Used for optional AI-generated analysis blocks. Not part of the base report. +- Surface background, strong border, `border-left: 3px solid var(--text-color)` +- Error variant `.ai-summary-error`: `border-left: 3px solid var(--danger-color)` +- The colored left border here is semantic (indicating "this is AI-generated content"), not decorative — an intentional exception to the no-card-left-border rule + +## Chart System + +### Chart.js Configuration +All charts use Chart.js (bundled, browser-side). Design tokens to apply: + +```javascript +// Color sequence — amber first, then muted earth tones +CHART_COLORS = [ + '#D97706', // amber (primary — always first) + '#92400E', // amber-dark + '#B45309', // amber-mid + '#78716C', // warm gray + '#57534E', // warm gray dark + '#A8A29E', // warm gray light + '#059669', // green (only for health/positive data) + '#DC2626', // red (only for error/negative data) +] +``` + +### Axis & Grid +- Grid lines: `var(--border-color)` (#cfcecd light / #3d3838 dark), `lineWidth: 1` +- Tick labels: `var(--text-secondary)`, 11px, monospace font +- Axis labels: `var(--text-muted)`, 11px +- No chart border (borderWidth: 0 on datasets where appropriate) + +### Tooltip +- Background: `var(--surface-color)` with `1px solid var(--border-strong)` +- Title: `var(--text-color)`, 12px bold +- Body: `var(--text-secondary)`, 12px +- No border-radius (consistent with zero-radius system) +- Padding: 8px 12px + +### Dark mode +Chart colors shift with CSS variables — use the `data-theme="dark"` class on `` to trigger re-read of CSS variable values for charts. Re-initialize chart theme on toggle. + +## Accessibility +- Amber `#D97706` on white `#ffffff`: contrast ratio 4.6:1 (passes WCAG AA) +- GitHub green heatmap scale: safe for deuteranopia/protanopia (green is not paired with red for the same signal) +- All nav links have min 44px touch targets (implemented) +- `:focus-visible` ring is amber, 2px, offset 2px (implemented) +- Color is never the sole conveyor of meaning — badges include text labels, status indicators include icons +- `color-scheme: light` / `dark` set on `:root` / `[data-theme="dark"]` for native browser dark mode (implemented) + +## What NOT to do +- No border-radius anywhere (not even on badges or buttons) +- No card-based layout (no `box-shadow` elevation tricks) +- No grain/noise texture (gitstats reports are opened as `file://` — embedded SVG noise has Safari flicker issues and degrades the static file use case) +- No purple gradients, no blobs, no decorative backgrounds +- Do not use system-ui or sans-serif for body text — the full monospace commitment is the identity +- Do not use the current steel blue (`#4a7ab5`) — replace with amber everywhere + +## Implementation Notes +- CSS lives in `gitstats/gitstats.css` (single file, ~584 lines as of 2026-03) +- Multi-dataset `CHART_COLORS` in `report_creator.py` should use amber-first ordering +- Body `font-family` must be `var(--font-mono)` — the current fallback to `var(--font-sans)` is a bug in the original implementation +- `--link-color` and `--bar-color` should both be `var(--amber)` in both themes + +## Decisions Log +| Date | Decision | Rationale | +|------|----------|-----------| +| 2026-03-24 | Full IBM Plex Mono (body included) | Gitstats users are developers — all-mono body text is correct for this audience and creates an unmistakable terminal identity | +| 2026-03-24 | Warm amber (#D97706) replaces steel blue | Steel blue is generic and fights the warm grayscale palette; amber is rare in the git analytics space and coheres with the warm near-black | +| 2026-03-24 | No grain/noise texture | Reports opened as file:// — embedded SVG noise flickers in Safari and degrades the static file use case | +| 2026-03-24 | Zero border-radius, no cards | Terminal aesthetic; card grids are SaaS patterns that weaken the developer-native identity | +| 2026-03-24 | Generation command block on index page | Strongest single signal of "this is a CLI tool" — surfaces the command that created the report | +| 2026-03-24 | Mobile nav = horizontal scroll, not hamburger | Developer tool audience — dense, direct access is correct. Hamburger adds interaction cost with no benefit for 6 links. | +| 2026-03-24 | Tables use display:block + overflow-x:auto | Prevents horizontal page scroll without disrupting table semantics; standard responsive table pattern | +| 2026-03-24 | AI summary left-border is semantic, not decorative | Intentional exception to no-left-border rule: distinguishes AI-generated content from report data | +| 2026-03-25 | Mobile nav = horizontal scroll, not hamburger | Developer tool audience — dense, direct access is correct. Hamburger adds interaction cost with no benefit for 6 links. | +| 2026-03-25 | Added: Interactive States, Responsive Design, Component Library, Chart System sections | Closes gap between CSS implementation and design documentation | +| 2026-03-24 | Design system created | Created by /design-consultation based on competitive research (GitHub Insights, GitLab Analytics, Sourcegraph, Wakatime, git-quick-stats) + Codex + Claude subagent review | diff --git a/gitstats/gitstats.css b/gitstats/gitstats.css index e262ad9..1e01468 100644 --- a/gitstats/gitstats.css +++ b/gitstats/gitstats.css @@ -6,6 +6,7 @@ /* ===== CSS Custom Properties ===== */ :root { + color-scheme: light; /* Typography — OpenCode uses IBM Plex Mono / Inter */ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; --font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; @@ -29,17 +30,24 @@ --radius-md: 0; --radius-lg: 0; - /* ── Light mode — OpenCode warm palette ── */ + /* ── Light mode — warm palette ── */ --bg-color: #ffffff; --surface-color: #f5f5f4; + --surface-2: #ebeaea; --text-color: #211e1e; - --text-secondary: #656363; - --text-muted: #b7b1b1; + --text-secondary: #4b4646; + --text-muted: #9e9a9a; --border-color: #cfcecd; --border-strong: #9e9a9a; --accent-color: #211e1e; --success-color: #2d6a3f; --danger-color: #8b2323; + --warning-color: #d4a017; + + /* Amber — the accent: links, bars, h2 prefix, hover */ + --amber: #D97706; + --amber-hover: #b45309; + --amber-light: #fef3c7; --table-bg: #ffffff; --table-header-bg: #f5f5f4; @@ -49,14 +57,14 @@ --nav-bg: #ffffff; --nav-text: #4b4646; - --nav-hover-color: #211e1e; + --nav-hover-color: var(--amber); --img-bg: #f5f5f4; --img-border: #cfcecd; - --link-color: #4a7ab5; + --link-color: var(--amber); - --bar-color: #4a7ab5; + --bar-color: var(--amber); /* Heatmap — GitHub green scale (user preference) */ --heat0-bg: #ebedf0; @@ -71,10 +79,12 @@ --heat4-color: #ffffff; } -/* ── Dark mode — OpenCode near-black ── */ +/* ── Dark mode — warm near-black ── */ [data-theme="dark"] { + color-scheme: dark; --bg-color: #211e1e; --surface-color: #2d2929; + --surface-2: #352f2f; --text-color: #f1ecec; --text-secondary: #b7b1b1; --text-muted: #656363; @@ -83,6 +93,11 @@ --accent-color: #f1ecec; --success-color: #4ade80; --danger-color: #f87171; + --warning-color: #e3b341; + + /* Amber — one stop brighter for dark contrast */ + --amber: #F59E0B; + --amber-hover: #fbbf24; --table-bg: #2d2929; --table-header-bg: #211e1e; @@ -92,14 +107,14 @@ --nav-bg: #211e1e; --nav-text: #b7b1b1; - --nav-hover-color: #f1ecec; + --nav-hover-color: var(--amber); --img-bg: #2d2929; --img-border: #3d3838; - --link-color: #6b95c8; + --link-color: var(--amber); - --bar-color: #6b95c8; + --bar-color: var(--amber); /* Heatmap dark — GitHub dark green */ --heat0-bg: #1c2128; @@ -120,9 +135,9 @@ } body { - font-family: var(--font-sans); - font-size: var(--font-size-base); - line-height: 1.6; + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.7; color: var(--text-color); background-color: var(--bg-color); margin: 0 auto; @@ -131,6 +146,41 @@ body { transition: background-color 0.2s ease, color 0.2s ease; } +/* ===== Generation Command Block ===== */ +.cmd-block { + display: inline-block; + margin: var(--space-3) 0 var(--space-5); + padding: var(--space-2) var(--space-4); + background-color: var(--surface-color); + border: 1px solid var(--border-color); + font-family: var(--font-mono); + font-size: var(--font-size-sm); + color: var(--text-secondary); +} + +.cmd-prompt { + color: var(--amber); + margin-right: var(--space-2); + font-weight: 600; +} + +.cmd-cursor { + color: var(--amber); + animation: blink 1.2s step-end infinite; + margin-left: 2px; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .cmd-cursor { + animation: none; + } +} + /* ===== Definition Lists ===== */ dt { font-weight: 600; @@ -157,9 +207,15 @@ table { font-size: var(--font-size-sm); margin-bottom: var(--space-4); border: 1px solid var(--border-strong); + display: block; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + max-width: 100%; } table.noborders { + display: table; + overflow-x: visible; border: none; background: transparent; } @@ -218,6 +274,7 @@ tr:hover td { display: flex; align-items: center; justify-content: space-between; + min-height: 44px; padding: var(--space-4) 0; margin-bottom: var(--space-5); border-bottom: 1px solid var(--border-strong); @@ -248,6 +305,7 @@ tr:hover td { .nav li a { display: inline-flex; align-items: center; + min-height: 44px; padding: var(--space-2) var(--space-3); text-decoration: none; color: var(--nav-text); @@ -260,13 +318,16 @@ tr:hover td { } .nav li a:hover { - color: var(--nav-hover-color); + color: var(--amber); background-color: var(--surface-color); text-decoration: none; } /* ===== Nav Brand ===== */ .nav-brand { + display: inline-flex; + align-items: center; + min-height: 44px; font-family: var(--font-mono); font-size: var(--font-size-md); font-weight: 700; @@ -296,6 +357,9 @@ tr:hover td { .nav-github { display: inline-flex; align-items: center; + justify-content: center; + min-width: 44px; + min-height: 44px; color: var(--text-secondary); padding: var(--space-1); transition: color 0.12s ease; @@ -316,6 +380,8 @@ tr:hover td { background: transparent; border: 1px solid var(--border-color); cursor: pointer; + min-width: 44px; + min-height: 44px; padding: var(--space-1) var(--space-2); font-size: var(--font-size-base); color: var(--text-secondary); @@ -340,6 +406,7 @@ h1 { border-bottom: 1px solid var(--border-strong); line-height: 1.3; letter-spacing: -0.02em; + text-wrap: balance; } h1 a { @@ -349,20 +416,21 @@ h1 a { h2 { font-family: var(--font-mono); - font-size: var(--font-size-md); - font-weight: 600; + font-size: 18px; + font-weight: 500; color: var(--text-color); margin: var(--space-6) 0 var(--space-3); padding: var(--space-2) var(--space-3); - border-left: 2px solid var(--text-color); + border-left: 2px solid var(--amber); background-color: var(--surface-color); clear: both; letter-spacing: -0.01em; + text-wrap: balance; } h2::before { content: '// '; - color: var(--text-muted); + color: var(--amber); font-weight: 400; letter-spacing: 0; } @@ -398,6 +466,12 @@ th img { display: inline; } +/* ===== Focus Visible ===== */ +:focus-visible { + outline: 2px solid var(--amber); + outline-offset: 2px; +} + /* ===== Links ===== */ a { color: var(--link-color); diff --git a/gitstats/report_creator.py b/gitstats/report_creator.py index 396595f..d9f90be 100644 --- a/gitstats/report_creator.py +++ b/gitstats/report_creator.py @@ -86,6 +86,10 @@ def create_index_html(self, data, path): f.write("

General

") + f.write( + f"""
$ gitstats {data.dir} {path}
""" + ) + f.write(html_header(2, "Git Overview")) f.write("") @@ -977,6 +981,9 @@ def print_header(self, file) -> None: GitStats - %s + + +