Skip to content

Commit

Permalink
feat(cmd): add commit and complete subcommands
Browse files Browse the repository at this point in the history
This commit adds the `commit` and `complete` subcommands to the command-line interface. The `commit` subcommand allows users to commit their changes to the codebase, while the `complete` subcommand generates tab-completion scripts for different shells.

The `commit` subcommand includes the following changes:
- Added a `commit` module with the `Cmd` struct.
- Added a `commit::tests` module for unit tests.
- Added a `commit::Cmd::api_key` method to retrieve the API key.
- Added a `commit::Cmd::prompt` method to retrieve the prompt.
- Implemented the `Run` trait for the `commit::Cmd` struct.
- Added a `sanitize` function to sanitize commit messages.

The `complete` subcommand includes the following changes:
- Added a `complete` module with the `Cmd` struct.
- Added a `complete::Cmd::shell` field to specify the shell for which to generate the completion script.
- Implemented the `Run` trait for the `complete::Cmd` struct.

The `common::log` module includes the following changes:
- Added a `Level` enum to represent different log levels.
- Implemented the `ValueEnum` trait for the `Level` enum.
- Implemented the `Display` trait for the `Level` enum.
- Added a conversion from `Level` to `LevelFilter`.

The `external::bitwarden` module includes the following changes:
- Added debug logging for the command executed to retrieve the API key.

The `external::git` module includes the following changes:
- Added debug logging for the commands executed to get the status and diff.

The `external::pre_commit` module includes the following changes:
- Added debug logging for the command executed to run the pre-commit hook.

The commit message describes the changes made to the codebase and provides a brief explanation of why the changes were made.
  • Loading branch information
liblaf committed Nov 25, 2023
1 parent bc42c74 commit 1db4f22
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 56 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ anyhow = "1.0.75"
async-openai = "0.16.3"
async-trait = "0.1.74"
clap = { features = ["cargo", "derive", "env"], version = "4.4.8" }
clap-markdown = "0.1.3"
clap_complete = "4.4.4"
colored = "2.0.4"
inquire = "0.6.2"
Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,32 @@ else
EXE :=
endif

all: check
all: check docs

check:
cargo check
cargo clippy

clean:
@ $(RM) --recursive --verbose dist
@ rm --force --recursive --verbose dist
cargo clean

dist: dist/$(NAME)-$(TARGET)$(EXE)

docs: docs/usage.md

###############
# Auxiliaries #
###############

dist/$(NAME)-$(TARGET)$(EXE): target/release/$(NAME)$(EXE)
@ install -D --no-target-directory --verbose $< $@

.PHONY: docs/usage.md
docs/usage.md:
@ mkdir --parents --verbose $(@D)
cargo run complete markdown >$@

.PHONY: target/release/$(NAME)$(EXE)
target/release/$(NAME)$(EXE):
cargo build --release
86 changes: 86 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Command-Line Help for `ai-commit-cli`

This document contains the help content for the `ai-commit-cli` command-line program.

**Command Overview:**

