Repository: abi/autocommit
Branch: main
Commit: 7a4e9af425c7
Files: 13
Total size: 17.2 KB
Directory structure:
gitextract_ahig21ds/
├── .gitattributes
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── LICENSE
├── README.md
├── autocommit/
│ ├── __init__.py
│ ├── commit.py
│ └── llm.py
├── null
├── pyproject.toml
├── requirements.txt
└── scan_repo.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Mac
.DS_Store
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.tabCompletion": "on",
"diffEditor.codeLens": true,
"python.analysis.typeCheckingMode": "basic",
"python.formatting.provider": "black"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Abi Raja
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# aicommit - AI-generated Git commit messages
A simple CLI tool that generates 5 commit message suggestions for the changes in your current Git repo. After you pick and edit the commit message you want, it commits the changes.

### Installation
**`pip install aicommit`**
On first run, it will prompt you for your OpenAI API key. Sign up for OpenAI if you haven't. Grab your API key by going to the dropdown on the top right, selecting "View API Keys" and creating a new key. Copy this key.
**NOTE:** it commits all changes, untracked and unstaged, in your current repo.
# Feedback/thoughts
Ping me on [Twitter](https://twitter.com/_abi_)
## scan_repo
`scan_repo` runs through all the commits in your repository to generate a CSV with AI-suggested commit messages side-by-side with your original commit messages. [Read more about this tool here](https://abiraja.substack.com/p/ai-generated-git-commit-messages)
To run scan_repo, copy `.env.example` to `.env` and add your OPENAI_KEY.
To update the repo it runs on, modify the `GITHUB_REPO_URL` variable at the top of `scan_repo.py`
# Publishing to pip
Version bump and clear out `dist/`
```
python3 -m build
twine check dist/*
twine upload dist/* --verbose
```
================================================
FILE: autocommit/__init__.py
================================================
================================================
FILE: autocommit/commit.py
================================================
from __future__ import print_function, unicode_literals
from PyInquirer import prompt as py_inquirer_prompt, style_from_dict, Token
import subprocess
import keyring
import os
import re
import markdown
from pydantic import BaseModel
from langchain.llms import OpenAI
from langchain.prompts import BasePromptTemplate
from dotenv import load_dotenv
load_dotenv()
OPENAI_KEY = os.getenv("OPENAI_KEY")
class CustomPromptTemplate(BasePromptTemplate, BaseModel):
template: str
def format(self, **kwargs) -> str:
c_kwargs = {k: v for k, v in kwargs.items()}
return self.template.format(**c_kwargs)
prompt = CustomPromptTemplate(
input_variables=["diff"],
template="""
What follows "-------" is a git diff for a potential commit.
Reply with a markdown unordered list of 5 possible, different Git commit messages
(a Git commit message should be concise but also try to describe
the important changes in the commit), order the list by what you think
would be the best commit message first, and don't include any other text
but the 5 messages in your response.
-------
{diff}
-------
""",
)
def generate_suggestions(diff, openai_api_key=OPENAI_KEY):
llm = OpenAI(
temperature=0.2,
openai_api_key=openai_api_key,
max_tokens=100,
model_name="text-davinci-003",
) # type: ignore
# query OpenAI
formattedPrompt = prompt.format(diff=diff)
response = llm(formattedPrompt)
# Convert the markdown string to HTML
html = markdown.markdown(response)
# Use a regular expression to extract the list items from the HTML
items = re.findall(r"
(.*?)", html)
return items
SERVICE_ID = "auto-commit-cli"
def prompt_for_openai_api_key():
questions = [
{
"type": "input",
"name": "openai_api_key",
"message": "Please enter your OpenAI API key:",
}
]
answers = py_inquirer_prompt(questions)
openai_api_key = answers["openai_api_key"]
keyring.set_password(SERVICE_ID, "user", openai_api_key)
return openai_api_key
def main():
# Prompt for OpenAI API key if it's not set
openai_api_key = keyring.get_password(SERVICE_ID, "user")
if openai_api_key is None:
openai_api_key = prompt_for_openai_api_key()
# Get the diff including untracked files (see https://stackoverflow.com/a/52093887)
git_command = "git --no-pager diff; for next in $( git ls-files --others --exclude-standard ) ; do git --no-pager diff --no-index /dev/null $next; done;"
# Windows support
if os.name == "nt":
git_command = """git diff && for /f "delims=" %a in ('git ls-files --others --exclude-standard') do (git diff --no-index /dev/null %a)"""
output = subprocess.run(
git_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if len(output.stderr) > 0:
error = output.stderr.decode("utf-8")
print("There was an error retrieving the current diff: ")
if error.startswith("warning: Not a git repository"):
print("You're not inside a git repo. Please run it from inside a git repo.")
else:
print(error)
exit(-1)
diff = output.stdout.decode("utf8")
# Trim the diff
diff = diff.strip()
if len(diff) == 0:
print("Diff is empty. Nothing to commit.")
exit(0)
suggestions = []
try:
suggestions = generate_suggestions(diff[:7000], openai_api_key=openai_api_key)
except Exception as e:
print("There was an error generating suggestions from OpenAI: ")
# Prompt for OpenAI API key if it's incorrect
if "Incorrect API key provided" in str(e):
openai_api_key = prompt_for_openai_api_key()
print("Please re-run the command now.")
else:
print(e)
exit(-1)
if len(suggestions) == 0:
print("No suggestions found.")
exit(0)
# Prompt user with commit messages and choices and allow edits
custom_style = style_from_dict(
{
Token.Separator: "#6C6C6C",
Token.QuestionMark: "#000000",
Token.Selected: "#FFFF00 bold",
Token.Pointer: "#FF9D00 bold",
Token.Instruction: "",
Token.Answer: "#EA9104 bold",
Token.Question: "",
}
)
questions = [
{
"type": "list",
"name": "commit_message",
"message": "Commit message suggestions:",
"choices": [f"{i + 1}. {item}" for i, item in enumerate(suggestions)],
"filter": lambda val: val[3:],
}
]
answers = py_inquirer_prompt(questions, style=custom_style)
answers = py_inquirer_prompt(
[
{
"type": "input",
"name": "final_commit_message",
"message": "Confirm or edit the commit message:",
"default": answers.get("commit_message"),
},
]
)
# Commit the changes
git_command = (
'git add -A; git commit -m "' + answers.get("final_commit_message") + '"'
)
# Windows support
if os.name == "nt":
git_command = (
'git add -A && git commit -m "' + answers.get("final_commit_message") + '"'
)
output = subprocess.run(
git_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if len(output.stderr) > 0:
error = output.stderr.decode("utf-8")
print("There was an error committing:")
print(error)
else:
print("Commit successful with message:", answers.get("final_commit_message"))
if __name__ == "__main__":
main()
================================================
FILE: autocommit/llm.py
================================================
import os
import re
import markdown
from pydantic import BaseModel
from langchain.llms import OpenAI
from langchain.prompts import BasePromptTemplate
from dotenv import load_dotenv
load_dotenv()
OPENAI_KEY = os.getenv("OPENAI_KEY")
class CustomPromptTemplate(BasePromptTemplate, BaseModel):
template: str
def format(self, **kwargs) -> str:
c_kwargs = {k: v for k, v in kwargs.items()}
return self.template.format(**c_kwargs)
prompt = CustomPromptTemplate(
input_variables=["diff"], template="""
What follows "-------" is a git diff for a potential commit.
Reply with a markdown unordered list of 5 possible, different Git commit messages
(a Git commit message should be concise but also try to describe
the important changes in the commit), order the list by what you think
would be the best commit message first, and don't include any other text
but the 5 messages in your response.
-------
{diff}
-------
""")
def generate_suggestions(diff, openai_api_key=OPENAI_KEY):
llm = OpenAI(temperature=0.2, openai_api_key=openai_api_key,
max_tokens=100, model_name="text-davinci-003") # type: ignore
# query OpenAI
formattedPrompt = prompt.format(diff=diff)
response = llm(formattedPrompt)
# Convert the markdown string to HTML
html = markdown.markdown(response)
# Use a regular expression to extract the list items from the HTML
items = re.findall(r'(.*?)', html)
return items
================================================
FILE: null
================================================
================================================
FILE: pyproject.toml
================================================
[project]
name = "aicommit"
version = "0.1.0"
authors = [
{ name="Abi Raja", email="abimanyuraja@gmail.com" },
]
description = "Generate AI powered commit messages"
readme = "README.md"
requires-python = ">=3.7"
dependencies = [
'async-timeout ~= 4.0',
'black ~= 22.12.0',
'certifi ~= 2022.12.7',
'cffi ~= 1.15.1',
'charset-normalizer ~= 2.1.1',
'click ~= 8.1.3',
'et-xmlfile ~= 1.1.0',
'gitdb ~= 4.0.10',
'GitPython ~= 3.1.29',
'idna ~= 3.4',
'importlib-metadata ~= 5.2.0',
'jaraco.classes ~= 3.2.3',
'keyring ~= 23.13.1',
'langchain ~= 0.0.45',
'Markdown ~= 3.4.1',
'more-itertools ~= 9.0.0',
'mypy-extensions ~= 0.4.3',
'numpy ~= 1.24.0',
'openai ~= 0.25.0',
'openpyxl ~= 3.0.10',
'pandas ~= 1.5.2',
'pandas-stubs ~= 1.5.2.221213',
'pathspec ~= 0.10.3',
'platformdirs ~= 2.6.0',
'prompt-toolkit ~= 1.0.14',
'pycparser ~= 2.21',
'pydantic ~= 1.10.2',
'pygit2 ~= 1.11.1',
'Pygments ~= 2.13.0',
'PyInquirer ~= 1.0.3',
'python-dateutil ~= 2.8.2',
'python-dotenv ~= 0.21.0',
'pytz ~= 2022.7',
'PyYAML ~= 6.0',
'redis ~= 4.4.0',
'regex ~= 2022.10.31',
'requests ~= 2.28.1',
'six ~= 1.16.0',
'smmap ~= 5.0.0',
'SQLAlchemy ~= 1.4.45',
'tomli ~= 2.0.1',
'tqdm ~= 4.64.1',
'types-pytz ~= 2022.7.0.0',
'typing_extensions ~= 4.4.0',
'urllib3 ~= 1.26.13',
'wcwidth ~= 0.2.5',
'zipp ~= 3.11.0'
]
[project.urls]
"Homepage" = "https://github.com/abi/autocommit"
"Bug Tracker" = "https://github.com/abi/autocommit/issues"
[project.scripts]
aicommit = "autocommit.commit:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
================================================
FILE: requirements.txt
================================================
async-timeout==4.0.2
black==22.12.0
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==2.1.1
click==8.1.3
et-xmlfile==1.1.0
gitdb==4.0.10
GitPython==3.1.29
idna==3.4
importlib-metadata==5.2.0
jaraco.classes==3.2.3
keyring==23.13.1
langchain==0.0.45
Markdown==3.4.1
more-itertools==9.0.0
mypy-extensions==0.4.3
numpy==1.24.0
openai==0.25.0
openpyxl==3.0.10
pandas==1.5.2
pandas-stubs==1.5.2.221213
pathspec==0.10.3
platformdirs==2.6.0
prompt-toolkit==1.0.14
pycparser==2.21
pydantic==1.10.2
pygit2==1.11.1
Pygments==2.13.0
PyInquirer==1.0.3
python-dateutil==2.8.2
python-dotenv==0.21.0
pytz==2022.7
PyYAML==6.0
redis==4.4.0
regex==2022.10.31
requests==2.28.1
six==1.16.0
smmap==5.0.0
SQLAlchemy==1.4.45
tomli==2.0.1
tqdm==4.64.1
types-pytz==2022.7.0.0
typing_extensions==4.4.0
urllib3==1.26.13
wcwidth==0.2.5
zipp==3.11.0
================================================
FILE: scan_repo.py
================================================
from collections import namedtuple
import os
import shutil
import csv
import sys
from dotenv import load_dotenv
import git
import pygit2
from autocommit.llm import generate_suggestions
load_dotenv()
GIT_REPO_URL = os.getenv("GIT_REPO_URL")
if GIT_REPO_URL is None:
raise ValueError("GIT_REPO_URL is not set")
temp_repo_dir = "/tmp/ai-commit-msg-repo"
# Delete the directory if it exists
if os.path.exists(temp_repo_dir):
shutil.rmtree(temp_repo_dir)
# Clone the repository
git.Repo.clone_from(GIT_REPO_URL, temp_repo_dir)
# Open the repository
repo = pygit2.Repository(temp_repo_dir)
commits = repo.walk(repo.head.target, pygit2.GIT_SORT_TIME)
# Iterate over the commits and organize the data we need
commit_objects = []
CommitObject = namedtuple("CommitObject", ["sha", "message", "diff"])
for commit in commits:
if len(commit.parents) > 0:
diff = repo.diff(commit.parents[0], commit).patch
else:
diff = ""
commit_objects.append(CommitObject(commit.id, commit.message, diff))
filtered_commit_objects = commit_objects
writer = csv.writer(sys.stdout, quoting=csv.QUOTE_MINIMAL)
for commit in filtered_commit_objects:
message = commit.message.replace("\n", ";")
# Skip merge commits
if message.startswith("Merge"):
continue
# text-davinci-003 supports 4000 tokens. Let's use upto 3500 tokens for the prompt.
# 3500 tokens = 14,000 characters (https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them)
# But in practice, > 7000 seems to exceed the limit
suggestions = generate_suggestions(commit.diff[:7000])
# generate CSV row
for item in suggestions:
# Only inlude SHA & original message for the first row
if item == suggestions[0]:
writer.writerow([commit.sha, message, item])
else:
writer.writerow(["", "", item])