Skip to content

Commit a03381e

Browse files
authored
fix: UnicodeEncodeError on Windows and add py314 (#126)
* chore: update test.yml as python 3.14 is out * chore: Update test.yml to pin windows to windows-2022 * fix: Unicode decoding error and wc command * fix: 'wc' is not recognized as an internal or external command * docs: revert changes to readme * ci: add more version test on windows and macos
1 parent 232af9d commit a03381e

File tree

5 files changed

+90
-42
lines changed

5 files changed

+90
-42
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ jobs:
2828
nox -s docs
2929
3030
test-windows:
31-
runs-on: windows-latest
31+
runs-on: windows-2022
3232
strategy:
3333
matrix:
34-
python-version: ["3.9"] # just test on one version to save time
34+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
3535

3636
steps:
3737
- name: Checkout Code
@@ -69,7 +69,7 @@ jobs:
6969
runs-on: macos-latest
7070
strategy:
7171
matrix:
72-
python-version: ["3.9"] # just test on one version to save time
72+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
7373

7474
steps:
7575
- name: Checkout Code
@@ -108,7 +108,7 @@ jobs:
108108
pull-requests: write
109109
strategy:
110110
matrix:
111-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-beta.1"]
111+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
112112

113113
steps:
114114
- name: Checkout Code

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ gitstats.egg-info
66
gitstory.egg-info
77
build/
88
gitstats-report
9-
test-report
10-
test-report.json
9+
test-report*
10+
test-report*.json
1111
docs/build

gitstats/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ def run(gitpath, outputpath, extra_fmt=None) -> int:
723723
import json
724724

725725
print(f'Generating JSON file: "{output_file}"')
726-
with open(output_file, "w") as file:
726+
with open(output_file, "w", encoding="utf-8") as file:
727727
json.dump(data.__dict__, file, default=str)
728728
else:
729729
print(f"Error: Unsupported format '{extra_fmt}'")

gitstats/report_creator.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def create(self, data, path):
5858
self.create_graphs(path)
5959

6060
def create_index_html(self, data, path):
61-
f = open(path + "/index.html", "w")
61+
f = open(path + "/index.html", "w", encoding="utf-8")
6262
format = "%Y-%m-%d %H:%M:%S"
6363
self.print_header(f)
6464

@@ -124,7 +124,7 @@ def create_index_html(self, data, path):
124124
def create_activity_html(self, data, path):
125125
###
126126
# Activity
127-
f = open(path + "/activity.html", "w")
127+
f = open(path + "/activity.html", "w", encoding="utf-8")
128128
self.print_header(f)
129129
f.write("<h1>Activity</h1>")
130130
self.print_nav(f)
@@ -179,7 +179,7 @@ def create_activity_html(self, data, path):
179179
for i in range(0, 24):
180180
f.write("<th>%d</th>" % i)
181181
f.write("</tr>\n<tr><th>Commits</th>")
182-
fp = open(path + "/hour_of_day.dat", "w")
182+
fp = open(path + "/hour_of_day.dat", "w", encoding="utf-8")
183183
for i in range(0, 24):
184184
if i in hour_of_day:
185185
r = 127 + int(
@@ -209,7 +209,7 @@ def create_activity_html(self, data, path):
209209
f.write("<td>0.00</td>")
210210
f.write("</tr></table>")
211211
f.write('<img src="hour_of_day.png" alt="Hour of Day">')
212-
fg = open(path + "/hour_of_day.dat", "w")
212+
fg = open(path + "/hour_of_day.dat", "w", encoding="utf-8")
213213
for i in range(0, 24):
214214
if i in hour_of_day:
215215
fg.write("%d %d\n" % (i + 1, hour_of_day[i]))
@@ -222,7 +222,7 @@ def create_activity_html(self, data, path):
222222
day_of_week = data.get_activity_by_day_of_week()
223223
f.write('<div class="vtable"><table>')
224224
f.write("<tr><th>Day</th><th>Total (%)</th></tr>")
225-
fp = open(path + "/day_of_week.dat", "w")
225+
fp = open(path + "/day_of_week.dat", "w", encoding="utf-8")
226226
for d in range(0, 7):
227227
commits = 0
228228
if d in day_of_week:
@@ -275,7 +275,7 @@ def create_activity_html(self, data, path):
275275
f.write(html_header(2, "Month of Year"))
276276
f.write('<div class="vtable"><table>')
277277
f.write("<tr><th>Month</th><th>Commits (%)</th></tr>")
278-
fp = open(path + "/month_of_year.dat", "w")
278+
fp = open(path + "/month_of_year.dat", "w", encoding="utf-8")
279279
for mm in range(1, 13):
280280
commits = 0
281281
if mm in data.activity_by_month_of_year:
@@ -306,7 +306,7 @@ def create_activity_html(self, data, path):
306306
)
307307
f.write("</table></div>")
308308
f.write('<img src="commits_by_year_month.png" alt="Commits by year/month">')
309-
fg = open(path + "/commits_by_year_month.dat", "w")
309+
fg = open(path + "/commits_by_year_month.dat", "w", encoding="utf-8")
310310
for yymm in sorted(data.commits_by_month.keys()):
311311
fg.write("%s %s\n" % (yymm, data.commits_by_month[yymm]))
312312
fg.close()
@@ -330,7 +330,7 @@ def create_activity_html(self, data, path):
330330
)
331331
f.write("</table></div>")
332332
f.write('<img src="commits_by_year.png" alt="Commits by Year">')
333-
fg = open(path + "/commits_by_year.dat", "w")
333+
fg = open(path + "/commits_by_year.dat", "w", encoding="utf-8")
334334
for yy in sorted(data.commits_by_year.keys()):
335335
fg.write("%d %d\n" % (yy, data.commits_by_year[yy]))
336336
fg.close()
@@ -356,7 +356,7 @@ def create_activity_html(self, data, path):
356356
def create_authors_html(self, data, path):
357357
###
358358
# Authors
359-
f = open(path + "/authors.html", "w")
359+
f = open(path + "/authors.html", "w", encoding="utf-8")
360360
self.print_header(f)
361361