- [Command-Line Help for `ai-commit-cli`](#command-line-help-for-ai-commit-cli)
- [`ai-commit-cli`](#ai-commit-cli) - [**Subcommands:**](#subcommands) - [**Options:**](#options)
- [`ai-commit-cli commit`](#ai-commit-cli-commit) - [**Options:**](#options-1)
- [`ai-commit-cli complete`](#ai-commit-cli-complete) - [**Arguments:**](#arguments)

## `ai-commit-cli`

**Usage:** `ai-commit-cli [OPTIONS] <COMMAND>`

###### **Subcommands:**

- `commit`
- `complete` — Generate tab-completion scripts for your shell

###### **Options:**

- `-l`, `--log-level <LOG_LEVEL>`

Default value: `info`

Possible values: `trace`, `debug`, `info`, `warn`, `error`

## `ai-commit-cli commit`

**Usage:** `ai-commit-cli commit [OPTIONS]`

###### **Options:**

- `-a`, `--api-key <API_KEY>` — If not provided, will use `bw get notes OPENAI_API_KEY`
- `-e`, `--exclude <EXCLUDE>`

Default values: `*-lock.*`, `*.lock`

- `-i`, `--include <INCLUDE>`
- `--no-pre-commit`

Default value: `false`

- `-p`, `--prompt <PROMPT>`
- `--prompt-file <PROMPT_FILE>`
- `--model <MODEL>` — ID of the model to use

Default value: `gpt-3.5-turbo-16k`

- `--max-tokens <MAX_TOKENS>` — The maximum number of tokens to generate in the chat completion

Default value: `500`

- `-n <N>` — How many chat completion choices to generate for each input message

Default value: `1`

- `--temperature <TEMPERATURE>` — What sampling temperature to use, between 0 and 2

Default value: `0`

- `--top-p <TOP_P>` — An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass

Default value: `0.1`

## `ai-commit-cli complete`

Generate tab-completion scripts for your shell

$ ai-commit-cli complete fish >$HOME/.local/share/fish/vendor_completions.d $ ai-commit-cli complete fish >/usr/local/share/fish/vendor_completions.d

**Usage:** `ai-commit-cli complete <SHELL>`

###### **Arguments:**

- `<SHELL>`

Possible values: `markdown`, `bash`, `elvish`, `fish`, `powershell`, `zsh`

<hr/>

<small><i>
This document was generated automatically by
<a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
</i></small>
60 changes: 30 additions & 30 deletions src/cmd/commit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(test)]
mod tests;

use std::path::PathBuf;

use anyhow::Result;
Expand All @@ -15,9 +18,6 @@ use regex::Regex;
use crate::cmd::Run;
use crate::common::log::LogResult;

#[cfg(test)]
mod tests;

#[derive(Debug, Args)]
pub struct Cmd {
/// If not provided, will use `bw get notes OPENAI_API_KEY`
Expand Down Expand Up @@ -62,17 +62,39 @@ pub struct Cmd {

const EXCLUDE: &[&str] = &["*-lock.*", "*.lock"];

impl Cmd {
fn api_key(&self) -> Result<String> {
if let Some(api_key) = self.api_key.as_deref() {
return Ok(api_key.to_string());
}
if let Ok(api_key) = crate::external::bitwarden::get_notes("OPENAI_API_KEY") {
return Ok(api_key);
}
crate::bail!("OPENAI_API_KEY is not provided");
}

fn prompt(&self) -> Result<String> {
if let Some(prompt) = self.prompt.as_deref() {
return Ok(prompt.to_string());
}
if let Some(prompt_file) = self.prompt_file.as_deref() {
return std::fs::read_to_string(prompt_file).log();
}
Ok(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/prompt.md")).to_string())
}
}

#[async_trait::async_trait]
impl Run for Cmd {
async fn run(&self) -> anyhow::Result<()> {
if !self.no_pre_commit {
crate::external::pre_commit::run()?;
}
let exclude: Vec<_> = EXCLUDE.iter().map(PathBuf::from).collect();
let exclude: Vec<_> = exclude.iter().chain(&self.exclude).collect();
crate::external::git::status(&exclude, &self.include)?;
let diff = crate::external::git::diff(&exclude, &self.include)?;
crate::ensure!(!diff.trim().is_empty());
crate::external::git::status(&self.exclude, &self.include)?;
let diff = crate::external::git::diff(&self.exclude, &self.include)?;
if diff.trim().is_empty() {
crate::bail!("no changes added to commit (use \"git add\" and/or \"git commit -a\")");
}
let client = Client::with_config(OpenAIConfig::new().with_api_key(self.api_key()?));
let request = CreateChatCompletionRequestArgs::default()
.messages([
Expand Down Expand Up @@ -123,28 +145,6 @@ impl Run for Cmd {
}
}

impl Cmd {
fn api_key(&self) -> Result<String> {
if let Some(api_key) = self.api_key.as_deref() {
return Ok(api_key.to_string());
}
if let Ok(api_key) = crate::external::bitwarden::get_notes("OPENAI_API_KEY") {
return Ok(api_key);
}
crate::bail!("OPENAI_API_KEY is not provided");
}

fn prompt(&self) -> Result<String> {
if let Some(prompt) = self.prompt.as_deref() {
return Ok(prompt.to_string());
}
if let Some(prompt_file) = self.prompt_file.as_deref() {
return std::fs::read_to_string(prompt_file).log();
}
Ok(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/prompt.md")).to_string())
}
}

fn sanitize<S>(message: S) -> Option<String>
where
S: AsRef<str>,
Expand Down
46 changes: 38 additions & 8 deletions src/cmd/complete.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
use clap::{Args, CommandFactory};
use clap::{builder::PossibleValue, Args, CommandFactory, ValueEnum};
use clap_complete::Shell;

use crate::cmd::Run;

/// Generate tab-completion scripts for your shell
///
/// $ ai-commit-cli complete fish >$HOME/.local/share/fish/vendor_completions.d
/// $ ai-commit-cli complete fish >/usr/local/share/fish/vendor_completions.d
#[derive(Debug, Args)]
pub struct Cmd {
shell: Shell,
shell: Generator,
}

#[derive(Clone, Debug)]
enum Generator {
Markdown,
Shell(Shell),
}

impl ValueEnum for Generator {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Markdown,
Self::Shell(Shell::Bash),
Self::Shell(Shell::Elvish),
Self::Shell(Shell::Fish),
Self::Shell(Shell::PowerShell),
Self::Shell(Shell::Zsh),
]
}

fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
match self {
Self::Markdown => Some(PossibleValue::new("markdown")),
Self::Shell(shell) => shell.to_possible_value(),
}
}
}

#[async_trait::async_trait]
impl Run for Cmd {
async fn run(&self) -> anyhow::Result<()> {
let cmd = &mut crate::cmd::Cmd::command();
clap_complete::generate(
self.shell,
cmd,
cmd.get_name().to_string(),
&mut std::io::stdout(),
);
match self.shell {
Generator::Markdown => clap_markdown::print_help_markdown::<crate::cmd::Cmd>(),
Generator::Shell(shell) => clap_complete::generate(
shell,
cmd,
cmd.get_name().to_string(),
&mut std::io::stdout(),
),
}
Ok(())
}
}
10 changes: 5 additions & 5 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
mod commit;
mod complete;

use anyhow::Result;
use clap::builder::styling::AnsiColor;
use clap::builder::Styles;
use clap::{Parser, Subcommand};

use crate::common::log::Level;

mod commit;
mod complete;

#[derive(Debug, Parser)]
#[command(version, author, styles = STYLES)]
pub struct Cmd {
Expand Down Expand Up @@ -41,14 +41,14 @@ impl Run for Cmd {
if self.log_level < Level::Info {
tracing_subscriber::fmt()
.pretty()
.with_max_level(self.log_level.as_level())
.with_max_level(self.log_level.to_owned())
.init();
} else {
tracing_subscriber::fmt()
.pretty()
.with_file(false)
.with_line_number(false)
.with_max_level(self.log_level.as_level())
.with_max_level(self.log_level.to_owned())
.with_target(false)
.without_time()
.init();
Expand Down
20 changes: 11 additions & 9 deletions src/common/log.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::{fmt::Display, panic::Location};
use std::fmt::Display;
use std::panic::Location;

use clap::ValueEnum;
use tracing::level_filters::LevelFilter;

#[derive(Clone, Debug, ValueEnum, PartialEq, PartialOrd)]
pub enum Level {
Expand Down Expand Up @@ -33,14 +35,14 @@ impl Display for Level {
}
}

impl Level {
pub fn as_level(&self) -> tracing::Level {
match self {
Self::Trace => tracing::Level::TRACE,
Self::Debug => tracing::Level::DEBUG,
Self::Info => tracing::Level::INFO,
Self::Warn => tracing::Level::WARN,
Self::Error => tracing::Level::ERROR,
impl From<Level> for LevelFilter {
fn from(value: Level) -> LevelFilter {
match value {
Level::Trace => tracing::Level::TRACE.into(),
Level::Debug => tracing::Level::DEBUG.into(),
Level::Info => tracing::Level::INFO.into(),
Level::Warn => tracing::Level::WARN.into(),
Level::Error => tracing::Level::ERROR.into(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/external/bitwarden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ where
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
tracing::debug!("{:?}", cmd);
let output = cmd.output().log()?;
crate::ensure!(output.status.success());
String::from_utf8(output.stdout).log()
Expand Down
Loading

0 comments on commit 1db4f22

Please sign in to comment.