git-ember 1.2.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.
- git_ember-1.2.0.dist-info/METADATA +165 -0
- git_ember-1.2.0.dist-info/RECORD +10 -0
- git_ember-1.2.0.dist-info/WHEEL +5 -0
- git_ember-1.2.0.dist-info/entry_points.txt +2 -0
- git_ember-1.2.0.dist-info/top_level.txt +1 -0
- gitember/__init__.py +1 -0
- gitember/cli.py +414 -0
- gitember/colors.py +143 -0
- gitember/git.py +535 -0
- gitember/render.py +293 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-ember
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A GitHub-style heatmap of commits for your terminal
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ruff>=0.15.9
|
|
8
|
+
|
|
9
|
+
# git-ember
|
|
10
|
+
|
|
11
|
+
A GitHub-style heatmap of commit activity for your terminal.
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`git-ember` displays a colored grid representing commit activity over time, similar to the contribution graph shown on GitHub profiles. It reads directly from your local Git history—no external APIs or network requests required.
|
|
16
|
+
|
|
17
|
+
The tool supports multiple color schemes, branch filtering, custom date ranges, and can display repository statistics, recent commits, and top contributors.
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- Python 3.11 or higher
|
|
22
|
+
- Git installed and available in PATH
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Clone the repository:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://codeberg.org/lukavr05/git-ember.git
|
|
30
|
+
cd git-ember
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Running without installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
PYTHONPATH=src python3 main.py .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
git-ember reads configuration from `~/.config/git-ember/config.toml`. CLI arguments take precedence over config values.
|
|
43
|
+
|
|
44
|
+
| Variable | Required | Default | Description |
|
|
45
|
+
|----------|----------|---------|-------------|
|
|
46
|
+
| `color` | No | `green` | Color scheme name |
|
|
47
|
+
| `border` | No | `=` | Border character |
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Show commit heatmap for the current year:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git-ember
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Show heatmap for a specific repository:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git-ember /path/to/repo
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Show multiple years:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git-ember --years 2
|
|
67
|
+
git-ember -y 3
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use different color schemes:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git-ember --color blue
|
|
74
|
+
git-ember --color orange
|
|
75
|
+
git-ember --color purple
|
|
76
|
+
git-ember --color mono
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Show heatmap for a specific branch:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git-ember --branch feature-x
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Display branch tree visualization:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git-ember --tree
|
|
89
|
+
git-ember --branch feature-x --tree
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Show custom date range:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git-ember --after 2025-01-01 --before 2025-06-30
|
|
96
|
+
git-ember --after 2025-01-01
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Show extended report with recent commits and top contributors:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git-ember --extended
|
|
103
|
+
git-ember -e
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Show repository statistics:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git-ember --stats
|
|
110
|
+
git-ember -S
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Command-line options
|
|
114
|
+
|
|
115
|
+
| Flag | Alias | Description | Default |
|
|
116
|
+
|------|-------|-------------|---------|
|
|
117
|
+
| `--color` | `-c` | Color scheme | `green` |
|
|
118
|
+
| `--years` | `-y` | Number of years to show | `1` |
|
|
119
|
+
| `--border` | `-b` | Border character | `=` |
|
|
120
|
+
| `--extended` | `-e` | Show recent commits and top contributors | `false` |
|
|
121
|
+
| `--stats` | `-S` | Show repository statistics | `false` |
|
|
122
|
+
| `--ascii` | `-a` | Use ASCII characters instead of Unicode | `false` |
|
|
123
|
+
| `--compact` | - | Show last 4 months only | `false` |
|
|
124
|
+
| `--branch` | - | Show heatmap for specific branch | all branches |
|
|
125
|
+
| `--tree` | `-t` | Show branch tree under heatmap | `false` |
|
|
126
|
+
| `--scale` | - | Intensity scaling: auto or adaptive | `auto` |
|
|
127
|
+
| `--after` | - | Show commits after date (YYYY-MM-DD) | - |
|
|
128
|
+
| `--before` | - | Show commits before date (YYYY-MM-DD) | - |
|
|
129
|
+
| `--version` | `-V` | Show version | - |
|
|
130
|
+
| `--help` | `-h` | Show help | - |
|
|
131
|
+
|
|
132
|
+
## Project Structure
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
git-ember/
|
|
136
|
+
├── main.py # Entry point
|
|
137
|
+
├── pyproject.toml # Package configuration
|
|
138
|
+
├── Makefile # Build targets
|
|
139
|
+
├── .python-version # Python version (3.11)
|
|
140
|
+
├── README.md # This file
|
|
141
|
+
├── docs/
|
|
142
|
+
│ ├── CHANGELOG.md # Version history
|
|
143
|
+
│ └── PLAN.md # Feature planning
|
|
144
|
+
└── src/
|
|
145
|
+
└── githeat/
|
|
146
|
+
├── __init__.py # Package version
|
|
147
|
+
├── cli.py # CLI argument parsing and config
|
|
148
|
+
├── git.py # Git command execution and parsing
|
|
149
|
+
├── render.py # Grid and branch tree rendering
|
|
150
|
+
└── colors.py # ANSI color scheme definitions
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
Run the linter:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
make lint
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> ⚠️ Note: No test suite exists currently.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
No LICENSE file exists in this repository.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gitember/__init__.py,sha256=MpAT5hgNoHnTtG1XRD_GV_A7QrHVU6vJjGSw_8qMGA4,22
|
|
2
|
+
gitember/cli.py,sha256=lfK87jtzweUyMOkFhYBw3_m3g8IdKw3ugw1aY5DdVIc,11764
|
|
3
|
+
gitember/colors.py,sha256=v5bHcCj9pdSqcieG3X-WzeSgsLVpLtNsSbf-eEHvZGY,3344
|
|
4
|
+
gitember/git.py,sha256=8YpoAml6bFumCN3hoNmqdv-YVX-XbbTpwu4_rWwuwXI,14303
|
|
5
|
+
gitember/render.py,sha256=zadIVrKCIXaaE6rDi23So656rvrH36qNqxGhgM8angk,8691
|
|
6
|
+
git_ember-1.2.0.dist-info/METADATA,sha256=tQk4rqXb6xt19NUtr_DUTd8IWfLffyvrJaf8bW_iNUM,3978
|
|
7
|
+
git_ember-1.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
git_ember-1.2.0.dist-info/entry_points.txt,sha256=-IiSHTFzrEdM25fuz-psxq-U24lv7XWCZ2tOpHpHk2s,48
|
|
9
|
+
git_ember-1.2.0.dist-info/top_level.txt,sha256=VEJk48Zmg73VytgvOKH_qeGSq6GGADX5-fjFElHBNPY,9
|
|
10
|
+
git_ember-1.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitember
|
gitember/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.0"
|
gitember/cli.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import calendar
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from githeat import __version__
|
|
10
|
+
from githeat.colors import get_color_scheme, RESET, LIGHT_GRAY
|
|
11
|
+
from githeat.git import (
|
|
12
|
+
run_git_log,
|
|
13
|
+
get_recent_commits,
|
|
14
|
+
get_top_contributors,
|
|
15
|
+
get_repo_stats,
|
|
16
|
+
get_branches,
|
|
17
|
+
get_branch_tree,
|
|
18
|
+
get_default_branch,
|
|
19
|
+
get_streaks,
|
|
20
|
+
)
|
|
21
|
+
from githeat.render import render_grid, render_branch_tree, calculate_thresholds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def config_file_path() -> Path:
|
|
25
|
+
"""Get the path to the config file.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Path to config.toml in platform-specific config directory.
|
|
29
|
+
"""
|
|
30
|
+
if sys.platform.startswith("win"):
|
|
31
|
+
base = os.getenv("APPDATA")
|
|
32
|
+
if base:
|
|
33
|
+
base_path = Path(base)
|
|
34
|
+
else:
|
|
35
|
+
base_path = Path.home() / "AppData" / "Roaming"
|
|
36
|
+
else:
|
|
37
|
+
xdg = os.getenv("XDG_CONFIG_HOME")
|
|
38
|
+
if xdg:
|
|
39
|
+
base_path = Path(xdg)
|
|
40
|
+
else:
|
|
41
|
+
base_path = Path.home() / ".config"
|
|
42
|
+
return base_path / "git-ember" / "config.toml"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
ALLOWED_CONFIG_KEYS = {"color", "border"}
|
|
46
|
+
# NOTE: branch is intentionally excluded from config to avoid user confusion
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_config() -> dict:
|
|
50
|
+
"""Load config from file, returning defaults if not exists.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dict with keys: color, border.
|
|
54
|
+
"""
|
|
55
|
+
path = config_file_path()
|
|
56
|
+
if not path.exists():
|
|
57
|
+
return {
|
|
58
|
+
"color": "green",
|
|
59
|
+
"border": "=",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
config = {"color": "green", "border": "="}
|
|
63
|
+
try:
|
|
64
|
+
text = path.read_text(encoding="utf-8")
|
|
65
|
+
except OSError:
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
# Simple TOML-like parsing: key=value, ignore comments
|
|
69
|
+
for line in text.splitlines():
|
|
70
|
+
stripped = line.strip()
|
|
71
|
+
# Skip empty lines and comments
|
|
72
|
+
if not stripped or stripped.startswith("#"):
|
|
73
|
+
continue
|
|
74
|
+
if "=" in stripped:
|
|
75
|
+
key, _, value = stripped.partition("=")
|
|
76
|
+
key = key.strip()
|
|
77
|
+
# Strip quotes from value - handles both "value" and 'value'
|
|
78
|
+
value = value.strip().strip('"').strip("'")
|
|
79
|
+
# Only allow predefined keys (whitelist for security)
|
|
80
|
+
if key in ALLOWED_CONFIG_KEYS:
|
|
81
|
+
config[key] = value
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def save_config(config: dict) -> None:
|
|
86
|
+
"""Save config to file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: Dict with keys: week_start, style, color, border.
|
|
90
|
+
"""
|
|
91
|
+
path = config_file_path()
|
|
92
|
+
try:
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
lines = []
|
|
95
|
+
for k, v in config.items():
|
|
96
|
+
if k in ALLOWED_CONFIG_KEYS:
|
|
97
|
+
escaped = v.replace('"', '\\"')
|
|
98
|
+
lines.append(f'{k} = "{escaped}"')
|
|
99
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_args() -> argparse.Namespace:
|
|
105
|
+
"""Parse command line arguments.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Parsed arguments namespace.
|
|
109
|
+
"""
|
|
110
|
+
parser = argparse.ArgumentParser(
|
|
111
|
+
description="Display a GitHub-style heatmap of commits in your terminal.",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"path",
|
|
115
|
+
nargs="?",
|
|
116
|
+
default=".",
|
|
117
|
+
help="Path to git repository (default: current directory)",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--color",
|
|
121
|
+
"-c",
|
|
122
|
+
choices=[
|
|
123
|
+
"green",
|
|
124
|
+
"blue",
|
|
125
|
+
"orange",
|
|
126
|
+
"purple",
|
|
127
|
+
"red",
|
|
128
|
+
"yellow",
|
|
129
|
+
"teal",
|
|
130
|
+
"pink",
|
|
131
|
+
"aqua",
|
|
132
|
+
"mono",
|
|
133
|
+
],
|
|
134
|
+
help="Color scheme",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--years",
|
|
138
|
+
"-y",
|
|
139
|
+
type=int,
|
|
140
|
+
default=1,
|
|
141
|
+
help="Number of years to show (default: 1)",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--border",
|
|
145
|
+
"-b",
|
|
146
|
+
type=str,
|
|
147
|
+
default="=",
|
|
148
|
+
help="Border character (default: =)",
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--extended",
|
|
152
|
+
"-e",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="Show extended report with recent commits and top contributors",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--stats",
|
|
158
|
+
"-S",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Show repository statistics (total commits, authors, dates)",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--ascii",
|
|
164
|
+
"-a",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help="Use ASCII characters instead of Unicode blocks",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--compact",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Only show months up to the current month",
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--branch",
|
|
175
|
+
action="store",
|
|
176
|
+
help="Show heatmap for specific branch instead of all branches",
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--tree",
|
|
180
|
+
"-t",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="Show branch tree under heatmap",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--scale",
|
|
186
|
+
type=str,
|
|
187
|
+
choices=["auto", "adaptive"],
|
|
188
|
+
default="auto",
|
|
189
|
+
help="Intensity scaling mode: auto or adaptive (default: auto)",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--after",
|
|
193
|
+
type=str,
|
|
194
|
+
help="Show commits after this date (YYYY-MM-DD)",
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--before",
|
|
198
|
+
type=str,
|
|
199
|
+
help="Show commits before this date (YYYY-MM-DD)",
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"--version",
|
|
203
|
+
"-V",
|
|
204
|
+
action="version",
|
|
205
|
+
version=f"git-ember {__version__}",
|
|
206
|
+
)
|
|
207
|
+
return parser.parse_args()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_repo_path(path: str) -> str:
|
|
211
|
+
"""Resolve path to absolute path.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
path: Relative or absolute path to git repository.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Absolute path as string.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ValueError: If path is unsafe (contains suspicious characters).
|
|
221
|
+
"""
|
|
222
|
+
if not path:
|
|
223
|
+
raise ValueError("Path cannot be empty")
|
|
224
|
+
|
|
225
|
+
import re
|
|
226
|
+
|
|
227
|
+
if re.search(r"[;&|`$()]", path):
|
|
228
|
+
raise ValueError(f"Invalid characters in path: {path}")
|
|
229
|
+
|
|
230
|
+
resolved = Path(path).resolve()
|
|
231
|
+
|
|
232
|
+
return str(resolved)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main() -> None:
|
|
236
|
+
"""Main entry point."""
|
|
237
|
+
args = parse_args()
|
|
238
|
+
repo_path = get_repo_path(args.path)
|
|
239
|
+
|
|
240
|
+
config = load_config()
|
|
241
|
+
|
|
242
|
+
color_name = args.color or config.get("color", "green")
|
|
243
|
+
years = args.years
|
|
244
|
+
border_char = args.border[0] if args.border else "=" # prevent escape codes
|
|
245
|
+
ascii_mode = args.ascii
|
|
246
|
+
branch = args.branch
|
|
247
|
+
|
|
248
|
+
if branch:
|
|
249
|
+
valid_branches = get_branches(repo_path)
|
|
250
|
+
if branch not in valid_branches:
|
|
251
|
+
print(f"Error: Branch '{branch}' not found")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
if args.color is not None:
|
|
255
|
+
new_config = config.copy()
|
|
256
|
+
if args.color:
|
|
257
|
+
new_config["color"] = args.color
|
|
258
|
+
save_config(new_config)
|
|
259
|
+
|
|
260
|
+
color_scheme = get_color_scheme(color_name)
|
|
261
|
+
|
|
262
|
+
today = dt.date.today()
|
|
263
|
+
output_parts = []
|
|
264
|
+
extended = args.extended
|
|
265
|
+
compact = args.compact
|
|
266
|
+
scale_mode = args.scale
|
|
267
|
+
|
|
268
|
+
custom_range = args.after or args.before
|
|
269
|
+
|
|
270
|
+
if args.after:
|
|
271
|
+
try:
|
|
272
|
+
start_date = dt.date.fromisoformat(args.after)
|
|
273
|
+
except ValueError:
|
|
274
|
+
print("Error: Invalid --after date. Use YYYY-MM-DD format.")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
if args.before:
|
|
277
|
+
try:
|
|
278
|
+
end_date = dt.date.fromisoformat(args.before)
|
|
279
|
+
except ValueError:
|
|
280
|
+
print("Error: Invalid --before date. Use YYYY-MM-DD format.")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
if custom_range:
|
|
284
|
+
num_iterations = 1
|
|
285
|
+
else:
|
|
286
|
+
num_iterations = years
|
|
287
|
+
|
|
288
|
+
for i in range(num_iterations):
|
|
289
|
+
if custom_range:
|
|
290
|
+
pass
|
|
291
|
+
else:
|
|
292
|
+
year = today.year - i
|
|
293
|
+
current_month = today.month
|
|
294
|
+
|
|
295
|
+
if compact and i == 0:
|
|
296
|
+
start_month = max(1, current_month - 3)
|
|
297
|
+
start_date = dt.date(year, start_month, 1)
|
|
298
|
+
end_date = dt.date(
|
|
299
|
+
year, current_month, calendar.monthrange(year, current_month)[1]
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
start_date = dt.date(year, 1, 1)
|
|
303
|
+
end_date = dt.date(year, 12, 31)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
counts = run_git_log(start_date, end_date, repo_path, branch=branch)
|
|
307
|
+
except ValueError as e:
|
|
308
|
+
print(f"Error: {e}")
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
311
|
+
thresholds = calculate_thresholds(counts, scale_mode)
|
|
312
|
+
|
|
313
|
+
heatmap = render_grid(
|
|
314
|
+
start_date,
|
|
315
|
+
end_date,
|
|
316
|
+
counts,
|
|
317
|
+
color_scheme,
|
|
318
|
+
"sunday",
|
|
319
|
+
border_char,
|
|
320
|
+
ascii_mode,
|
|
321
|
+
thresholds=thresholds,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if custom_range:
|
|
325
|
+
output_parts.append(heatmap)
|
|
326
|
+
elif num_iterations > 1:
|
|
327
|
+
output_parts.append(f"[{year}]\n{heatmap}")
|
|
328
|
+
else:
|
|
329
|
+
output_parts.append(heatmap)
|
|
330
|
+
|
|
331
|
+
output_parts.reverse()
|
|
332
|
+
print("\n" + "\n".join(output_parts))
|
|
333
|
+
|
|
334
|
+
if args.tree:
|
|
335
|
+
tree_commits = get_branch_tree(repo_path, branch=branch)
|
|
336
|
+
default_branch = get_default_branch(repo_path)
|
|
337
|
+
tree_output = render_branch_tree(
|
|
338
|
+
tree_commits,
|
|
339
|
+
default_branch=default_branch,
|
|
340
|
+
selected_branch=branch,
|
|
341
|
+
color_scheme=color_scheme,
|
|
342
|
+
ascii_mode=ascii_mode,
|
|
343
|
+
compact=args.compact,
|
|
344
|
+
)
|
|
345
|
+
if tree_output:
|
|
346
|
+
print(tree_output)
|
|
347
|
+
|
|
348
|
+
show_stats = args.stats or extended
|
|
349
|
+
|
|
350
|
+
if show_stats:
|
|
351
|
+
stats = get_repo_stats(repo_path, branch=branch)
|
|
352
|
+
|
|
353
|
+
print("\n--- Statistics ---")
|
|
354
|
+
print(f" Total commits: {stats.get('total_commits', 0)}")
|
|
355
|
+
print(f" Authors: {stats.get('num_authors', 0)}")
|
|
356
|
+
|
|
357
|
+
first_commit_date = stats.get("first_commit_date", "")
|
|
358
|
+
if first_commit_date:
|
|
359
|
+
print(f" First commit: {first_commit_date}")
|
|
360
|
+
|
|
361
|
+
last_commit_date = stats.get("last_commit_date", "")
|
|
362
|
+
if last_commit_date:
|
|
363
|
+
print(f" Last commit: {last_commit_date}")
|
|
364
|
+
|
|
365
|
+
streak_start_date = dt.date.today() - dt.timedelta(days=years * 365)
|
|
366
|
+
streak_end_date = dt.date.today()
|
|
367
|
+
streak_counts = run_git_log(
|
|
368
|
+
streak_start_date, streak_end_date, repo_path, branch=branch
|
|
369
|
+
)
|
|
370
|
+
streaks = get_streaks(streak_counts)
|
|
371
|
+
|
|
372
|
+
if streaks["current_streak"] > 0:
|
|
373
|
+
print(f" Current streak: {streaks['current_streak']} days")
|
|
374
|
+
|
|
375
|
+
if streaks["longest_streak"] > 0:
|
|
376
|
+
longest_end = streaks["longest_streak_end"]
|
|
377
|
+
if longest_end:
|
|
378
|
+
month_name = longest_end.strftime("%b %Y")
|
|
379
|
+
print(
|
|
380
|
+
f" Longest streak: {streaks['longest_streak']} days ({month_name})"
|
|
381
|
+
)
|
|
382
|
+
else:
|
|
383
|
+
print(f" Longest streak: {streaks['longest_streak']} days")
|
|
384
|
+
|
|
385
|
+
if extended:
|
|
386
|
+
print("\n--- Recent Commits ---")
|
|
387
|
+
for commit in get_recent_commits(repo_path, branch=branch):
|
|
388
|
+
commit_color = color_scheme.get(3)
|
|
389
|
+
author_color = color_scheme.get(2)
|
|
390
|
+
|
|
391
|
+
commit_time = (
|
|
392
|
+
time.strftime("%H:%M", time.localtime(int(commit["timestamp"])))
|
|
393
|
+
if commit["timestamp"]
|
|
394
|
+
else ""
|
|
395
|
+
)
|
|
396
|
+
date_parts = commit["date"].split("-")
|
|
397
|
+
formatted_date = (
|
|
398
|
+
f"{date_parts[2]}-{date_parts[1]}-{date_parts[0]}"
|
|
399
|
+
if len(date_parts) == 3
|
|
400
|
+
else commit["date"]
|
|
401
|
+
)
|
|
402
|
+
print(
|
|
403
|
+
f" {commit_color}{commit['hash']}{RESET} | {author_color}{commit['author']}{RESET} | {LIGHT_GRAY}{formatted_date} {commit_time}{RESET}"
|
|
404
|
+
)
|
|
405
|
+
print(f" {commit['message']}")
|
|
406
|
+
|
|
407
|
+
print("\n--- Top Contributors ---")
|
|
408
|
+
author_color = color_scheme.get(2)
|
|
409
|
+
for author, count in get_top_contributors(repo_path, branch=branch):
|
|
410
|
+
print(f" {count:>4} {author_color}{author}{RESET}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
main()
|