362362
f.write("<h1>Authors</h1>")
@@ -414,8 +414,8 @@ def create_authors_html(self, data, path):
414414
% conf["max_authors"]
415415
)
416416

417-
fgl = open(path + "/lines_of_code_by_author.dat", "w")
418-
fgc = open(path + "/commits_by_author.dat", "w")
417+
fgl = open(path + "/lines_of_code_by_author.dat", "w", encoding="utf-8")
418+
fgc = open(path + "/commits_by_author.dat", "w", encoding="utf-8")
419419

420420
lines_by_authors = {} # cumulated added lines by
421421
# author. to save memory,
@@ -509,7 +509,7 @@ def create_authors_html(self, data, path):
509509
domains_by_commits.reverse() # most first
510510
f.write('<div class="vtable"><table>')
511511
f.write("<tr><th>Domains</th><th>Total (%)</th></tr>")
512-
fp = open(path + "/domains.dat", "w")
512+
fp = open(path + "/domains.dat", "w", encoding="utf-8")
513513
n = 0
514514
for domain in domains_by_commits:
515515
if n == conf["max_domains"]:
@@ -536,7 +536,7 @@ def create_authors_html(self, data, path):
536536
def create_files_html(self, data, path):
537537
###
538538
# Files
539-
f = open(path + "/files.html", "w")
539+
f = open(path + "/files.html", "w", encoding="utf-8")
540540
self.print_header(f)
541541
f.write("<h1>Files</h1>")
542542
self.print_nav(f)
@@ -567,7 +567,7 @@ def create_files_html(self, data, path):
567567
)
568568
)
569569

570-
fg = open(path + "/files_by_date.dat", "w")
570+
fg = open(path + "/files_by_date.dat", "w", encoding="utf-8")
571571
for line in sorted(list(files_by_date)):
572572
fg.write("%s\n" % line)
573573
# for stamp in sorted(data.files_by_stamp.keys()):
@@ -610,7 +610,7 @@ def create_files_html(self, data, path):
610610
def create_lines_html(self, data, path):
611611
###
612612
# Lines
613-
f = open(path + "/lines.html", "w")
613+
f = open(path + "/lines.html", "w", encoding="utf-8")
614614
self.print_header(f)
615615
f.write("<h1>Lines</h1>")
616616
self.print_nav(f)
@@ -622,7 +622,7 @@ def create_lines_html(self, data, path):
622622
f.write(html_header(2, "Lines of Code"))
623623
f.write('<img src="lines_of_code.png" alt="Lines of Code">')
624624

