|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +import re |
| 4 | +import sys |
| 5 | +from datetime import datetime |
| 6 | +from math import floor |
| 7 | +from subprocess import check_output |
| 8 | +from typing import NoReturn, Optional |
| 9 | + |
| 10 | + |
| 11 | +def run_command(command: str) -> str: |
| 12 | + stdout: str = check_output(command.split()).decode("utf-8").strip() |
| 13 | + return stdout |
| 14 | + |
| 15 | + |
| 16 | +def current_git_branch_name() -> str: |
| 17 | + return run_command("git symbolic-ref --short HEAD") |
| 18 | + |
| 19 | + |
| 20 | +def extract_jira_issue_key(message: str) -> Optional[str]: |
| 21 | + project_key, issue_number = r"[A-Z]{2,}", r"[0-9]+" |
| 22 | + match = re.search(f"{project_key}-{issue_number}", message) |
| 23 | + if match: |
| 24 | + return match.group(0) |
| 25 | + return None |
| 26 | + |
| 27 | + |
| 28 | +def last_commit_datetime() -> datetime: |
| 29 | + # https://git-scm.com/docs/git-log#_pretty_formats |
| 30 | + git_log = "git log -1 --branches --format=%aI" |
| 31 | + author = run_command("git config user.email") |
| 32 | + last_author_datetime = run_command(f"{git_log} --author={author}") or run_command(git_log) |
| 33 | + if "+" in last_author_datetime: |
| 34 | + return datetime.strptime(last_author_datetime.split("+")[0], "%Y-%m-%dT%H:%M:%S") |
| 35 | + return datetime.now() |
| 36 | + |
| 37 | + |
| 38 | +def num_lunches(start: datetime, end: datetime) -> int: |
| 39 | + n = (end.date() - start.date()).days - 1 |
| 40 | + if start < start.replace(hour=12, minute=0, second=0): |
| 41 | + n += 1 |
| 42 | + if end > end.replace(hour=12, minute=45, second=0): |
| 43 | + n += 1 |
| 44 | + return max(n, 0) |
| 45 | + |
| 46 | + |
| 47 | +def num_nights(start: datetime, end: datetime) -> int: |
| 48 | + n = (end.date() - start.date()).days - 1 |
| 49 | + if start < start.replace(hour=1, minute=0, second=0): |
| 50 | + n += 1 |
| 51 | + if end > end.replace(hour=5, minute=0, second=0): |
| 52 | + n += 1 |
| 53 | + return max(n, 0) |
| 54 | + |
| 55 | + |
| 56 | +def time_worked_on_commit() -> Optional[str]: |
| 57 | + now = datetime.now() |
| 58 | + last = last_commit_datetime() |
| 59 | + # Determine the number of minutes worked on this commit as the number of |
| 60 | + # minutes since the last commit minus the lunch breaks and nights. |
| 61 | + working_hours_per_day = 8 |
| 62 | + working_days_per_week = 5 |
| 63 | + minutes = max( |
| 64 | + round((now - last).total_seconds() / 60) |
| 65 | + - num_nights(last, now) * (24 - working_hours_per_day) * 60 |
| 66 | + - num_lunches(last, now) * 45, |
| 67 | + 0, |
| 68 | + ) |
| 69 | + # Convert the number of minutes worked to working weeks, days, hours, |
| 70 | + # minutes. |
| 71 | + if minutes > 0: |
| 72 | + hours = floor(minutes / 60) |
| 73 | + minutes -= hours * 60 |
| 74 | + days = floor(hours / working_hours_per_day) |
| 75 | + hours -= days * working_hours_per_day |
| 76 | + weeks = floor(days / working_days_per_week) |
| 77 | + days -= weeks * working_days_per_week |
| 78 | + return f"{weeks}w {days}d {hours}h {minutes}m" |
| 79 | + return None |
| 80 | + |
| 81 | + |
| 82 | +def main() -> NoReturn: |
| 83 | + # https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html |
| 84 | + # Exit if the branch name does not contain a Jira issue key. |
| 85 | + git_branch_name = current_git_branch_name() |
| 86 | + jira_issue_key = extract_jira_issue_key(git_branch_name) |
| 87 | + if not jira_issue_key: |
| 88 | + sys.exit(0) |
| 89 | + # Read the commit message. |
| 90 | + commit_msg_filepath = sys.argv[1] |
| 91 | + with open(commit_msg_filepath, "r") as f: |
| 92 | + commit_msg = f.read() |
| 93 | + # Split the commit into a subject and body and apply some light formatting. |
| 94 | + commit_elements = commit_msg.split("\n", maxsplit=1) |
| 95 | + commit_subject = commit_elements[0].strip() |
| 96 | + commit_subject = f"{commit_subject[:1].upper()}{commit_subject[1:]}" |
| 97 | + commit_subject = re.sub(r"\.+$", "", commit_subject) |
| 98 | + commit_body = None if len(commit_elements) == 1 else commit_elements[1].strip() |
| 99 | + # Build the new commit message: |
| 100 | + # 1. If there is a body, turn it into a comment on the issue. |
| 101 | + if "#comment" not in commit_msg and commit_body: |
| 102 | + commit_body = f"{jira_issue_key} #comment {commit_body}" |
| 103 | + # 2. Add the time worked to the Work Log in the commit body. |
| 104 | + work_time = time_worked_on_commit() |
| 105 | + if "#time" not in commit_msg and work_time: |
| 106 | + work_log = f"{jira_issue_key} #time {work_time} {commit_subject}" |
| 107 | + commit_body = f"{commit_body}\n\n{work_log}" if commit_body else work_log |
| 108 | + # 3. Make sure the subject starts with a Jira issue key. |
| 109 | + if not extract_jira_issue_key(commit_subject): |
| 110 | + commit_subject = f"{jira_issue_key} {commit_subject}" |
| 111 | + # Override commit message. |
| 112 | + commit_msg = f"{commit_subject}\n\n{commit_body}" if commit_body else commit_subject |
| 113 | + with open(commit_msg_filepath, "w") as f: |
| 114 | + f.write(commit_msg) |
| 115 | + sys.exit(0) |
| 116 | + |
| 117 | + |
| 118 | +if __name__ == "__main__": |
| 119 | + main() |
0 commit comments