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.