├── .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 |
--------------------------------------------------------------------------------