Skip to content

Commit 3179cb7

Browse files
committed
The fix was verified by actually simulating Windows CP1252 stdout (io.TextIOWrapper(..., encoding='cp1252', errors='strict')).. runs clean.
Windows uses CP1252 encoding by default for stdout.. it can't print emoji. Every emoji in every print() and string in git_data.py and cli.py crashes on Windows. fix all of them plus add PYTHONIOENCODING to CI as a safety net. What's new vs previous version ✅ Renamed to git-diff, command is git-diff ✅ 3 themes: 🌙 Dark · ☀️ Light · ⚫ AMOLED (persisted in localStorage) ✅ Compare any two refs — branch/branch, SHA/SHA, tag/tag, any mix ✅ Blame view — full git blame with commit hash + author + date per line ✅ File history — commits that touched a file ✅ Activity heatmap — GitHub-calendar style (last 90 days) ✅ Language stats — color bar chart by file extension ✅ Merge commit detection + parent navigation ✅ Keyboard shortcuts (Esc, Ctrl+R, Ctrl+K, Ctrl+\) ✅ Full REST API with 15 endpoint
1 parent 4266942 commit 3179cb7

File tree

5 files changed

+56
-83
lines changed

5 files changed

+56
-83
lines changed

.github/workflows/ci.yml

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ jobs:
1616
os: [ubuntu-latest, macos-latest, windows-latest]
1717
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1818

19+
# Force UTF-8 stdout on ALL platforms — critical for Windows (CP1252 default)
20+
env:
21+
PYTHONIOENCODING: utf-8
22+
PYTHONUTF8: 1
23+
1924
steps:
2025
- uses: actions/checkout@v4
2126

@@ -24,58 +29,26 @@ jobs:
2429
with:
2530
python-version: ${{ matrix.python-version }}
2631

32+
- name: Upgrade pip (silently)
33+
run: python -m pip install --upgrade pip --quiet
34+
2735
- name: Install package (editable)
2836
run: pip install -e .
2937

30-
- name: Smoke test import
38+
- name: Smoke test - import
3139
run: python -c "import git_diff; print('Version:', git_diff.__version__)"
3240

33-
- name: Smoke test CLI version
41+
- name: Smoke test - CLI version
3442
run: git-diff --version
3543

36-
- name: Smoke test CLI help
44+
- name: Smoke test - CLI help
3745
run: git-diff --help
3846

39-
- name: Functional test — git data collection
40-
run: |
41-
python -c "
42-
from git_diff.git_data import get_repo_root, collect_all_data, parse_diff
43-
root = get_repo_root('.')
44-
print('Root:', root)
45-
46-
# Test parse_diff
47-
sample = '''diff --git a/hello.py b/hello.py
48-
index abc..def 100644
49-
--- a/hello.py
50-
+++ b/hello.py
51-
@@ -1,3 +1,4 @@
52-
def hello():
53-
- print('hello')
54-
+ print('hello world')
55-
+ return True
56-
'''.replace(' ', '')
57-
result = parse_diff(sample)
58-
assert result['total_files'] == 1
59-
assert result['total_additions'] == 2
60-
assert result['total_deletions'] == 1
61-
print('parse_diff: OK')
47+
- name: Functional test - git data collection
48+
run: python -c "from git_diff.git_data import get_repo_root, collect_all_data, parse_diff; root = get_repo_root('.'); print('Root:', root); result = parse_diff('diff --git a/f.py b/f.py\nindex a..b 100644\n--- a/f.py\n+++ b/f.py\n@@ -1,2 +1,3 @@\n line1\n-old\n+new1\n+new2\n'); assert result['total_files']==1; assert result['total_additions']==2; assert result['total_deletions']==1; print('parse_diff: OK'); data = collect_all_data(root); assert 'repo' in data; print('Repo:', data['repo']['name']); print('Commits:', len(data['commits'])); print('Files:', len(data['file_tree'])); print('All tests passed!')"
6249

