Skip to content

Commit 2fa475e

Browse files
authored
Merge pull request #13 from git-quick-stats/3-let-users-ignore-authors
Allow users to ignore authors
2 parents 18fdc98 + 93bbe55 commit 2fa475e

16 files changed

+193
-80
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ tags
2626
# macOS-related
2727
.DS_Store
2828

29+
# Python-based virtual environment I use for publishing
30+
.venv-publish
31+
2932
# Shamelessly taken from here because I'm lazy:
3033
# https://github.com/github/gitignore/blob/main/Python.gitignore
3134

MANIFEST.in

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
include LICENSE
22
include README.md
3-
include TODO.md
3+
include CODE_OF_CONDUCT.md
4+
include CONTRIBUTING.md
45
recursive-include git_py_stats *.py
5-
recursive-include man *
6-
global-exclude *.pyc __pycache__/
6+
recursive-include man *.1
7+
global-exclude *.pyc __pycache__/ *.py[cod]
78
prune git_py_stats/tests
89

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,22 @@ export _GIT_MERGE_VIEW="exclusive"
307307
### Git Branch
308308
309309
You can set the variable `_GIT_BRANCH` to set the branch of the stats.
310-
Works with commands `--git-stats-by-branch` and `--csv-output-by-branch`.
310+
Works with command `--csv-output-by-branch` only currently.
311311
312312
```bash
313313
export _GIT_BRANCH="master"
314314
```
315315
316+
### Ignore Authors
317+
318+
You can set the variable `_GIT_IGNORE_AUTHORS` to filter out specific
319+
authors. It will currently work with the "Code reviewers", "New contributors",
320+
"All branches", and "Output daily stats by branch in CSV format" options.
321+
322+
```bash
323+
export _GIT_IGNORE_AUTHORS="(author@examle.com|username)"
324+
```
325+
316326
### Sorting Contribution Stats
317327
318328
You can sort contribution stats by field `name`, `commits`, `insertions`,

TODO.md

Lines changed: 0 additions & 26 deletions
This file was deleted.

