├── .pre-commit-hooks.yaml ├── LICENSE ├── README.md ├── auto_smart_commit.py └── setup.py /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: auto-smart-commit 2 | name: Auto Jira smart commit 3 | description: Automatically transform your Git commit messages into Jira smart commits 4 | entry: auto-smart-commit 5 | language: python 6 | stages: [prepare-commit-msg] 7 | always_run: true 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 radix.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Auto Jira smart commit 2 | 3 | This [pre-commit](https://pre-commit.com/) hook transforms your Git commit messages into [Jira smart commits](https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html). 4 | 5 | If your branch name contains a [Jira issue key](https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html) such as `ABC-123`, the hook will automatically format your commit message into a Jira smart commit: 6 | 7 | | Command | Log entry | 8 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 9 | | git commit -m "release the kraken." | ABC-123 Release the kraken

ABC-123 #time 0w 0d 2h 8m Release the kraken

_Effect:_ Logs the time since your last commit on any branch in the Work Log tab. | 10 | | git commit -m "Release the kraken

A kraken lives in dark depths, usually a sunken rift or a cavern filled with detritus, treasure, and wrecked ships." | ABC-123 Release the kraken

ABC-123 #comment A kraken lives in dark depths, usually a sunken rift or a cavern filled with detritus, treasure, and wrecked ships.

ABC-123 #time 0w 0d 2h 8m Release the kraken

_Effect:_ Posts a comment to the Jira issue and logs the time since your last commit in the Work Log tab. | 11 | 12 | If the branch name does not contain a Jira issue key, the commit message is not modified. The time logged takes into account non-working hours such as lunch breaks and nights. 13 | 14 | See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) for an explanation of the seven rules of a great Git commit message: 15 | 16 | 1. Separate subject from body with a blank line 17 | 2. Limit the subject line to 50 characters 18 | 3. Capitalize the subject line (automated) 19 | 4. Do not end the subject line with a period (automated) 20 | 5. Use the imperative mood in the subject line 21 | 6. Wrap the body at 72 characters 22 | 7. Use the body to explain what and why vs. how 23 | 24 | ## Installation 25 | 26 | Add the following to your `.pre-commit-config.yaml` file: 27 | 28 | ```yaml 29 | repos: 30 | - repo: https://github.com/radix-ai/auto-smart-commit 31 | rev: v1.0.3 32 | hooks: 33 | - id: auto-smart-commit 34 | ``` 35 | 36 | and make sure to run `pre-commit install --hook-type prepare-commit-msg` to install the hook type necessary for this hook. 37 | -------------------------------------------------------------------------------- /auto_smart_commit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | from datetime import datetime 7 | from math import floor 8 | from subprocess import check_output 9 | from typing import NoReturn, Optional 10 | 11 | 12 | def run_command(command: str) -> str: 13 | try: 14 | stdout: str = check_output(command.split()).decode("utf-8").strip() 15 | except Exception: 16 | stdout = "" 17 | return stdout 18 | 19 | 20 | def current_git_branch_name() -> str: 21 | return run_command("git symbolic-ref --short HEAD") 22 | 23 | 24 | def extract_jira_issue_key(message: str) -> Optional[str]: 25 | project_key, issue_number = r"[A-Z]{2,}", r"[0-9]+" 26 | match = re.search(f"{project_key}-{issue_number}", message) 27 | if match: 28 | return match.group(0) 29 | return None 30 | 31 | 32 | def last_commit_datetime() -> datetime: 33 | # https://git-scm.com/docs/git-log#_pretty_formats 34 | git_log = "git log -1 --branches --format=%aI" 35 | author = run_command("git config user.email") 36 | last_author_datetime = run_command(f"{git_log} --author={author}") or run_command(git_log) 37 | if "+" in last_author_datetime: 38 | return datetime.strptime(last_author_datetime.split("+")[0], "%Y-%m-%dT%H:%M:%S") 39 | return datetime.now() 40 | 41 | 42 | def num_lunches(start: datetime, end: datetime) -> int: 43 | n = (end.date() - start.date()).days - 1 44 | if start < start.replace(hour=12, minute=0, second=0): 45 | n += 1 46 | if end > end.replace(hour=12, minute=45, second=0): 47 | n += 1 48 | return max(n, 0) 49 | 50 | 51 | def num_nights(start: datetime, end: datetime) -> int: 52 | n = (end.date() - start.date()).days - 1 53 | if start < start.replace(hour=1, minute=0, second=0): 54 | n += 1 55 | if end > end.replace(hour=5, minute=0, second=0): 56 | n += 1 57 | return max(n, 0) 58 | 59 | 60 | def time_worked_on_commit() -> Optional[str]: 61 | now = datetime.now() 62 | last = last_commit_datetime() 63 | # Determine the number of minutes worked on this commit as the number of minutes since the last 64 | # commit minus the lunch breaks and nights. 65 | working_hours_per_day = 8 66 | working_days_per_week = 5 67 | minutes = max( 68 | round((now - last).total_seconds() / 60) 69 | - num_nights(last, now) * (24 - working_hours_per_day) * 60 70 | - num_lunches(last, now) * 45, 71 | 0, 72 | ) 73 | # Convert the number of minutes worked to working weeks, days, hours, minutes. 74 | if minutes > 0: 75 | hours = floor(minutes / 60) 76 | minutes -= hours * 60 77 | days = floor(hours / working_hours_per_day) 78 | hours -= days * working_hours_per_day 79 | weeks = floor(days / working_days_per_week) 80 | days -= weeks * working_days_per_week 81 | return f"{weeks}w {days}d {hours}h {minutes}m" 82 | return None 83 | 84 | 85 | def main() -> NoReturn: 86 | # https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html 87 | # Exit if the branch name does not contain a Jira issue key. 88 | git_branch_name = current_git_branch_name() 89 | jira_issue_key = extract_jira_issue_key(git_branch_name) 90 | if not jira_issue_key: 91 | sys.exit(0) 92 | # Read the commit message. 93 | commit_msg_filepath = sys.argv[1] 94 | with open(commit_msg_filepath, "r") as f: 95 | commit_msg = f.read() 96 | # Split the commit into a subject and body and apply some light formatting. 97 | commit_elements = commit_msg.split("\n", maxsplit=1) 98 | commit_subject = commit_elements[0].strip() 99 | commit_subject = f"{commit_subject[:1].upper()}{commit_subject[1:]}" 100 | commit_subject = re.sub(r"\.+$", "", commit_subject) 101 | commit_body = None if len(commit_elements) == 1 else commit_elements[1].strip() 102 | # Build the new commit message: 103 | # 1. If there is a body, turn it into a comment on the issue. 104 | if "#comment" not in commit_msg and commit_body: 105 | commit_body = f"{jira_issue_key} #comment {commit_body}" 106 | # 2. Add the time worked to the Work Log in the commit body. 107 | work_time = time_worked_on_commit() 108 | if "#time" not in commit_msg and work_time: 109 | work_log = f"{jira_issue_key} #time {work_time} {commit_subject}" 110 | commit_body = f"{commit_body}\n\n{work_log}" if commit_body else work_log 111 | # 3. Make sure the subject starts with a Jira issue key. 112 | if not extract_jira_issue_key(commit_subject): 113 | commit_subject = f"{jira_issue_key} {commit_subject}" 114 | # Override commit message. 115 | commit_msg = f"{commit_subject}\n\n{commit_body}" if commit_body else commit_subject 116 | with open(commit_msg_filepath, "w") as f: 117 | f.write(commit_msg) 118 | sys.exit(0) 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="auto-smart-commit", 5 | version="1.0.3", 6 | py_modules=["auto_smart_commit"], 7 | entry_points={ 8 | "console_scripts": [ 9 | "auto-smart-commit=auto_smart_commit:main", 10 | ], 11 | }, 12 | author="Laurent Sorber", 13 | author_email="laurent@radix.ai", 14 | description="Automatically transform your Git commit messages into Jira smart commits", 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | ) 21 | --------------------------------------------------------------------------------