63-
# Collect full data from this repo
64-
data = collect_all_data(root)
65-
assert 'repo' in data
66-
assert 'commits' in data
67-
print('Repo:', data['repo']['name'])
68-
print('Commits:', len(data['commits']))
69-
print('Files:', len(data['file_tree']))
70-
print('All tests passed!')
71-
"
50+
- name: Functional test - server imports
51+
run: python -c "from git_diff.server import find_free_port, GitDiffHandler; port = find_free_port(); print('Free port:', port); assert 7433 <= port <= 7500; print('Server imports: OK')"
7252

73-
- name: Test server imports
74-
run: |
75-
python -c "
76-
from git_diff.server import find_free_port, GitDiffHandler
77-
port = find_free_port()
78-
print('Free port found:', port)
79-
assert 7433 <= port <= 7500
80-
print('Server imports: OK')
81-
"
53+
- name: Functional test - diff edge cases
54+
run: python -c "from git_diff.git_data import parse_diff; r=parse_diff(''); assert r['total_files']==0; r=parse_diff('diff --git a/n.py b/n.py\nnew file mode 100644\nindex 0000000..abc\n--- /dev/null\n+++ b/n.py\n@@ -0,0 +1,2 @@\n+a\n+b\n'); assert r['files'][0]['is_new']==True; assert r['total_additions']==2; print('Edge cases OK')"

