Skip to content

Commit

Permalink
feat: add commit command to generate commit messages using LLM
Browse files Browse the repository at this point in the history
- Integrates a new command to automatically generate commit messages using a language model
- Enhances the CLI with a streamlined process for creating meaningful commit messages
- Adds utility functions to extract content between tags and run subprocesses asynchronously
  • Loading branch information
liblaf committed Nov 25, 2024
1 parent 239e765 commit 813593c
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/llm_cli/assets/prompts/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ${GIT_DIFF}
Now, if provided, use this context to understand the motivation behind the changes and any relevant background information:
<AdditionalContext>
<RepositoryStructure>
${FILES}
${GIT_FILES}
</RepositoryStructure>
</AdditionalContext>

Expand Down Expand Up @@ -45,6 +45,7 @@ The commit message should be structured as follows:
```

- lines must not be longer than 74 characters
- use a markdown list for the optional body if it contains multiple items
- choose only 1 type from the type-to-description below:
- feat: Introduce new features
- fix: Fix a bug
Expand Down
4 changes: 2 additions & 2 deletions src/llm_cli/cmd/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import repo
from . import commit, repo
from ._app import app

__all__ = ["app", "repo"]
__all__ = ["app", "commit", "repo"]
5 changes: 3 additions & 2 deletions src/llm_cli/cmd/_app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import typer

import llm_cli.cmd as lc
import llm_cli.utils as lu
from llm_cli import cmd

app: typer.Typer = typer.Typer(name="llm-cli", no_args_is_help=True)
lu.add_command(app, lc.repo.app)
lu.add_command(app, cmd.repo.app)
lu.add_command(app, cmd.commit.app)
3 changes: 3 additions & 0 deletions src/llm_cli/cmd/commit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import lazy_loader as lazy

__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
4 changes: 4 additions & 0 deletions src/llm_cli/cmd/commit/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._app import app
from ._main import main

__all__ = ["app", "main"]
12 changes: 12 additions & 0 deletions src/llm_cli/cmd/commit/_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import asyncio

import typer

app = typer.Typer(name="commit")


@app.command()
def main() -> None:
from ._main import main

asyncio.run(main())
30 changes: 30 additions & 0 deletions src/llm_cli/cmd/commit/_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import asyncio
import string

import git
import litellm
import typer

import llm_cli as lc
import llm_cli.utils as lu


async def main(*, verify: bool = True) -> None:
prompt_template = string.Template(lu.get_prompt("commit"))
repo = git.Repo(search_parent_directories=True)
diff: str = repo.git.diff("--cached", "--no-ext-diff")
files: str = repo.git.ls_files()
prompt: str = prompt_template.substitute({"GIT_DIFF": diff, "GIT_FILES": files})
resp: litellm.ModelResponse = await lc.output(prompt, prefix="<Answer>")
choices: litellm.Choices = resp.choices[0] # pyright: ignore [reportAssignmentType]
message: str = lu.extract_between_tags(choices.message.content)
proc: asyncio.subprocess.Process = await lu.run(
"git",
"commit",
f"--message={message}",
"--verify" if verify else "--no-verify",
"--edit",
check=False,
)
if proc.returncode:
raise typer.Exit(proc.returncode)
3 changes: 2 additions & 1 deletion src/llm_cli/cmd/repo/description/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._app import app
from ._main import main

__all__ = ["app"]
__all__ = ["app", "main"]
3 changes: 2 additions & 1 deletion src/llm_cli/cmd/repo/topics/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._app import app
from ._main import main

__all__ = ["app"]
__all__ = ["app", "main"]
7 changes: 5 additions & 2 deletions src/llm_cli/interactive/_output.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from typing import Any

import litellm
Expand All @@ -7,12 +7,14 @@
from rich.panel import Panel

import llm_cli as lc
import llm_cli.utils as lu


async def output(
prompt: str,
*,
prefix: str | None = None,
sanitize: Callable[[str], str] | None = lu.extract_between_tags,
stop: str | Sequence[str] | None = None,
title: str | None = None,
) -> litellm.ModelResponse:
Expand All @@ -36,7 +38,8 @@ async def output(
response = litellm.stream_chunk_builder(chunks) # pyright: ignore [reportAssignmentType]
choices: litellm.Choices = response.choices[0] # pyright: ignore [reportAssignmentType]
content: str = choices.message.content or ""
content = content.strip()
if sanitize:
content = sanitize(content)
live.update(
Group(
Panel(content, title=title, title_align="left"),
Expand Down
11 changes: 10 additions & 1 deletion src/llm_cli/utils/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from . import git
from ._add_command import add_command
from ._extract_between_tags import extract_between_tags
from ._get_app_dir import get_app_dir
from ._get_prompt import get_prompt
from ._repomix import repomix
from ._run import run

__all__ = ["add_command", "get_app_dir", "get_prompt", "git", "repomix", "run"]
__all__ = [
"add_command",
"extract_between_tags",
"get_app_dir",
"get_prompt",
"git",
"repomix",
"run",
]
11 changes: 11 additions & 0 deletions src/llm_cli/utils/_extract_between_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def extract_between_tags(content: str | None, tag: str = "Answer") -> str:
if content is None:
return ""
start: int = content.find("<" + tag + ">")
if start >= 0:
start += len(tag) + 2
content = content[start:]
end: int = content.find("</" + tag + ">")
if end >= 0:
content = content[:end]
return content.strip()
3 changes: 2 additions & 1 deletion src/llm_cli/utils/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

async def run(
program: _StrOrBytesPath, *args: _StrOrBytesPath, check: bool = True
) -> None:
) -> asp.Process:
proc: asp.Process = await asp.create_subprocess_exec(program, *args)
returncode: int = await proc.wait()
if check and returncode != 0:
raise subprocess.CalledProcessError(returncode, [program, *args])
return proc

0 comments on commit 813593c

Please sign in to comment.