-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
150 additions
and
157 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
- id: auto-smart-commit | ||
name: Auto Jira smart commit | ||
description: Automatically transform your Git commit messages into Jira smart commits | ||
entry: auto-smart-commit.py | ||
language: script | ||
always_run: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,44 @@ | ||
## Automated Jira smart commits | ||
## Auto Jira smart commit | ||
|
||
This [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) Git hook transforms your Git commit messages into [Jira smart commits](https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html). | ||
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). | ||
|
||
After naming your branch after a [Jira issue key](https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html) such as `ML-42`, the hook will automatically format your commit message into a Jira smart commit: | ||
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: | ||
|
||
| Command | Log entry | | ||
| ------- | --------- | | ||
| git commit -m "open the pod bay doors." | ML-42 Open the pod bay doors<br><br>Jira #time 0w 0d 2h 8m Open the pod bay doors<br><br>_Effect:_ Logs the time since your last commit on any branch in the Work Log tab. | | ||
| git commit -m "Open the pod bay doors<br><br>I should get back inside, so I must open the pod bay doors." | ML-42 Open the pod bay doors<br><br>Jira #comment I should get back inside, so I must open the pod bay doors.<br><br>Jira #time 0w 0d 2h 8m Open the pod bay doors<br><br>_Effect:_ Posts a comment to the Jira issue and logs the time since your last commit in the Work Log tab. | | ||
| git commit | ML-42 d$:<If applied, this commit will "Open the pod bay doors"><br><br>Jira #comment d$:<What does this commit do, and why?><br><br>Jira #time 0w 0d 2h 8m Open the pod bay doors<br><br>_Effect:_ Edit the smart commit with your favourite editor before publishing it. Since the default is usually Vim, we remind the user how to delete a line starting from the cursor with `d$`. | | ||
| git commit -m "release the kraken." | ABC-123 Release the kraken<br><br>ABC-123 #time 0w 0d 2h 8m Release the kraken<br><br>_Effect:_ Logs the time since your last commit on any branch in the Work Log tab. | | ||
| git commit -m "Release the kraken<br><br>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<br><br>ABC-123 #comment A kraken lives in dark depths, usually a sunken rift or a cavern filled with detritus, treasure, and wrecked ships.<br><br>ABC-123 #time 0w 0d 2h 8m Release the kraken<br><br>_Effect:_ Posts a comment to the Jira issue and logs the time since your last commit in the Work Log tab. | | ||
|
||
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. | ||
|
||
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: | ||
|
||
1. Separate subject from body with a blank line | ||
2. Limit the subject line to 50 characters | ||
3. Capitalize the subject line | ||
4. Do not end the subject line with a period | ||
3. Capitalize the subject line (automated) | ||
4. Do not end the subject line with a period (automated) | ||
5. Use the imperative mood in the subject line | ||
6. Wrap the body at 72 characters | ||
7. Use the body to explain what and why vs. how | ||
|
||
## Installation | ||
|
||
To install the git hooks in the directory `githooks`, run the following command from the root of your repository: | ||
### Installation with pre-commit | ||
|
||
Add the following to your `.pre-commit-config.yaml` file: | ||
|
||
```yaml | ||
repos: | ||
- repo: https://github.com/radix-ai/auto-smart-commit | ||
rev: v1.0.0 | ||
hooks: | ||
- id: auto-smart-commit | ||
``` | ||
### Manual installation | ||
Copy `auto-smart-commit.py` to a `githooks` directory in your repository, then run the following command from the root of your repository: | ||
|
||
```bash | ||
git config --local core.hooksPath githooks | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
#!/usr/bin/env python | ||
|
||
import re | ||
import sys | ||
from datetime import datetime | ||
from math import floor | ||
from subprocess import check_output | ||
from typing import NoReturn, Optional | ||
|
||
|
||
def run_command(command: str) -> str: | ||
stdout: str = check_output(command.split()).decode("utf-8").strip() | ||
return stdout | ||
|
||
|
||
def current_git_branch_name() -> str: | ||
return run_command("git symbolic-ref --short HEAD") | ||
|
||
|
||
def extract_jira_issue_key(message: str) -> Optional[str]: | ||
project_key, issue_number = r"[A-Z]{2,}", r"[0-9]+" | ||
match = re.search(f"{project_key}-{issue_number}", message) | ||
if match: | ||
return match.group(0) | ||
return None | ||
|
||
|
||
def last_commit_datetime() -> datetime: | ||
# https://git-scm.com/docs/git-log#_pretty_formats | ||
git_log = "git log -1 --branches --format=%aI" | ||
author = run_command("git config user.email") | ||
last_author_datetime = run_command(f"{git_log} --author={author}") or run_command(git_log) | ||
if "+" in last_author_datetime: | ||
return datetime.strptime(last_author_datetime.split("+")[0], "%Y-%m-%dT%H:%M:%S") | ||
return datetime.now() | ||
|
||
|
||
def num_lunches(start: datetime, end: datetime) -> int: | ||
n = (end.date() - start.date()).days - 1 | ||
if start < start.replace(hour=12, minute=0, second=0): | ||
n += 1 | ||
if end > end.replace(hour=12, minute=45, second=0): | ||
n += 1 | ||
return max(n, 0) | ||
|
||
|
||
def num_nights(start: datetime, end: datetime) -> int: | ||
n = (end.date() - start.date()).days - 1 | ||
if start < start.replace(hour=1, minute=0, second=0): | ||
n += 1 | ||
if end > end.replace(hour=5, minute=0, second=0): | ||
n += 1 | ||
return max(n, 0) | ||
|
||
|
||
def time_worked_on_commit() -> Optional[str]: | ||
now = datetime.now() | ||
last = last_commit_datetime() | ||
# Determine the number of minutes worked on this commit as the number of | ||
# minutes since the last commit minus the lunch breaks and nights. | ||
working_hours_per_day = 8 | ||
working_days_per_week = 5 | ||
minutes = max( | ||
round((now - last).total_seconds() / 60) | ||
- num_nights(last, now) * (24 - working_hours_per_day) * 60 | ||
- num_lunches(last, now) * 45, | ||
0, | ||
) | ||
# Convert the number of minutes worked to working weeks, days, hours, | ||
# minutes. | ||
if minutes > 0: | ||
hours = floor(minutes / 60) | ||
minutes -= hours * 60 | ||
days = floor(hours / working_hours_per_day) | ||
hours -= days * working_hours_per_day | ||
weeks = floor(days / working_days_per_week) | ||
days -= weeks * working_days_per_week | ||
return f"{weeks}w {days}d {hours}h {minutes}m" | ||
return None | ||
|
||
|
||
def main() -> NoReturn: | ||
# https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html | ||
# Exit if the branch name does not contain a Jira issue key. | ||
git_branch_name = current_git_branch_name() | ||
jira_issue_key = extract_jira_issue_key(git_branch_name) | ||
if not jira_issue_key: | ||
sys.exit(0) | ||
# Read the commit message. | ||
commit_msg_filepath = sys.argv[1] | ||
with open(commit_msg_filepath, "r") as f: | ||
commit_msg = f.read() | ||
# Split the commit into a subject and body and apply some light formatting. | ||
commit_elements = commit_msg.split("\n", maxsplit=1) | ||
commit_subject = commit_elements[0].strip() | ||
commit_subject = f"{commit_subject[:1].upper()}{commit_subject[1:]}" | ||
commit_subject = re.sub(r"\.+$", "", commit_subject) | ||
commit_body = None if len(commit_elements) == 1 else commit_elements[1].strip() | ||
# Build the new commit message: | ||
# 1. If there is a body, turn it into a comment on the issue. | ||
if "#comment" not in commit_msg and commit_body: | ||
commit_body = f"{jira_issue_key} #comment {commit_body}" | ||
# 2. Add the time worked to the Work Log in the commit body. | ||
work_time = time_worked_on_commit() | ||
if "#time" not in commit_msg and work_time: | ||
work_log = f"{jira_issue_key} #time {work_time} {commit_subject}" | ||
commit_body = f"{commit_body}\n\n{work_log}" if commit_body else work_log | ||
# 3. Make sure the subject starts with a Jira issue key. | ||
if not extract_jira_issue_key(commit_subject): | ||
commit_subject = f"{jira_issue_key} {commit_subject}" | ||
# Override commit message. | ||
commit_msg = f"{commit_subject}\n\n{commit_body}" if commit_body else commit_subject | ||
with open(commit_msg_filepath, "w") as f: | ||
f.write(commit_msg) | ||
sys.exit(0) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file was deleted.
Oops, something went wrong.