git_diff/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""git-diff Beautiful git viewer in your browser. Like GitHub, but local."""
1+
"""git-diff -- Beautiful git viewer in your browser. Like GitHub, but local."""
22

33
__version__ = "0.1.0"
44
__author__ = "Ankit Chaubey"

git_diff/cli.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
cli.py Command-line interface for git-diff.
2+
cli.py - Command-line interface for git-diff.
33
"""
44
import argparse
55
import os
@@ -18,7 +18,7 @@
1818
def main():
1919
parser = argparse.ArgumentParser(
2020
prog="git-diff",
21-
description="Beautiful git diff viewer in your browser like GitHub, but local.",
21+
description="Beautiful git diff viewer in your browser -- like GitHub, but local.",
2222
formatter_class=argparse.RawDescriptionHelpFormatter,
2323
epilog="""
2424
Examples:
@@ -61,18 +61,18 @@ def main():
6161
repo_path = args.path or os.getcwd()
6262
repo_root = get_repo_root(repo_path)
6363
except RuntimeError as e:
64-
print(f" ❌ Error: {e}\n", file=sys.stderr)
64+
print(f" ERROR: {e}\n", file=sys.stderr)
6565
sys.exit(1)
6666

6767
repo_name = os.path.basename(repo_root)
68-
print(f" 📂 Repository: {repo_name} ({repo_root})\n")
68+
print(f" Repo: {repo_name} ({repo_root})\n")
6969

7070
try:
71-
print(" 🔄 Collecting repository data...")
71+
print(" Collecting repository data...")
7272
data = collect_all_data(repo_root)
73-
print(f"\n Ready! {data['repo']['total_commits']} commits · {len(data['file_tree'])} files · {len(data['repo']['contributors'])} contributors\n")
73+
print(f"\n Ready! {data['repo']['total_commits']} commits, {len(data['file_tree'])} files, {len(data['repo']['contributors'])} contributors\n")
7474
except Exception as e:
75-
print(f" ❌ Failed to collect git data: {e}\n", file=sys.stderr)
75+
print(f" FAILED: Could not collect git data: {e}\n", file=sys.stderr)
7676
sys.exit(1)
7777

7878
start_server(repo_root, data, port=args.port, no_browser=args.no_browser, host=args.host)

git_diff/git_data.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
git_data.py Comprehensive git repository data collection for git-diff.
2+
git_data.py -- Comprehensive git repository data collection for git-diff.
33
Collects everything: commits, diffs, branches, tags, stashes, stats, blame, log graphs.
44
"""
55
import subprocess
@@ -717,34 +717,34 @@ def get_all_refs(repo_root):
717717

718718
def collect_all_data(repo_root):
719719
"""Collect all repository data for initial page load."""
720-
print(" 📦 Collecting repository metadata...")
720+
print(" [1/10] Collecting repository metadata...")
721721
repo_info = get_repo_info(repo_root)
722722

723-
print(" 📋 Collecting commit history (up to 500)...")
723+
print(" [2/10] Collecting commit history (up to 500)...")
724724
commits = get_commit_history(repo_root, limit=500)
725725

726-
print(" 🔍 Collecting working tree status...")
726+
print(" [3/10] Collecting working tree status...")
727727
status = get_status(repo_root)
728728

729-
print(" 📝 Collecting staged diff...")
729+
print(" [4/10] Collecting staged diff...")
730730
staged_diff = get_staged_diff(repo_root)
731731

732-
print(" ✏️ Collecting unstaged diff...")
732+
print(" [5/10] Collecting unstaged diff...")
733733
unstaged_diff = get_unstaged_diff(repo_root)
734734

735-
print(" 🌿 Collecting file tree...")
735+
print(" [6/10] Collecting file tree...")
736736
file_tree = get_file_tree(repo_root)
737737

738-
print(" 📦 Collecting stashes...")
738+
print(" [7/10] Collecting stashes...")
739739
stashes = get_stashes(repo_root)
740740

741-
print(" 📊 Collecting commit stats (90 days)...")
741+
print(" [8/10] Collecting commit stats (90 days)...")
742742
commit_stats = get_commit_stats_by_day(repo_root, days=90)
743743

744-
print(" 🔤 Collecting language stats...")
744+
print(" [9/10] Collecting language stats...")
745745
lang_stats = get_language_stats(repo_root)
746746

747-
print(" 🔖 Collecting all refs...")
747+
print(" [10/10] Collecting all refs...")
748748
all_refs = get_all_refs(repo_root)
749749

750750
return {

git_diff/server.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
2-
server.py Lightweight HTTP server and REST API for git-diff.
3-
No external dependencies pure Python stdlib only.
2+
server.py -- Lightweight HTTP server and REST API for git-diff.
3+
No external dependencies -- pure Python stdlib only.
44
"""
55
import json
66
import os
@@ -90,18 +90,18 @@ def pi(key, default=0):
9090
def _route(self, path, p, pi):
9191
root = self.repo_root
9292

93-
# ── Serve the SPA ──────────────────────────────────────────────
93+
# -- Serve the SPA ----------------------------------------------
9494
if path in ("/", "/index.html"):
9595
html = TEMPLATE_PATH.read_text(encoding="utf-8")
9696
self.send_html(html)
9797
return
9898

99-
# ── Initial bundle ─────────────────────────────────────────────
99+
# -- Initial bundle ---------------------------------------------
100100
if path == "/api/data":
101101
self.send_json(self.initial_data)
102102
return
103103

104-
# ── Commit detail + diff ───────────────────────────────────────
104+
# -- Commit detail + diff ---------------------------------------
105105
if path == "/api/commit":
106106
h = p("hash")
107107
if not h:
@@ -113,7 +113,7 @@ def _route(self, path, p, pi):
113113
self.send_json({"diff": diff, "detail": detail})
114114
return
115115

116-
# ── Paginated commit history ───────────────────────────────────
116+
# -- Paginated commit history -----------------------------------
117117
if path == "/api/commits":
118118
branch = p("branch", "HEAD")
119119
limit = pi("limit", 100)
@@ -125,7 +125,7 @@ def _route(self, path, p, pi):
125125
self.send_json({"commits": commits[offset:offset + limit], "total": len(commits)})
126126
return
127127

128-
# ── Range diff: branch-to-branch or SHA-to-SHA ────────────────
128+
# -- Range diff: branch-to-branch or SHA-to-SHA ----------------
129129
if path == "/api/range-diff":
130130
base = p("base")
131131
compare = p("compare")
@@ -150,26 +150,26 @@ def _route(self, path, p, pi):
150150
self.send_json({"diff": diff, "commits": range_commits, "base": base, "compare": compare})
151151
return
152152

153-
# ── Staged diff ───────────────────────────────────────────────
153+
# -- Staged diff -----------------------------------------------
154154
if path == "/api/staged":
155155
ctx = pi("context", 3)
156156
self.send_json(get_staged_diff(root, context=ctx))
157157
return
158158

159-
# ── Unstaged diff ─────────────────────────────────────────────
159+
# -- Unstaged diff ---------------------------------------------
160160
if path == "/api/unstaged":
161161
ctx = pi("context", 3)
162162
self.send_json(get_unstaged_diff(root, context=ctx))
163163
return
164164

165-
# ── Stash diff ────────────────────────────────────────────────
165+
# -- Stash diff ------------------------------------------------
166166
if path == "/api/stash":
167167
ref = p("ref", "stash@{0}")
168168
ctx = pi("context", 3)
169169
self.send_json({"diff": get_stash_diff(root, ref, context=ctx)})
170170
return
171171

172-
# ── File content at ref ───────────────────────────────────────
172+
# -- File content at ref ---------------------------------------
173173
if path == "/api/file":
174174
fp = p("path")
175175
ref = p("ref", "HEAD")
@@ -179,7 +179,7 @@ def _route(self, path, p, pi):
179179
self.send_json(get_file_content(root, fp, ref))
180180
return
181181

182-
# ── File commit history ───────────────────────────────────────
182+
# -- File commit history ---------------------------------------
183183
if path == "/api/file-log":
184184
fp = p("path")
185185
limit = pi("limit", 50)
@@ -189,7 +189,7 @@ def _route(self, path, p, pi):
189189
self.send_json({"commits": get_file_log(root, fp, limit=limit)})
190190
return
191191

192-
# ── File blame ────────────────────────────────────────────────
192+
# -- File blame ------------------------------------------------
193193
if path == "/api/blame":
194194
fp = p("path")
195195
ref = p("ref", "HEAD")
@@ -199,25 +199,25 @@ def _route(self, path, p, pi):
199199
self.send_json({"blame": get_file_blame(root, fp, ref)})
200200
return
201201

202-
# ── Commit activity chart ─────────────────────────────────────
202+
# -- Commit activity chart -------------------------------------
203203
if path == "/api/activity":
204204
days = pi("days", 90)
205205
self.send_json({"data": get_commit_stats_by_day(root, days=days)})
206206
return
207207

208-
# ── Language stats ────────────────────────────────────────────
208+
# -- Language stats --------------------------------------------
209209
if path == "/api/langs":
210210
self.send_json({"data": get_language_stats(root)})
211211
return
212212

213-
# ── Full refresh ──────────────────────────────────────────────
213+
# -- Full refresh ----------------------------------------------
214214
if path == "/api/refresh":
215215
data = collect_all_data(root)
216216
GitDiffHandler.initial_data = data
217217
self.send_json({"status": "ok", "timestamp": int(time.time())})
218218
return
219219

220-
# ── Raw git command (safe read-only subset) ───────────────────
220+
# -- Raw git command (safe read-only subset) -------------------
221221
if path == "/api/git":
222222
cmd = p("cmd")
223223
SAFE = {"log", "show", "diff", "blame", "ls-tree", "ls-files",
@@ -258,8 +258,8 @@ def start_server(repo_root: str, data: dict, port: int = None, no_browser: bool
258258
server = HTTPServer((host, port), GitDiffHandler)
259259
url = f"http://{host}:{port}"
260260

261-
print(f"\n 🌐 Server {url}")
262-
print(f" 📁 Repo {repo_root}")
261+
print(f"\n Server -> {url}")
262+
print(f" Repo -> {repo_root}")
263263
print(f"\n Press Ctrl+C to stop\n")
264264

265265
if not no_browser:
@@ -271,6 +271,6 @@ def _open():
271271
try:
272272
server.serve_forever()
273273
except KeyboardInterrupt:
274-
print("\n 👋 git-diff stopped. Have a good one!\n")
274+
print("\n git-diff stopped. Have a good one!\n")
275275
finally:
276276
server.server_close()

0 commit comments

Comments
 (0)