git_py_stats/config.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,34 @@
33
"""
44

55
import os
6+
import re
67
from datetime import datetime
7-
from typing import Dict, Union, Optional
8+
from typing import Dict, Union, Optional, Callable
89
from git_py_stats.git_operations import run_git_command
910

1011

12+
def _build_author_exclusion_filter(pattern: str) -> Callable[[str], bool]:
13+
"""
14+
Compile a string of authors that tells you whether an author
15+
should be ignored based on a user-configured environment
16+
variable.
17+
18+
Args:
19+
pattern (str): A regex (Example: "(user@example.com|Some User)").
20+
No flags are injected automatically, but users can
21+
include them for case-insensitive matches.
22+
23+
Returns:
24+
Callable[[str], bool]: Input string 's' that matches the pattern to be
25+
ignored. False otherwise.
26+
"""
27+
pattern = (pattern or "").strip()
28+
if not pattern:
29+
return lambda _s: False
30+
rx = re.compile(pattern)
31+
return lambda s: bool(rx.search(s or ""))
32+
33+
1134
def _parse_git_sort_by(raw: str) -> tuple[str, str]:
1235
"""
1336
Helper function for handling sorting features for contribution stats.
@@ -81,11 +104,14 @@ def get_config() -> Dict[str, Union[str, int]]:
81104
- 'enable' to use the user's default merge view from the conf.
82105
Default is usually to show both regular and merge commits.
83106
- Any other value defaults to '--no-merges' currently.
107+
_GIT_BRANCH (str): Sets branch you want to target for some stats.
108+
Default is empty which falls back to the current branch you're on.
84109
_GIT_LIMIT (int): Limits the git log output. Defaults to 10.
85110
_GIT_LOG_OPTIONS (str): Additional git log options. Default is empty.
86111
_GIT_DAYS (int): Defines number of days for the heatmap. Default is empty.
87112
_GIT_SORT_BY (str): Defines sort metric and direction for contribution stats.
88113
Default is name-asc.
114+
_GIT_IGNORE_AUTHORS (str): Defines authors to ignore. Default is empty.
89115
_MENU_THEME (str): Toggles between the default theme and legacy theme.
90116
- 'legacy' to set the legacy theme
91117
- 'none' to disable the menu theme
@@ -100,8 +126,12 @@ def get_config() -> Dict[str, Union[str, int]]:
100126
- 'until' (str): Git command option for the end date.
101127
- 'pathspec' (str): Git command option for pathspec.
102128
- 'merges' (str): Git command option for merge commit view strategy.
129+
- 'branch' (str): Git branch name.
103130
- 'limit' (int): Git log output limit.
104131
- 'log_options' (str): Additional git log options.
132+
- 'days' (str): Number of days for the heatmap.
133+
- 'sort_by' (str): Sort by field and sort direction (asc/desc).
134+
- 'ignore_authors': (str): Any author(s) to ignore.
105135
- 'menu_theme' (str): Menu theme color.
106136
"""
107137
config: Dict[str, Union[str, int]] = {}
@@ -146,6 +176,13 @@ def get_config() -> Dict[str, Union[str, int]]:
146176
else:
147177
config["merges"] = "--no-merges"
148178

179+
# _GIT_BRANCH
180+
git_branch: Optional[str] = os.environ.get("_GIT_BRANCH")
181+
if git_branch:
182+
config["branch"] = git_branch
183+
else:
184+
config["branch"] = ""
185+
149186
# _GIT_LIMIT
150187
git_limit: Optional[str] = os.environ.get("_GIT_LIMIT")
151188
if git_limit:
@@ -184,6 +221,10 @@ def get_config() -> Dict[str, Union[str, int]]:
184221
config["sort_by"] = sort_by
185222
config["sort_dir"] = sort_dir
186223

224+
# _GIT_IGNORE_AUTHORS
225+
ignore_authors_pattern: Optional[str] = os.environ.get("_GIT_IGNORE_AUTHORS")
226+
config["ignore_authors"] = _build_author_exclusion_filter(ignore_authors_pattern)
227+
187228
# _MENU_THEME
188229
menu_theme: Optional[str] = os.environ.get("_MENU_THEME")
189230
if menu_theme == "legacy":

git_py_stats/generate_cmds.py

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,11 @@ def output_daily_stats_csv(config: Dict[str, Union[str, int]]) -> None:
451451
until = config.get("until", "")
452452
log_options = config.get("log_options", "")
453453
pathspec = config.get("pathspec", "")
454+
branch = config.get("branch", "")
455+
ignore_authors = config.get("ignore_authors", lambda _s: False)
454456

455-
branch = input("Enter branch name (leave empty for current branch): ")
457+
if not branch:
458+
branch = input("Enter branch name (leave empty for current branch): ")
456459

457460
# Original command:
458461
# git -c log.showSignature=false log ${_branch} --use-mailmap $_merges --numstat \
@@ -478,22 +481,70 @@ def output_daily_stats_csv(config: Dict[str, Union[str, int]]) -> None:
478481
cmd = [arg for arg in cmd if arg]
479482

480483
output = run_git_command(cmd)
481-
if output:
482-
dates = output.split("\n")
483-
counter = collections.Counter(dates)
484-
filename = "daily_stats.csv"
485-
try:
486-
with open(filename, "w", newline="") as csvfile:
487-
fieldnames = ["Date", "Commits"]
488-
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
489-
writer.writeheader()
490-
for date, count in sorted(counter.items()):
491-
writer.writerow({"Date": date, "Commits": count})
492-
print(f"Daily stats saved to {filename}")
493-
except IOError as e:
494-
print(f"Failed to write to {filename}: {e}")
495-
else:
484+
485+
# Exit early if no output valid
486+
if not output:
496487
print("No data available.")
488+
return
489+
490+
# NOTE: This has to be expanded to handle the new ability to ignore
491+
# authors, but there might be a better way to handle this...
492+
kept_lines = []
493+
current_block = []
494+
current_ignored = False
495+
have_seen_author = False
496+
497+
for line in output.splitlines():
498+
# New commit starts
499+
if line.startswith("commit "):
500+
# Flush the previous block
501+
if current_block and not current_ignored:
502+
kept_lines.extend(current_block)
503+
# Reset for the next block
504+
current_block = [line]
505+
current_ignored = False
506+
have_seen_author = False
507+
continue
508+
509+
# Only check author once per block
510+
if not have_seen_author and line.startswith("Author: "):
511+
author_line = line[len("Author: ") :].strip()
512+
name = author_line
513+
email = ""
514+
if "<" in author_line and ">" in author_line:
515+
name = author_line.split("<", 1)[0].strip()
516+
email = author_line.split("<", 1)[1].split(">", 1)[0].strip()
517+
518+
# If any form matches (name or email), drop the whole block
519+
if (
520+
ignore_authors(author_line)
521+
or ignore_authors(name)
522+
or (email and ignore_authors(email))
523+
):
524+
current_ignored = True
525+
have_seen_author = True
526+
current_block.append(line)
527+
528+
# Flush the last block
529+
if current_block and not current_ignored:
530+
kept_lines.extend(current_block)
531+
532+
# Found nothing worth keeping? Just exit then
533+
if not kept_lines:
534+
print("No data available.")
535+
return
536+
537+
counter = collections.Counter(kept_lines)
538+
filename = "git_daily_stats.csv"
539+
try:
540+
with open(filename, "w", newline="") as csvfile:
541+
writer = csv.DictWriter(csvfile, fieldnames=["Date", "Commits"])
542+
writer.writeheader()
543+
for text, count in sorted(counter.items()):
544+
writer.writerow({"Date": text, "Commits": count})
545+
print(f"Daily stats saved to {filename}")
546+
except IOError as e:
547+
print(f"Failed to write to {filename}: {e}")
497548

498549

499550
# TODO: This doesn't match the original functionality as it uses some pretty

git_py_stats/interactive_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def handle_interactive_mode(config: Dict[str, Union[str, int]]) -> None:
3030
"6": lambda: generate_cmds.output_daily_stats_csv(config),
3131
"7": lambda: generate_cmds.save_git_log_output_json(config),
3232
"8": lambda: list_cmds.branch_tree(config),
33-
"9": list_cmds.branches_by_date,
33+
"9": lambda: list_cmds.branches_by_date(config),
3434
"10": lambda: list_cmds.contributors(config),
3535
"11": lambda: list_cmds.new_contributors(config, input("Enter cutoff date (YYYY-MM-DD): ")),
3636
"12": lambda: list_cmds.git_commits_per_author(config),

git_py_stats/list_cmds.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,32 @@ def branch_tree(config: Dict[str, Union[str, int]]) -> None:
7979
print("No data available.")
8080

8181

82-
def branches_by_date() -> None:
82+
def branches_by_date(config: Dict[str, Union[str, int]]) -> None:
8383
"""
8484
Lists branches sorted by the latest commit date.
8585
8686
Args:
87-
None
87+
config: Dict[str, Union[str, int]]: Config dictionary holding env vars.
8888
8989
Returns:
9090
None
9191
"""
9292

93+
# Grab the config options from our config.py.
94+
ignore_authors = config.get("ignore_authors", lambda _s: False)
95+
9396
# Original command:
9497
# git for-each-ref --sort=committerdate refs/heads/ \
9598
# --format='[%(authordate:relative)] %(authorname) %(refname:short)' | cat -n
9699
# TODO: Wouldn't git log --pretty=format:'%ad' --date=short be better here?
97100
# Then we could pipe it through sort, uniq -c, sort -nr, etc.
98101
# Possibly feed back into the parent project
99-
format_str = "[%(authordate:relative)] %(authorname) %(refname:short)"
102+
103+
# Include the email so we can filter based off it, but keep the visible
104+
# part the same as before.
105+
visible_fmt = "[%(authordate:relative)] %(authorname) %(refname:short)"
106+
format_str = f"{visible_fmt}|%(authoremail)"
107+
100108
cmd = [
101109
"git",
102110
"for-each-ref",
@@ -106,19 +114,35 @@ def branches_by_date() -> None:
106114
]
107115

108116
output = run_git_command(cmd)
109-
if output:
110-
# Split the output into lines
111-
lines = output.split("\n")
117+
if not output:
118+
print("No commits found.")
119+
return
112120

113-
# Number the lines similar to 'cat -n'
114-
numbered_lines = [f"{idx + 1} {line}" for idx, line in enumerate(lines)]
121+
# Split lines and filter by author (both name and email), but keep
122+
# visible text only.
123+
visible_lines = []
124+
for raw in output.split("\n"):
125+
if not raw.strip():
126+
continue
127+
if "|" in raw:
128+
visible, email = raw.split("|", 1)
129+
else:
130+
visible, email = raw, ""
115131

116-
# Output numbered lines
117-
print("All branches (sorted by most recent commit):\n")
118-
for line in numbered_lines:
119-
print(f"\t{line}")
120-
else:
132+
# Filter by either email or the visible chunk.
133+
if ignore_authors(email) or ignore_authors(visible):
134+
continue
135+
136+
visible_lines.append(visible)
137+
138+
if not visible_lines:
121139
print("No commits found.")
140+
return
141+
142+
# Number like `cat -n`
143+
print("All branches (sorted by most recent commit):\n")
144+
for idx, line in enumerate(visible_lines, 1):
145+
print(f"\t{idx} {line}")
122146

123147

124148
def contributors(config: Dict[str, Union[str, int]]) -> None:
@@ -213,6 +237,7 @@ def new_contributors(config: Dict[str, Union[str, int]], new_date: str) -> None:
213237
until = config.get("until", "")
214238
log_options = config.get("log_options", "")
215239
pathspec = config.get("pathspec", "")
240+
ignore_authors = config.get("ignore_authors", lambda _s: False)
216241

217242
# Original command:
218243
# git -c log.showSignature=false log --use-mailmap $_merges \
@@ -245,6 +270,9 @@ def new_contributors(config: Dict[str, Union[str, int]], new_date: str) -> None:
245270
try:
246271
email, timestamp = line.split("|")
247272
timestamp = int(timestamp)
273+
# Skip ignored by email
274+
if ignore_authors(email):
275+
continue
248276
# If the contributor is not in the dictionary or the current timestamp is earlier
249277
if email not in contributors_dict or timestamp < contributors_dict[email]:
250278
contributors_dict[email] = timestamp
@@ -283,12 +311,14 @@ def new_contributors(config: Dict[str, Union[str, int]], new_date: str) -> None:
283311
name_cmd = [arg for arg in name_cmd if arg]
284312

285313
# Grab name + email if we can. Otherwise, just grab email
286-
name = run_git_command(name_cmd)
287-
if name:
288-
new_contributors_list.append((name, email))
289-
else:
290-
new_contributors_list.append(("", email))
291-
314+
# while also making sure to ignore any authors that may be
315+
# in our ignore_author env var
316+
name = (run_git_command(name_cmd) or "").strip()
317+
combo = f"{name} <{email}>" if name else f"<{email}>"
318+
if ignore_authors(email) or ignore_authors(name) or ignore_authors(combo):
319+
continue
320+
321+
new_contributors_list.append((name, email))
292322
# Sort the list alphabetically by name to match the original
293323
# and print all of this out
294324
if new_contributors_list:

0 commit comments

Comments
 (0)