diffstory 0.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.
- diffstory/__init__.py +3 -0
- diffstory/__main__.py +5 -0
- diffstory/cli.py +402 -0
- diffstory/diff_parser.py +298 -0
- diffstory/git_utils.py +365 -0
- diffstory/html_generator.py +2343 -0
- diffstory/syntax.py +145 -0
- diffstory-0.2.0.dist-info/METADATA +207 -0
- diffstory-0.2.0.dist-info/RECORD +12 -0
- diffstory-0.2.0.dist-info/WHEEL +5 -0
- diffstory-0.2.0.dist-info/entry_points.txt +2 -0
- diffstory-0.2.0.dist-info/top_level.txt +1 -0
diffstory/syntax.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Syntax highlighting using Pygments, works entirely offline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pygments import highlight
|
|
8
|
+
from pygments.lexers import (
|
|
9
|
+
get_lexer_by_name,
|
|
10
|
+
get_lexer_for_filename,
|
|
11
|
+
ClassNotFound,
|
|
12
|
+
)
|
|
13
|
+
from pygments.formatters import HtmlFormatter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Mapping of file extensions to language names for quick lookup
|
|
17
|
+
EXTENSION_MAP: dict[str, str] = {
|
|
18
|
+
".py": "python",
|
|
19
|
+
".js": "javascript",
|
|
20
|
+
".jsx": "javascript",
|
|
21
|
+
".ts": "typescript",
|
|
22
|
+
".tsx": "typescript",
|
|
23
|
+
".java": "java",
|
|
24
|
+
".cs": "csharp",
|
|
25
|
+
".go": "go",
|
|
26
|
+
".rs": "rust",
|
|
27
|
+
".php": "php",
|
|
28
|
+
".rb": "ruby",
|
|
29
|
+
".kt": "kotlin",
|
|
30
|
+
".kts": "kotlin",
|
|
31
|
+
".sql": "sql",
|
|
32
|
+
".sh": "bash",
|
|
33
|
+
".bash": "bash",
|
|
34
|
+
".zsh": "bash",
|
|
35
|
+
".yml": "yaml",
|
|
36
|
+
".yaml": "yaml",
|
|
37
|
+
".json": "json",
|
|
38
|
+
".html": "html",
|
|
39
|
+
".htm": "html",
|
|
40
|
+
".css": "css",
|
|
41
|
+
".md": "markdown",
|
|
42
|
+
".markdown": "markdown",
|
|
43
|
+
".xml": "xml",
|
|
44
|
+
".svg": "xml",
|
|
45
|
+
".toml": "ini",
|
|
46
|
+
".cfg": "ini",
|
|
47
|
+
".ini": "ini",
|
|
48
|
+
".txt": "text",
|
|
49
|
+
".dockerfile": "docker",
|
|
50
|
+
".tf": "terraform",
|
|
51
|
+
".vue": "html",
|
|
52
|
+
".svelte": "html",
|
|
53
|
+
".c": "c",
|
|
54
|
+
".h": "c",
|
|
55
|
+
".cpp": "cpp",
|
|
56
|
+
".cc": "cpp",
|
|
57
|
+
".hpp": "cpp",
|
|
58
|
+
".swift": "swift",
|
|
59
|
+
".scala": "scala",
|
|
60
|
+
".ex": "elixir",
|
|
61
|
+
".exs": "elixir",
|
|
62
|
+
".erl": "erlang",
|
|
63
|
+
".hrl": "erlang",
|
|
64
|
+
".hs": "haskell",
|
|
65
|
+
".lua": "lua",
|
|
66
|
+
".r": "r",
|
|
67
|
+
".R": "r",
|
|
68
|
+
".m": "matlab",
|
|
69
|
+
".mm": "matlab",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def get_lexer_for_file(filepath: str) -> object:
|
|
73
|
+
"""Get the appropriate Pygments lexer for a file path."""
|
|
74
|
+
# Try by extension first
|
|
75
|
+
ext = "." + filepath.rsplit(".", 1)[-1].lower() if "." in filepath else ""
|
|
76
|
+
if ext in EXTENSION_MAP:
|
|
77
|
+
try:
|
|
78
|
+
return get_lexer_by_name(EXTENSION_MAP[ext])
|
|
79
|
+
except ClassNotFound:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Try by full filename
|
|
83
|
+
try:
|
|
84
|
+
return get_lexer_for_filename(filepath)
|
|
85
|
+
except ClassNotFound:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Try to guess from common filenames
|
|
89
|
+
basename = filepath.rsplit("/", 1)[-1].lower()
|
|
90
|
+
if basename in ("dockerfile",):
|
|
91
|
+
try:
|
|
92
|
+
return get_lexer_by_name("docker")
|
|
93
|
+
except ClassNotFound:
|
|
94
|
+
pass
|
|
95
|
+
if basename in ("makefile", "gnumakefile"):
|
|
96
|
+
try:
|
|
97
|
+
return get_lexer_by_name("make")
|
|
98
|
+
except ClassNotFound:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_highlighted_line(line: str, filepath: str, lexer_cache: dict) -> str:
|
|
105
|
+
"""Highlight a single line of code using a cached lexer."""
|
|
106
|
+
from html import escape
|
|
107
|
+
lexer = lexer_cache.get(filepath)
|
|
108
|
+
if lexer is None:
|
|
109
|
+
lexer = get_lexer_for_file(filepath)
|
|
110
|
+
lexer_cache[filepath] = lexer
|
|
111
|
+
|
|
112
|
+
if lexer is None:
|
|
113
|
+
return escape(line)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
formatter = HtmlFormatter(nowrap=True, style="default")
|
|
117
|
+
return highlight(line, lexer, formatter)
|
|
118
|
+
except Exception:
|
|
119
|
+
return escape(line)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_syntax_css(style: str = "default") -> str:
|
|
123
|
+
"""Get the CSS for syntax highlighting."""
|
|
124
|
+
formatter = HtmlFormatter(style=style)
|
|
125
|
+
return formatter.get_style_defs(".highlight")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _scope_css(css: str, theme: str) -> str:
|
|
129
|
+
"""Scope every .highlight rule under a data-theme attribute selector."""
|
|
130
|
+
lines = css.splitlines()
|
|
131
|
+
result = []
|
|
132
|
+
for line in lines:
|
|
133
|
+
stripped = line.strip()
|
|
134
|
+
if stripped.startswith(".highlight"):
|
|
135
|
+
indent = line[:len(line) - len(line.lstrip())]
|
|
136
|
+
line = indent + '[data-theme="' + theme + '"] ' + stripped
|
|
137
|
+
result.append(line)
|
|
138
|
+
return "\n".join(result)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_syntax_styles() -> str:
|
|
142
|
+
"""Get both light and dark syntax highlight CSS, scoped by data-theme."""
|
|
143
|
+
light_css = get_syntax_css("default")
|
|
144
|
+
dark_css = get_syntax_css("monokai")
|
|
145
|
+
return "\n" + _scope_css(light_css, "light") + "\n\n" + _scope_css(dark_css, "dark") + "\n"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffstory
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Transform Git diffs into rich, interactive, self-contained HTML reports
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/lakshayjindal/diffstory
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: Pygments>=2.10
|
|
17
|
+
|
|
18
|
+
# DiffStory
|
|
19
|
+
|
|
20
|
+
**Transform Git diffs into rich, interactive, self-contained HTML reports.**
|
|
21
|
+
|
|
22
|
+
DiffStory turns any `git diff` into a beautiful, portable HTML report that answers not just *what* changed, but *who* changed it, *when*, and *why* — all offline, in a single file.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install diffstory
|
|
26
|
+
cd my-repo
|
|
27
|
+
diffstory --staged -o report.html
|
|
28
|
+
# Open report.html in any browser
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
### Phase 1 (MVP)
|
|
36
|
+
- **Unified View** — Classic git-style diff with syntax highlighting
|
|
37
|
+
- **Side-by-Side View** — Original and modified columns, synchronized
|
|
38
|
+
- **Inline Edit View** — Word-level diff showing exact token changes
|
|
39
|
+
- **Syntax Highlighting** — 30+ languages via Pygments, light + dark themes
|
|
40
|
+
- **Statistics Dashboard** — Files changed, +/-, authors breakdown
|
|
41
|
+
- **File Sidebar** — Navigate files with search, collapse/expand
|
|
42
|
+
- **Keyboard Shortcuts** — `U`/`S`/`I` to switch views, `D` for theme, `Esc` to close
|
|
43
|
+
- **Theme Toggle** — Light/dark with system preference detection and persistence
|
|
44
|
+
- **Export Formats** — HTML, JSON, Markdown, CSV
|
|
45
|
+
|
|
46
|
+
### Phase 2 (Blame Integration)
|
|
47
|
+
- **Blame Tooltips** — Hover any changed line to see author, commit, date, and message
|
|
48
|
+
- **Commit Drawer** — Click a line to open a detailed side panel with full commit metadata
|
|
49
|
+
- **Relative Time** — "2h ago", "3d ago" for at-a-glance recency
|
|
50
|
+
|
|
51
|
+
### Future Phases
|
|
52
|
+
- Search across filename, author, commit message, and code content
|
|
53
|
+
- Filtering by author, date range, file type, change type
|
|
54
|
+
- Commit evolution viewer and timeline
|
|
55
|
+
- Deep linking to specific lines and files
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
### From PyPI (once published)
|
|
62
|
+
```bash
|
|
63
|
+
pip install diffstory
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### From source
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/user/diffstory.git
|
|
69
|
+
cd diffstory
|
|
70
|
+
pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Requirements:** Python 3.10+, Git
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
### Basic Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Working tree diff
|
|
83
|
+
diffstory
|
|
84
|
+
|
|
85
|
+
# Staged changes
|
|
86
|
+
diffstory --staged
|
|
87
|
+
|
|
88
|
+
# Compare commits
|
|
89
|
+
diffstory HEAD~3 HEAD
|
|
90
|
+
|
|
91
|
+
# Compare branches
|
|
92
|
+
diffstory main feature
|
|
93
|
+
|
|
94
|
+
# Custom output file
|
|
95
|
+
diffstory -o my-report.html
|
|
96
|
+
|
|
97
|
+
# Multiple export formats
|
|
98
|
+
diffstory --staged --json --md --csv -o report
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Keyboard Shortcuts (in the HTML report)
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `U` | Unified view |
|
|
106
|
+
| `S` | Side-by-side view |
|
|
107
|
+
| `I` | Inline edit view |
|
|
108
|
+
| `D` | Toggle theme |
|
|
109
|
+
| `Esc` | Close drawer / stats panel |
|
|
110
|
+
| `F` | Focus file search |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Report Features
|
|
115
|
+
|
|
116
|
+
### Three View Modes
|
|
117
|
+
|
|
118
|
+
| Mode | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| **Unified** | Classic git diff format with line numbers |
|
|
121
|
+
| **Side-by-Side** | Two-column layout — original on left, modified on right |
|
|
122
|
+
| **Inline Edit** | Word-level diff showing additions (green) and removals (red strikethrough) within the same line |
|
|
123
|
+
|
|
124
|
+
### Blame Tooltips (Phase 2)
|
|
125
|
+
|
|
126
|
+
Hover over any changed line to see:
|
|
127
|
+
- **Author** name
|
|
128
|
+
- **Commit hash** (short, 7 chars)
|
|
129
|
+
- **Commit subject**
|
|
130
|
+
- **Date** with relative time ("2h ago")
|
|
131
|
+
|
|
132
|
+
Click any line to open the **Commit Drawer** with full metadata: body, committer, parents, files changed, insertions/deletions.
|
|
133
|
+
|
|
134
|
+
### Statistics
|
|
135
|
+
|
|
136
|
+
The statistics panel shows:
|
|
137
|
+
- Files changed, additions, deletions
|
|
138
|
+
- Added / deleted / modified / renamed file counts
|
|
139
|
+
- Top 10 most-changed files with per-file breakdown
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Output
|
|
144
|
+
|
|
145
|
+
Reports are fully self-contained single HTML files:
|
|
146
|
+
- All CSS inlined
|
|
147
|
+
- All JavaScript inlined
|
|
148
|
+
- All data embedded as JSON
|
|
149
|
+
- No external dependencies
|
|
150
|
+
- Works offline in any modern browser
|
|
151
|
+
- Safe to email or archive
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Project Structure
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
diffstory/
|
|
159
|
+
├── pyproject.toml # Build config & entry point
|
|
160
|
+
├── requirements.md # Full product requirements
|
|
161
|
+
├── .gitignore
|
|
162
|
+
├── src/diffstory/
|
|
163
|
+
│ ├── __init__.py # Package version
|
|
164
|
+
│ ├── __main__.py # python -m diffstory
|
|
165
|
+
│ ├── cli.py # CLI argument parsing & orchestration
|
|
166
|
+
│ ├── git_utils.py # Git subprocess wrappers
|
|
167
|
+
│ ├── diff_parser.py # Unified diff → structured data
|
|
168
|
+
│ ├── syntax.py # Pygments syntax highlighting
|
|
169
|
+
│ └── html_generator.py # Self-contained HTML report generation
|
|
170
|
+
└── tests/
|
|
171
|
+
└── __init__.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Install in editable mode
|
|
180
|
+
pip install -e .
|
|
181
|
+
|
|
182
|
+
# Run against a test repo
|
|
183
|
+
cd /tmp && mkdir test && cd test
|
|
184
|
+
git init
|
|
185
|
+
echo "hello" > test.py
|
|
186
|
+
git add -A && git commit -m "init"
|
|
187
|
+
echo "world" >> test.py
|
|
188
|
+
diffstory
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Security
|
|
194
|
+
|
|
195
|
+
DiffStory is designed for air-gapped, audit-safe use:
|
|
196
|
+
- Never uploads code
|
|
197
|
+
- Never transmits data
|
|
198
|
+
- No telemetry
|
|
199
|
+
- No accounts required
|
|
200
|
+
- No external API calls
|
|
201
|
+
- Never modifies your repository
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diffstory/__init__.py,sha256=CLe-2vhq8c6aj3_3Y4Nq37NngqZ9oPQwTy1r8Xqy9Ec,100
|
|
2
|
+
diffstory/__main__.py,sha256=Pc47yF5jQlbjKC_BSH_pna-n3MnEA3lIKsE3TvJ_Jlo,128
|
|
3
|
+
diffstory/cli.py,sha256=aZ7KbfT7CWlVTR-ERBmPWnYscafV10uFvmtiDHN7Nsk,12678
|
|
4
|
+
diffstory/diff_parser.py,sha256=n9L-MmWBSZP-nYohMb1jhOZrERv8O8rnJ3OzeGMpYgU,9417
|
|
5
|
+
diffstory/git_utils.py,sha256=eT77bnYKOlY30ZK07gK60VJWioBjKKgKBNsS-jzXCMg,10786
|
|
6
|
+
diffstory/html_generator.py,sha256=iyHkk9uxlVLpeYuYJXGRPnNOjRJKFNFjz7Spkab3yHQ,72226
|
|
7
|
+
diffstory/syntax.py,sha256=8KPUvBObrZ0uQ5n0-axbC6wRO_ygWhICsADwpYdoSmw,3867
|
|
8
|
+
diffstory-0.2.0.dist-info/METADATA,sha256=QlR_yL1h8bxzZw2uSeVudq2a85dCh8SBfXK0G86nRG0,5330
|
|
9
|
+
diffstory-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
diffstory-0.2.0.dist-info/entry_points.txt,sha256=_PGs0-KAfUwKvHnoDa7V-ouABl0dAMeRetsEgijp0LM,49
|
|
11
|
+
diffstory-0.2.0.dist-info/top_level.txt,sha256=qxbolVX6YxYMHor32Ih3RMUHGmhvspVJKnSKZsLwVPQ,10
|
|
12
|
+
diffstory-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
diffstory
|