625-
fg = open(path + "/lines_of_code.dat", "w")
625+
fg = open(path + "/lines_of_code.dat", "w", encoding="utf-8")
626626
for stamp in sorted(data.changes_by_date.keys()):
627627
fg.write("%d %d\n" % (stamp, data.changes_by_date[stamp]["lines"]))
628628
fg.close()
@@ -633,7 +633,7 @@ def create_lines_html(self, data, path):
633633
def create_tags_html(self, data, path):
634634
###
635635
# tags.html
636-
f = open(path + "/tags.html", "w")
636+
f = open(path + "/tags.html", "w", encoding="utf-8")
637637
self.print_header(f)
638638
f.write("<h1>Tags</h1>")
639639
self.print_nav(f)
@@ -693,7 +693,7 @@ def create_graphs(self, path):
693693

694694
def create_graph_hour_of_day(self, path):
695695
# hour of day
696-
f = open(path + "/hour_of_day.plot", "w")
696+
f = open(path + "/hour_of_day.plot", "w", encoding="utf-8")
697697
f.write(GNUPLOT_COMMON)
698698
f.write(
699699
"""
@@ -711,7 +711,7 @@ def create_graph_hour_of_day(self, path):
711711

712712
def create_graph_day_of_week(self, path):
713713
# day of week
714-
f = open(path + "/day_of_week.plot", "w")
714+
f = open(path + "/day_of_week.plot", "w", encoding="utf-8")
715715
f.write(GNUPLOT_COMMON)
716716
f.write(
717717
"""
@@ -729,7 +729,7 @@ def create_graph_day_of_week(self, path):
729729

730730
def create_graph_domains(self, path):
731731
# Domains
732-
f = open(path + "/domains.plot", "w")
732+
f = open(path + "/domains.plot", "w", encoding="utf-8")
733733
f.write(GNUPLOT_COMMON)
734734
f.write(
735735
"""
@@ -746,7 +746,7 @@ def create_graph_domains(self, path):
746746

747747
def create_graph_month_of_year(self, path):
748748
# Month of Year
749-
f = open(path + "/month_of_year.plot", "w")
749+
f = open(path + "/month_of_year.plot", "w", encoding="utf-8")
750750
f.write(GNUPLOT_COMMON)
751751
f.write(
752752
"""
@@ -764,7 +764,7 @@ def create_graph_month_of_year(self, path):
764764

765765
def create_graph_commits_by_year_month(self, path):
766766
# commits_by_year_month
767-
f = open(path + "/commits_by_year_month.plot", "w")
767+
f = open(path + "/commits_by_year_month.plot", "w", encoding="utf-8")
768768
f.write(GNUPLOT_COMMON)
769769
f.write(
770770
"""
@@ -785,7 +785,7 @@ def create_graph_commits_by_year_month(self, path):
785785

786786
def create_graph_commits_by_year(self, path):
787787
# commits_by_year
788-
f = open(path + "/commits_by_year.plot", "w")
788+
f = open(path + "/commits_by_year.plot", "w", encoding="utf-8")
789789
f.write(GNUPLOT_COMMON)
790790
f.write(
791791
"""
@@ -803,7 +803,7 @@ def create_graph_commits_by_year(self, path):
803803

804804
def create_graph_files_by_date(self, path):
805805
# Files by date
806-
f = open(path + "/files_by_date.plot", "w")
806+
f = open(path + "/files_by_date.plot", "w", encoding="utf-8")
807807
f.write(GNUPLOT_COMMON)
808808
f.write(
809809
"""
@@ -825,7 +825,7 @@ def create_graph_files_by_date(self, path):
825825

826826
def create_graph_lines_of_code(self, path):
827827
# Lines of Code
828-
f = open(path + "/lines_of_code.plot", "w")
828+
f = open(path + "/lines_of_code.plot", "w", encoding="utf-8")
829829
f.write(GNUPLOT_COMMON)
830830
f.write(
831831
"""
@@ -846,7 +846,7 @@ def create_graph_lines_of_code(self, path):
846846

847847
def create_graph_lines_of_code_by_author(self, path):
848848
# Lines of Code Added per author
849-
f = open(path + "/lines_of_code_by_author.plot", "w")
849+
f = open(path + "/lines_of_code_by_author.plot", "w", encoding="utf-8")
850850
f.write(GNUPLOT_COMMON)
851851
f.write(
852852
"""
@@ -879,7 +879,7 @@ def create_graph_lines_of_code_by_author(self, path):
879879

880880
def create_graph_commits_by_author(self, path):
881881
# Commits per author
882-
f = open(path + "/commits_by_author.plot", "w")
882+
f = open(path + "/commits_by_author.plot", "w", encoding="utf-8")
883883
f.write(GNUPLOT_COMMON)
884884
f.write(
885885
"""

gitstats/utils.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
conf = load_config()
1515

1616

17+
def count_lines_in_text(text):
18+
"""Cross-platform function to count lines in text"""
19+
if not text or not text.strip():
20+
return 0
21+
return len(text.strip().split("\n"))
22+
23+
24+
def filter_lines_by_pattern(text, pattern):
25+
"""Filter out lines matching a pattern (cross-platform grep -v replacement)"""
26+
if not text or not text.strip():
27+
return ""
28+
lines = text.split("\n")
29+
filtered_lines = [line for line in lines if not re.match(pattern, line)]
30+
return "\n".join(filtered_lines)
31+
32+
1733
def get_version():
1834
return version("gitstats")
1935

@@ -33,21 +49,53 @@ def get_pipe_output(cmds, quiet=False):
3349
if not quiet and ON_LINUX and os.isatty(1):
3450
print(">> " + " | ".join(cmds), end=" ")
3551
sys.stdout.flush()
36-
p = subprocess.Popen(cmds[0], stdout=subprocess.PIPE, shell=True)
37-
processes = [p]
38-
for x in cmds[1:]:
39-
p = subprocess.Popen(x, stdin=p.stdout, stdout=subprocess.PIPE, shell=True)
40-
processes.append(p)
41-
output = p.communicate()[0]
42-
for p in processes:
52+
53+
# Handle cross-platform cases
54+
if len(cmds) == 2 and cmds[1] == "wc -l":
55+
# Handle line counting cross-platform
56+
p = subprocess.Popen(cmds[0], stdout=subprocess.PIPE, shell=True)
57+
output = p.communicate()[0]
58+
p.wait()
59+
try:
60+
text = output.decode("utf-8", errors="replace").rstrip("\n")
61+
except UnicodeDecodeError:
62+
# Fallback for binary files
63+
text = output.decode("latin-1", errors="replace").rstrip("\n")
64+
line_count = count_lines_in_text(text)
65+
result = str(line_count)
66+
elif len(cmds) == 2 and cmds[1].startswith("grep -v"):
67+
# Handle grep -v cross-platform
68+
pattern = cmds[1].split("grep -v ")[1]
69+
p = subprocess.Popen(cmds[0], stdout=subprocess.PIPE, shell=True)
70+
output = p.communicate()[0]
4371
p.wait()
72+
try:
73+
text = output.decode("utf-8", errors="replace").rstrip("\n")
74+
except UnicodeDecodeError:
75+
text = output.decode("latin-1", errors="replace").rstrip("\n")
76+
result = filter_lines_by_pattern(text, pattern)
77+
else:
78+
# Standard pipe behavior for other cases
79+
p = subprocess.Popen(cmds[0], stdout=subprocess.PIPE, shell=True)
80+
processes = [p]
81+
for x in cmds[1:]:
82+
p = subprocess.Popen(x, stdin=p.stdout, stdout=subprocess.PIPE, shell=True)
83+
processes.append(p)
84+
output = p.communicate()[0]
85+
for p in processes:
86+
p.wait()
87+
try:
88+
result = output.decode("utf-8", errors="replace").rstrip("\n")
89+
except UnicodeDecodeError:
90+
result = output.decode("latin-1", errors="replace").rstrip("\n")
91+
4492
end = time.time()
4593
if not quiet:
4694
if ON_LINUX and os.isatty(1):
4795
print("\r", end=" ")
4896
print("[%.5f] >> %s" % (end - start, " | ".join(cmds)))
4997
exectime_external += end - start
50-
return output.decode("utf-8").rstrip("\n")
98+
return result
5199

52100

53101
def get_commit_range(defaultrange="HEAD", end_only=False):

0 commit comments

Comments
 (0)