Skip to content

activity calendar by author #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions git_py_stats/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ def parse_arguments(argv: Optional[List[str]] = None) -> Namespace:
help="Displays a list of commits per timezone by author",
)

# Calendar Options
parser.add_argument(
"-k",
"--commits-calendar-by-author",
metavar='"AUTHOR NAME"',
type=str,
help="Show a calendar of commits by author",
)

# Suggest Options
parser.add_argument(
"-r",
Expand Down
1 change: 1 addition & 0 deletions git_py_stats/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def get_config() -> Dict[str, Union[str, int]]:
_GIT_LOG_OPTIONS (str): Additional git log options. Default is empty.
_MENU_THEME (str): Toggles between the default theme and legacy theme.
- 'legacy' to set the legacy theme
- 'none' to disable the menu theme
- Empty to set the default theme

Args:
Expand Down
89 changes: 89 additions & 0 deletions git_py_stats/generate_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
from typing import Optional, Dict, Any, List, Union
from datetime import datetime, timedelta
from collections import defaultdict

from git_py_stats.git_operations import run_git_command

Expand Down Expand Up @@ -305,6 +306,94 @@ def changelogs(config: Dict[str, Union[str, int]], author: Optional[str] = None)
next_date = date # Update next_date for the next iteration


def commits_calendar_by_author(config: Dict[str, Union[str, int]], author: Optional[str]) -> None:
"""
Displays a calendar of commits by author

Args:
config: Dict[str, Union[str, int]]: Config dictionary holding env vars.
author: Optional[str]: The author's name to filter commits by.

Returns:
None
"""

# Initialize variables similar to the Bash version
author_option = f"--author={author}" if author else ""

# Grab the config options from our config.py.
# config.py should give fallbacks for these, but for sanity,
# lets also provide some defaults just in case.
merges = config.get("merges", "--no-merges")
since = config.get("since", "")
until = config.get("until", "")
log_options = config.get("log_options", "")
pathspec = config.get("pathspec", "")

# Original git command:
# git -c log.showSignature=false log --use-mailmap $_merges \
# --date=iso --author="$author" "$_since" "$_until" $_log_options \
# --pretty='%ad' $_pathspec
cmd = [
"git",
"-c",
"log.showSignature=false",
"log",
"--use-mailmap",
"--date=iso",
f"--author={author}",
"--pretty=%ad",
]

if author_option:
cmd.append(author_option)

cmd.extend([since, until, log_options, merges, pathspec])

# Remove any empty space from the cmd
cmd = [arg for arg in cmd if arg]

print(f"Commit Activity Calendar for '{author}'")

# Get commit dates
output = run_git_command(cmd)
if not output:
print("No commits found.")
return

print("\n Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec")

count = defaultdict(lambda: defaultdict(int))
for line in output.strip().split("\n"):
try:
date_str = line.strip().split(" ")[0]
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
weekday = date_obj.isoweekday() # 1=Mon, ..., 7=Sun
month = date_obj.month
count[weekday][month] += 1
except ValueError:
continue

# Print the calendar
weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
for d in range(1, 8):
row = f"{weekdays[d-1]:<5} "
for m in range(1, 13):
c = count[d][m]
if c == 0:
out = "..."
elif c <= 9:
out = "░░░"
elif c <= 19:
out = "▒▒▒"
else:
out = "▓▓▓"
row += out + (" " if m < 12 else "")
print(row)

print("\nLegend: ... = 0 ░░░ = 1–9 ▒▒▒ = 10–19 ▓▓▓ = 20+ commits")


def my_daily_status(config: Dict[str, Union[str, int]]) -> None:
"""
Displays the user's commits from the last day.
Expand Down
3 changes: 3 additions & 0 deletions git_py_stats/interactive_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def handle_interactive_mode(config: Dict[str, Union[str, int]]) -> None:
"20": lambda: list_cmds.git_commits_per_timezone(config),
"21": lambda: list_cmds.git_commits_per_timezone(config, input("Enter author name: ")),
"22": lambda: suggest_cmds.suggest_reviewers(config),
"23": lambda: generate_cmds.commits_calendar_by_author(
config, input("Enter author name: ")
),
}

while True:
Expand Down
2 changes: 2 additions & 0 deletions git_py_stats/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str:
print(f"{NUMS} 21){TEXT} Git commits per timezone by author")
print(f"\n{TITLES} Suggest:{NORMAL}")
print(f"{NUMS} 22){TEXT} Code reviewers (based on git history)")
print(f"\n{TITLES} Calendar:{NORMAL}")
print(f"{NUMS} 23){TEXT} Activity calendar by author")
print(f"\n{HELP_TXT}Please enter a menu option or {EXIT_TXT}press Enter to exit.{NORMAL}")

choice = input(f"{TEXT}> {NORMAL}")
Expand Down
3 changes: 3 additions & 0 deletions git_py_stats/non_interactive_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def handle_non_interactive_mode(args: Namespace, config: Dict[str, Union[str, in
config, args.commits_by_author_by_timezone
),
"suggest_reviewers": lambda: suggest_cmds.suggest_reviewers(config),
"commits_calendar_by_author": lambda: generate_cmds.commits_calendar_by_author(
config, args.commits_calendar_by_author
),
}

# Call the appropriate function based on the command-line argument
Expand Down
21 changes: 21 additions & 0 deletions git_py_stats/tests/test_generate_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ def test_changelogs_with_author(self, mock_print, mock_run_git_command):

self.assertTrue(mock_print.called)

@patch("git_py_stats.generate_cmds.run_git_command")
@patch("builtins.print")
def test_commits_calendar_by_author(self, mock_print, mock_run_git_command):
"""
Test commits_calendar_by_author function with an author specified.
"""
# Mock git command outputs
mock_run_git_command.side_effect = [
"", # git diff output (no changes)
"John Doe", # git config user.name
"", # git log output (no commits)
]

generate_cmds.commits_calendar_by_author(self.mock_config, author="John Doe")

# Verify that the author option was included in the command
called_cmd = mock_run_git_command.call_args_list[0][0][0]
self.assertIn("--author=John Doe", called_cmd)

self.assertTrue(mock_print.called)

@patch("git_py_stats.generate_cmds.run_git_command")
@patch("builtins.print")
def test_my_daily_status(self, mock_print, mock_run_git_command):
Expand Down
9 changes: 9 additions & 0 deletions git_py_stats/tests/test_non_interactive_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def setUp(self):
"commits_by_timezone": False,
"commits_by_author_by_timezone": None,
"suggest_reviewers": False,
"commits_calendar_by_author": None,
}

@patch("git_py_stats.non_interactive_mode.generate_cmds.detailed_git_stats")
Expand Down Expand Up @@ -104,6 +105,14 @@ def test_json_output(self, mock_save_json):
non_interactive_mode.handle_non_interactive_mode(args, self.mock_config)
mock_save_json.assert_called_once_with(self.mock_config)

@patch("git_py_stats.non_interactive_mode.generate_cmds.commits_calendar_by_author")
def test_commits_calendar_by_author(self, mock_commits_calendar_by_author):
args_dict = self.all_args.copy()
args_dict["commits_calendar_by_author"] = "John Doe"
args = Namespace(**args_dict)
non_interactive_mode.handle_non_interactive_mode(args, self.mock_config)
mock_commits_calendar_by_author.assert_called_once_with(self.mock_config, "John Doe")

@patch("git_py_stats.non_interactive_mode.list_cmds.branch_tree")
def test_branch_tree(self, mock_branch_tree):
args_dict = self.all_args.copy()
Expand Down
4 changes: 4 additions & 0 deletions man/git-py-stats.1
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ Display a list of commits by timezone by author.
.B \-r, \--suggest-reviewers
Suggest code reviewers based on contribution history.

.TP
.B \-k, \--commits-calendar-by-author "AUTHOR NAME"
Display a calendar of commits by author.

.TP
.B \-h, \--help
Show this help message and exit.
Expand Down