From 7d34fb5d3454dab09b3841507b430dec6de76e7c Mon Sep 17 00:00:00 2001 From: Tom Ice Date: Sun, 7 Sep 2025 16:31:33 -0400 Subject: [PATCH] Add heatmap logic and tests * The calendar option has been expanding, so it makes sense to start breaking out the calendar options to its own area. Refactor necessary tests, functionality, etc. into their own separate files so we can continue expanding on the calendar option functionality * Adds general heatmap logic, tests, and necessary helper functions * Also adds all necessary hooks and documentation changes to handle the new heatmap feature Closes #10 --- README.md | 8 + git_py_stats/arg_parser.py | 7 + git_py_stats/calendar_cmds.py | 252 ++++++++++++++++++ git_py_stats/config.py | 13 + git_py_stats/generate_cmds.py | 89 ------- git_py_stats/interactive_mode.py | 5 +- git_py_stats/menu.py | 10 + git_py_stats/non_interactive_mode.py | 5 +- git_py_stats/tests/test_calendar_cmds.py | 151 +++++++++++ git_py_stats/tests/test_generate_cmds.py | 21 -- .../tests/test_non_interactive_mode.py | 11 +- man/git-py-stats.1 | 4 + 12 files changed, 461 insertions(+), 115 deletions(-) create mode 100644 git_py_stats/calendar_cmds.py create mode 100644 git_py_stats/tests/test_calendar_cmds.py diff --git a/README.md b/README.md index 068baa4..8308b2c 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,14 @@ Works with commands `--git-stats-by-branch` and `--csv-output-by-branch`. export _GIT_BRANCH="master" ``` +### Commit days + +You can set the variable `_GIT_DAYS` to set the number of days for the heatmap. + +```bash +export _GIT_DAYS=30 +``` + ### Color Themes You can change to the legacy color scheme by toggling the variable `_MENU_THEME` diff --git a/git_py_stats/arg_parser.py b/git_py_stats/arg_parser.py index a68cf43..859aee2 100644 --- a/git_py_stats/arg_parser.py +++ b/git_py_stats/arg_parser.py @@ -175,6 +175,13 @@ def parse_arguments(argv: Optional[List[str]] = None) -> Namespace: help="Show a calendar of commits by author", ) + parser.add_argument( + "-H", + "--commits-heatmap", + action="store_true", + help="Show a heatmap of commits per day-of-week", + ) + # Suggest Options parser.add_argument( "-r", diff --git a/git_py_stats/calendar_cmds.py b/git_py_stats/calendar_cmds.py new file mode 100644 index 0000000..085218d --- /dev/null +++ b/git_py_stats/calendar_cmds.py @@ -0,0 +1,252 @@ +""" +Functions related to the 'Calendar' section. +""" + +from typing import Optional, Dict, Union +from datetime import datetime, timedelta +from collections import defaultdict + +from git_py_stats.git_operations import run_git_command + + +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 commits_heatmap(config: Dict[str, Union[str, int]]) -> None: + """ + Shows a heatmap of commits per hour of each day for the last N days. + + Uses 256-color ANSI sequences to emulate the original tput color palette: + 226 (bright yellow) + 220 (gold) + 214 (orange) + 208 (dark orange), + 202 (red-orange), + 160 (red), + 88 (deep red), + 52 (darkest red) + + Args: + config: Dict[str, Union[str, int]]: Config dictionary holding env vars. + + Returns: + None + """ + + # ANSI color code helpers + RESET = "\033[0m" + + def ansi256(n: int) -> str: + return f"\033[38;5;{n}m" + + COLOR_BRIGHT_YELLOW = ansi256(226) + COLOR_GOLD = ansi256(220) + COLOR_ORANGE = ansi256(214) + COLOR_DARK_ORANGE = ansi256(208) + COLOR_RED_ORANGE = ansi256(202) + COLOR_RED = ansi256(160) + COLOR_DARK_RED = ansi256(88) + COLOR_DEEPEST_RED = ansi256(52) + COLOR_GRAY = ansi256(240) # Gives the dark color for no commits + + def color_for_count(n: int) -> str: + # Map counts to colors + if n == 1: + return COLOR_BRIGHT_YELLOW + elif n == 2: + return COLOR_GOLD + elif n == 3: + return COLOR_ORANGE + elif n == 4: + return COLOR_DARK_ORANGE + elif n == 5: + return COLOR_RED_ORANGE + elif n == 6: + return COLOR_RED + elif 7 <= n <= 8: + return COLOR_DARK_RED + elif 9 <= n <= 10: + return COLOR_DEEPEST_RED + else: + return COLOR_DEEPEST_RED # 11+ + + # 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") + log_options = config.get("log_options", "") + pathspec = config.get("pathspec", "--") + days = int(config.get("days", 30)) + + print(f"Commit Heatmap for the last {days} days") + + # Header bar thing + header = "Day | Date/Hours |" + for h in range(24): + header += f" {h:2d}" + print(header) + print( + "------------------------------------------------------------------------------------------" + ) + + # Build each day row from oldest to newest, marking weekends, + # and printing the row header in "DDD | YYYY-MM-DD |" format + today = datetime.now().date() + for delta in range(days - 1, -1, -1): + day = today - timedelta(days=delta) + is_weekend = day.isoweekday() > 5 + day_prefix_color = COLOR_GRAY if is_weekend else RESET + dayname = day.strftime("%a") + print(f"{day_prefix_color}{dayname} | {day.isoformat()} |", end="") + + # Count commits per hour for this day + since = f"--since={day.isoformat()} 00:00" + until = f"--until={day.isoformat()} 23:59" + + cmd = [ + "git", + "-c", + "log.showSignature=false", + "log", + "--use-mailmap", + merges, + since, + until, + "--pretty=%ci", + log_options, + pathspec, + ] + + # Remove any empty space from the cmd + cmd = [arg for arg in cmd if arg] + + output = run_git_command(cmd) or "" + + # Create 24 cell per-hour commit histrogram for the day, + # grabbing only what is parseable. + counts = [0] * 24 + if output: + for line in output.splitlines(): + parts = line.strip().split() + if len(parts) >= 2: + time_part = parts[1] + try: + hour = int(time_part.split(":")[0]) + if 0 <= hour <= 23: + counts[hour] += 1 + except ValueError: + continue + + # Render the cells + for hour in range(24): + n = counts[hour] + if n == 0: + # gray dot for zero commits + print(f" {COLOR_GRAY}.{RESET} ", end="") + else: + c = color_for_count(n) + print(f"{c} █ {RESET}", end="") + # End the row/reset + print(RESET) + + # Match original version in the bash impl + print( + "------------------------------------------------------------------------------------------" + ) + # Legend + print("\nLegend:") + print(f" {COLOR_BRIGHT_YELLOW}█{RESET} 1 commit") + print(f" {COLOR_GOLD}█{RESET} 2 commits") + print(f" {COLOR_ORANGE}█{RESET} 3 commits") + print(f" {COLOR_DARK_ORANGE}█{RESET} 4 commits") + print(f" {COLOR_RED_ORANGE}█{RESET} 5 commits") + print(f" {COLOR_RED}█{RESET} 6 commits") + print(f" {COLOR_DARK_RED}█{RESET} 7–8 commits") + print(f" {COLOR_DEEPEST_RED}█{RESET} 9–10 commits") + print(f" {COLOR_DEEPEST_RED}█{RESET} 11+ commits") + print(f" {COLOR_GRAY}.{RESET} = no commits\n") diff --git a/git_py_stats/config.py b/git_py_stats/config.py index 05363e2..bd60486 100644 --- a/git_py_stats/config.py +++ b/git_py_stats/config.py @@ -37,6 +37,7 @@ def get_config() -> Dict[str, Union[str, int]]: - Any other value defaults to '--no-merges' currently. _GIT_LIMIT (int): Limits the git log output. Defaults to 10. _GIT_LOG_OPTIONS (str): Additional git log options. Default is empty. + _GIT_DAYS (int): Defines number of days for the heatmap. 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 @@ -117,6 +118,18 @@ def get_config() -> Dict[str, Union[str, int]]: else: config["log_options"] = "" + # _GIT_DAYS + git_days: Optional[str] = os.environ.get("_GIT_DAYS") + if git_days: + # Slight sanitization, but we're still gonna wild west this a bit + try: + config["days"] = int(git_days) + except ValueError: + print("Invalid value for _GIT_DAYS. Using default value 30.") + config["days"] = 30 + else: + config["days"] = 30 + # _MENU_THEME menu_theme: Optional[str] = os.environ.get("_MENU_THEME") if menu_theme == "legacy": diff --git a/git_py_stats/generate_cmds.py b/git_py_stats/generate_cmds.py index df6d551..9f57052 100644 --- a/git_py_stats/generate_cmds.py +++ b/git_py_stats/generate_cmds.py @@ -7,7 +7,6 @@ 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 @@ -306,94 +305,6 @@ 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. diff --git a/git_py_stats/interactive_mode.py b/git_py_stats/interactive_mode.py index 853176e..ba008c2 100644 --- a/git_py_stats/interactive_mode.py +++ b/git_py_stats/interactive_mode.py @@ -4,7 +4,7 @@ from typing import Dict, Union -from git_py_stats import generate_cmds, list_cmds, suggest_cmds +from git_py_stats import generate_cmds, list_cmds, suggest_cmds, calendar_cmds from git_py_stats.menu import interactive_menu @@ -44,9 +44,10 @@ 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( + "23": lambda: calendar_cmds.commits_calendar_by_author( config, input("Enter author name: ") ), + "24": lambda: calendar_cmds.commits_heatmap(config), } while True: diff --git a/git_py_stats/menu.py b/git_py_stats/menu.py index 4f76b17..0eaa3a4 100644 --- a/git_py_stats/menu.py +++ b/git_py_stats/menu.py @@ -17,6 +17,10 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: str: The user's menu choice. """ # ANSI escape codes for colors and formatting + # FIXME: We now have colors in two places - one in the calendar area + # and one here. Refactor this to have a global color scheme for the + # whole program that it can leverage instead of having multiple colors + # in different places. NORMAL = "\033[0m" BOLD = "\033[1m" RED = "\033[31m" @@ -27,6 +31,11 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: # Handle default, legacy, and colorless menu theme = config.get("menu_theme", "") + # Grab the days set by the user for _GIT_DAYS + # NOTE: We should already have a guard here to default this to 30 + # in the config.py file. Possible redundancy here. + days = config.get("days", 30) + if theme == "legacy": TITLES = f"{BOLD}{RED}" TEXT = f"{NORMAL}{CYAN}" @@ -74,6 +83,7 @@ def interactive_menu(config: Dict[str, Union[str, int]]) -> str: 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"{NUMS} 24){TEXT} Activity heatmap for the last {days} days") print(f"\n{HELP_TXT}Please enter a menu option or {EXIT_TXT}press Enter to exit.{NORMAL}") choice = input(f"{TEXT}> {NORMAL}") diff --git a/git_py_stats/non_interactive_mode.py b/git_py_stats/non_interactive_mode.py index bf046c4..cc271c5 100644 --- a/git_py_stats/non_interactive_mode.py +++ b/git_py_stats/non_interactive_mode.py @@ -5,7 +5,7 @@ from argparse import ArgumentParser, Namespace, RawTextHelpFormatter from typing import Dict, Union -from git_py_stats import generate_cmds, list_cmds, suggest_cmds +from git_py_stats import generate_cmds, list_cmds, suggest_cmds, calendar_cmds def handle_non_interactive_mode(args: Namespace, config: Dict[str, Union[str, int]]) -> None: @@ -50,9 +50,10 @@ 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( + "commits_calendar_by_author": lambda: calendar_cmds.commits_calendar_by_author( config, args.commits_calendar_by_author ), + "commits_heatmap": lambda: calendar_cmds.commits_heatmap(config), } # Call the appropriate function based on the command-line argument diff --git a/git_py_stats/tests/test_calendar_cmds.py b/git_py_stats/tests/test_calendar_cmds.py new file mode 100644 index 0000000..c58ad50 --- /dev/null +++ b/git_py_stats/tests/test_calendar_cmds.py @@ -0,0 +1,151 @@ +import unittest +from unittest.mock import patch +from datetime import datetime + +from git_py_stats import calendar_cmds + + +class TestCalendarCmds(unittest.TestCase): + """ + Unit test class for testing the functionality of the calendar_cmds module + """ + + def setUp(self): + # Mock configuration for testing + self.mock_config = { + "since": "--since=2020-01-01", + "until": "--until=2024-12-31", + "merges": "--no-merges", + "log_options": "", + "pathspec": "--", + "limit": 10, # Ensure limit is an integer + "menu_theme": "", + } + + @patch("git_py_stats.calendar_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) + ] + + calendar_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) + + # HEATMAP HELPER FUNCTIONS + def _freeze_today(self, y: int, m: int, d: int): + """ + Patch calendar_cmds.datetime.now() to a fixed date (keeps strptime intact). + """ + + class _FixedDT(datetime): + @classmethod + def now(cls, tz=None): + return cls(y, m, d, 12, 0, 0) + + # Swap datetime in the module + self._orig_datetime = calendar_cmds.datetime + calendar_cmds.datetime = _FixedDT + self.addCleanup(self._restore_datetime) + + def _restore_datetime(self): + if hasattr(self, "_orig_datetime"): + calendar_cmds.datetime = self._orig_datetime + + @patch("git_py_stats.calendar_cmds.run_git_command") + @patch("builtins.print") + def test_commits_heatmap_invokes_git_per_day_and_prints_header( + self, mock_print, mock_run_git_command + ): + """ + With days=2 and today fixed to 2024-01-03, expect two git calls: + for 2024-01-02 and 2024-01-03. Also validate header and row stubs. + """ + # Freeze "today" as 2024-01-03 (Wed) + self._freeze_today(2024, 1, 3) + cfg = dict(self.mock_config, days=2) + + # First day has two commits; second day none. + mock_run_git_command.side_effect = [ + "2024-01-02 00:15:00 +0000\n2024-01-02 15:20:00 +0000", + "", + ] + + calendar_cmds.commits_heatmap(cfg) + + # Two calls total (one per day) + self.assertEqual(mock_run_git_command.call_count, 2) + + # Validate the first command args + first_cmd = mock_run_git_command.call_args_list[0][0][0] + self.assertIn("git", first_cmd) + self.assertIn("-c", first_cmd) + self.assertIn("log.showSignature=false", first_cmd) + self.assertIn("log", first_cmd) + self.assertIn("--use-mailmap", first_cmd) + self.assertIn("--no-merges", first_cmd) + self.assertIn("--pretty=%ci", first_cmd) + self.assertIn("--since=2024-01-02 00:00", first_cmd) + self.assertIn("--until=2024-01-02 23:59", first_cmd) + self.assertIn("--", first_cmd) # pathspec + + # Validate the second command args (today) + second_cmd = mock_run_git_command.call_args_list[1][0][0] + self.assertIn("--since=2024-01-03 00:00", second_cmd) + self.assertIn("--until=2024-01-03 23:59", second_cmd) + + # Stitch printed output to a single string for simple assertions + out = "\n".join(" ".join(map(str, c.args)) for c in mock_print.call_args_list) + + # Title and header + self.assertIn("Commit Heatmap for the last 2 days", out) + self.assertIn("Day | Date/Hours |", out) + # Row headers (ignore ANSI codes beyond substring check) + self.assertIn("Tue | 2024-01-02 |", out) + self.assertIn("Wed | 2024-01-03 |", out) + + @patch("git_py_stats.calendar_cmds.run_git_command", return_value="") + @patch("builtins.print") + def test_commits_heatmap_weekend_rows_are_gray(self, mock_print, _mock_run): + """ + When the single rendered day is a Saturday, the line should start + with the 256-color gray prefix (38;5;240m). + """ + # Freeze to a Saturday: 2024-01-06 + self._freeze_today(2024, 1, 6) + cfg = dict(self.mock_config, days=1) + + calendar_cmds.commits_heatmap(cfg) + + out = "\n".join(" ".join(map(str, c.args)) for c in mock_print.call_args_list) + # Gray prefix must appear before "Sat | 2024-01-06 |" + self.assertIn("\x1b[38;5;240mSat | 2024-01-06 |", out) + + @patch("git_py_stats.calendar_cmds.run_git_command", return_value="") + @patch("builtins.print") + def test_commits_heatmap_respects_days_setting(self, _mock_print, mock_run): + """ + If days=3, run_git_command is called exactly 3 times (one per day). + """ + # Freeze some arbitrary date + self._freeze_today(2024, 5, 10) + cfg = dict(self.mock_config, days=3) + + calendar_cmds.commits_heatmap(cfg) + + self.assertEqual(mock_run.call_count, 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/git_py_stats/tests/test_generate_cmds.py b/git_py_stats/tests/test_generate_cmds.py index 8b7fc97..f51ae2a 100644 --- a/git_py_stats/tests/test_generate_cmds.py +++ b/git_py_stats/tests/test_generate_cmds.py @@ -105,27 +105,6 @@ 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): diff --git a/git_py_stats/tests/test_non_interactive_mode.py b/git_py_stats/tests/test_non_interactive_mode.py index ecf8178..6f1cdc8 100644 --- a/git_py_stats/tests/test_non_interactive_mode.py +++ b/git_py_stats/tests/test_non_interactive_mode.py @@ -47,6 +47,7 @@ def setUp(self): "commits_by_author_by_timezone": None, "suggest_reviewers": False, "commits_calendar_by_author": None, + "commits_heatmap": None, } @patch("git_py_stats.non_interactive_mode.generate_cmds.detailed_git_stats") @@ -105,7 +106,7 @@ 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") + @patch("git_py_stats.non_interactive_mode.calendar_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" @@ -113,6 +114,14 @@ def test_commits_calendar_by_author(self, mock_commits_calendar_by_author): 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.calendar_cmds.commits_heatmap") + def test_commits_heatmap(self, mock_commits_heatmap): + args_dict = self.all_args.copy() + args_dict["commits_heatmap"] = True + args = Namespace(**args_dict) + non_interactive_mode.handle_non_interactive_mode(args, self.mock_config) + mock_commits_heatmap.assert_called_once_with(self.mock_config) + @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() diff --git a/man/git-py-stats.1 b/man/git-py-stats.1 index d13961e..41e4ad9 100644 --- a/man/git-py-stats.1 +++ b/man/git-py-stats.1 @@ -102,6 +102,10 @@ Suggest code reviewers based on contribution history. .B \-k, \--commits-calendar-by-author "AUTHOR NAME" Display a calendar of commits by author. +.TP +.B \-H, \--commits-heatmap +Shows a heatmap of commits per day-of-week per month for the last 30 days. + .TP .B \-h, \--help Show this help message and exit.