From 617dfc6736c3983f020870623eca3e4bd1d362d9 Mon Sep 17 00:00:00 2001 From: Roger Zurawicki Date: Wed, 22 Feb 2023 12:42:02 -0500 Subject: [PATCH 1/4] Improve local config list and set handling [e2e/test_config_local_list_get_set.sh] - Add a temporary directory and git init to the test script - Add checks to ensure the output of `gptcommit config list` is valid TOML - Add checks to ensure the default value is returned when deleting a local configuration setting --- e2e/test_config_local_list_get_set.sh | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/e2e/test_config_local_list_get_set.sh b/e2e/test_config_local_list_get_set.sh index f887f1e..b42a3f6 100755 --- a/e2e/test_config_local_list_get_set.sh +++ b/e2e/test_config_local_list_get_set.sh @@ -1,19 +1,26 @@ #!/bin/sh set -eu -gptcommit config list -# assert is valid TOML +export TEMPDIR=$(mktemp -d) +( + cd "${TEMPDIR}" + git init -gptcommit config get openai.model -# assert default = text-davinci-003 -gptcommit config set --local openai.model foo -gptcommit config set openai.model bar -gptcommit config get openai.model -# assert is foo + gptcommit config list + # assert is valid TOML -gptcommit config delete openai.model -gptcommit config get openai.model -# assert still is foo -gptcommit config delete --local openai.model -gptcommit config get openai.model -# assert is default + gptcommit config get openai.model + # assert default = text-davinci-003 + gptcommit config set --local openai.model foo + gptcommit config set openai.model bar + gptcommit config get openai.model + # assert is foo + + gptcommit config delete openai.model + gptcommit config get openai.model + # assert still is foo + gptcommit config delete --local openai.model + gptcommit config get openai.model + # assert is default +) +rm -rf "${TEMPDIR}" From a261981a90ce2209dde823340f849353f9be652c Mon Sep 17 00:00:00 2001 From: Roger Zurawicki Date: Wed, 22 Feb 2023 14:18:32 -0500 Subject: [PATCH 2/4] Update config handling and add e2e tests - Add tests for local list get and set of OpenAI model - Add `get_config_path` function for retrieving either local or user config path - Update `delete` and `set` functions to use `get_config_path` function - Add async-std dependency and update anyhow and async-trait versions [e2e/test_config_local_list_get_set.sh] - Add tests for local list get and set of OpenAI model - Assert that local config set and delete fail when not using --local flag - Assert that local config get returns the correct value after setting and deleting with --local flag [src/actions/config.rs] - Add `async_std::path::Path` to the imports - Change the `get_config_path` function to handle both local and user config paths - Add `get_config_path` function for retrieving either local or user config path - Add error handling to the `delete` and `set` functions for when no config path is found - Update the `delete` and `set` functions to use the `get_config_path` function [Cargo.toml] - Add async-std dependency - Update anyhow and async-trait versions --- Cargo.toml | 1 + e2e/test_config_local_list_get_set.sh | 32 +++++++++++++++++++++++++++ src/actions/config.rs | 29 +++++++++++++++--------- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e0ef898..208dbc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.69" +async-std = "1.12.0" async-trait = "0.1.64" clap = { version = "4.1.6", features = ["derive"] } colored = "2.0.0" diff --git a/e2e/test_config_local_list_get_set.sh b/e2e/test_config_local_list_get_set.sh index b42a3f6..265d961 100755 --- a/e2e/test_config_local_list_get_set.sh +++ b/e2e/test_config_local_list_get_set.sh @@ -24,3 +24,35 @@ export TEMPDIR=$(mktemp -d) # assert is default ) rm -rf "${TEMPDIR}" + + +export TEMPDIR=$(mktemp -d) +( + cd "${TEMPDIR}" + + gptcommit config list + # assert is valid TOML + + gptcommit config get openai.model + # assert default = text-davinci-003 + set +e + gptcommit config set --local openai.model foo + # TODO assert output + test $? -ne 0 || exit $? + set -e + gptcommit config set openai.model bar + gptcommit config get openai.model + # assert is foo + + gptcommit config delete openai.model + gptcommit config get openai.model + # assert still is foo + set +e + gptcommit config delete --local openai.model + # TODO assert output + test $? -ne 0 || exit $? + set -e + gptcommit config get openai.model + # assert is default +) +rm -rf "${TEMPDIR}" diff --git a/src/actions/config.rs b/src/actions/config.rs index a1dfa1e..94a1fe4 100644 --- a/src/actions/config.rs +++ b/src/actions/config.rs @@ -1,5 +1,6 @@ use std::{collections::VecDeque, fs, path::PathBuf}; +use async_std::path::Path; use clap::{Args, Subcommand}; use toml::Value; @@ -50,14 +51,26 @@ pub(crate) async fn main(settings: Settings, args: ConfigArgs) -> Result<()> { } } +fn get_config_path(local: bool) -> Result { + if local { + if let Some(config_path) = get_local_config_path() { + Ok(config_path) + } else { + bail!("No repo-local config found. Please run `git init` to create a repo first"); + } + } else { + if let Some(config_path) = get_user_config_path() { + Ok(config_path) + } else { + bail!("No user config found."); + } + } +} + async fn delete(_settings: Settings, full_key: String, local: bool) -> Result<()> { let settings = &Settings::from_clear(&full_key)?; let toml_string = toml::to_string_pretty(settings).unwrap(); - let config_path: PathBuf = if local { - get_local_config_path().expect("Could not find repo-local config path") - } else { - get_user_config_path().expect("Could not find user config path") - }; + let config_path = get_config_path(local)?; fs::write(&config_path, toml_string)?; println!("Cleared {full_key}"); println!("Config saved to {}", config_path.display()); @@ -67,11 +80,7 @@ async fn delete(_settings: Settings, full_key: String, local: bool) -> Result<() async fn set(_settings: Settings, full_key: String, value: String, local: bool) -> Result<()> { let settings = &Settings::from_set_override(&full_key, &value)?; let toml_string = toml::to_string_pretty(settings).unwrap(); - let config_path: PathBuf = if local { - get_local_config_path().expect("Could not find repo-local config path") - } else { - get_user_config_path().expect("Could not find user config path") - }; + let config_path = get_config_path(local)?; fs::write(&config_path, toml_string)?; println!("{full_key} = {value}"); println!("Config saved to {}", config_path.display()); From 17ff13e094259b295afadae39a4765f4ed706867 Mon Sep 17 00:00:00 2001 From: Roger Zurawicki Date: Wed, 22 Feb 2023 17:31:40 -0500 Subject: [PATCH 3/4] Update config API and add dependencies --- Cargo.lock | 25 +++---- Cargo.toml | 1 + README.md | 3 +- e2e/test_config_list_get_set.sh | 1 + src/actions/config.rs | 26 ++++++-- src/main.rs | 1 + src/toml.rs | 115 ++++++++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 src/toml.rs diff --git a/Cargo.lock b/Cargo.lock index 5f2cf6d..53b7bda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -730,6 +730,7 @@ dependencies = [ "tiktoken-rs", "tokio", "toml 0.7.2", + "toml_edit", "which", ] @@ -1095,15 +1096,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "num_cpus" version = "1.15.0" @@ -1969,15 +1961,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" dependencies = [ "indexmap", - "nom8", "serde", "serde_spanned", "toml_datetime", + "winnow", ] [[package]] @@ -2394,6 +2386,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winnow" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdd927d1a3d5d98abcfc4cf8627371862ee6abfe52a988050621c50c66b4493" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 208dbc7..2839f6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ tera = { version = "1.17.1", default-features = false } tiktoken-rs = "0.1.2" tokio = { version = "1.25.0", features = ["full"] } toml = "0.7.2" +toml_edit = "0.19.4" which = "4.4.0" [dev-dependencies] diff --git a/README.md b/README.md index b8b6145..4117dbf 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Configs are applied in the following order: - The settings as read from the repo clone at `$GIT_ROOT/.git/gptcommit.toml`. - Environment variables starting with `GPTCOMMIT__*`. +See all the config options available with `gptcommit config keys`. + ### Set your OpenAI API key Persist your OpenAI key @@ -139,7 +141,6 @@ All of these awesome projects are built using `gptcommit`. If you encounter any bugs or have any suggestions for improvements, please open an issue on the repository. - ## License This project is licensed under the [MIT License](./LICENSE). diff --git a/e2e/test_config_list_get_set.sh b/e2e/test_config_list_get_set.sh index 48a2a52..85ac50f 100755 --- a/e2e/test_config_list_get_set.sh +++ b/e2e/test_config_list_get_set.sh @@ -3,6 +3,7 @@ set -eu gptcommit config list # assert is valid TOML +gptcommit config keys gptcommit config get openai.model # assert default = text-davinci-003 diff --git a/src/actions/config.rs b/src/actions/config.rs index 94a1fe4..984e13b 100644 --- a/src/actions/config.rs +++ b/src/actions/config.rs @@ -1,14 +1,18 @@ use std::{collections::VecDeque, fs, path::PathBuf}; -use async_std::path::Path; use clap::{Args, Subcommand}; use toml::Value; -use crate::settings::{get_local_config_path, get_user_config_path, Settings}; +use crate::{ + settings::{get_local_config_path, get_user_config_path, Settings}, + toml::DeepKeysCollector, +}; use anyhow::{bail, Result}; #[derive(Subcommand, Debug)] pub(crate) enum ConfigAction { + /// List all config keys + Keys, /// List all config values List { /// if set, will save the config to the user's config file @@ -44,6 +48,7 @@ pub(crate) async fn main(settings: Settings, args: ConfigArgs) -> Result<()> { debug!("Config subcommand - Settings = {:?}", settings); match args.action { + ConfigAction::Keys => keys(settings).await, ConfigAction::List { save } => list(settings, save).await, ConfigAction::Get { key } => get(settings, key).await, ConfigAction::Set { key, value, local } => set(settings, key, value, local).await, @@ -58,13 +63,20 @@ fn get_config_path(local: bool) -> Result { } else { bail!("No repo-local config found. Please run `git init` to create a repo first"); } + } else if let Some(config_path) = get_user_config_path() { + Ok(config_path) } else { - if let Some(config_path) = get_user_config_path() { - Ok(config_path) - } else { - bail!("No user config found."); - } + bail!("No user config found."); + } +} + +async fn keys(settings: Settings) -> Result<()> { + let toml_string = toml::to_string_pretty(&settings).unwrap(); + let keys = DeepKeysCollector::get_keys(toml_string); + for key in keys { + println!("{key}"); } + Ok(()) } async fn delete(_settings: Settings, full_key: String, local: bool) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 82df57c..bdd0fd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod cmd; mod git; mod prompt; mod summarize; +mod toml; mod util; use anyhow::Result; diff --git a/src/toml.rs b/src/toml.rs new file mode 100644 index 0000000..533bc5d --- /dev/null +++ b/src/toml.rs @@ -0,0 +1,115 @@ +use toml_edit::{visit::*, Document, Item, Value}; + +#[derive(Default)] +pub(crate) struct DeepKeysCollector<'doc> { + current_path: Vec<&'doc str>, + pub keys: Vec, +} + +impl DeepKeysCollector<'_> { + pub fn get_keys(toml_string: String) -> Vec { + let document: Document = toml_string.parse().unwrap(); + let mut visitor = DeepKeysCollector::default(); + visitor.visit_document(&document); + + visitor.keys.sort(); + visitor.keys + } +} + +impl<'doc> Visit<'doc> for DeepKeysCollector<'doc> { + fn visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item) { + self.current_path.push(key); + self.visit_item(node); + self.current_path.pop(); + } + + fn visit_value(&mut self, node: &'doc Value) { + match node { + Value::InlineTable(_table) => {} + _ => { + self.keys.push(self.current_path.join(".")); + } + }; + + match node { + Value::String(s) => self.visit_string(s), + Value::Integer(i) => self.visit_integer(i), + Value::Float(f) => self.visit_float(f), + Value::Boolean(b) => self.visit_boolean(b), + Value::Datetime(dt) => self.visit_datetime(dt), + Value::Array(array) => self.visit_array(array), + Value::InlineTable(table) => self.visit_inline_table(table), + } + } +} + +#[cfg(test)] +mod tests { + use toml_edit::Document; + + use crate::settings::Settings; + + use super::*; + + #[test] + fn test_basic() { + let input = r#" +laputa = "sky-castle" +the-force = { value = "surrounds-you" } +"#; + + let document: Document = input.parse().unwrap(); + let mut visitor = DeepKeysCollector::default(); + visitor.visit_document(&document); + + assert_eq!(visitor.current_path, Vec::<&str>::new()); + assert_eq!(visitor.keys, vec!["laputa", "the-force.value"]); + } + + #[test] + fn test_default_config() { + let input = toml::to_string_pretty(&Settings::new().unwrap()).unwrap(); + + let document: Document = input.parse().unwrap(); + let mut visitor = DeepKeysCollector::default(); + visitor.visit_document(&document); + + assert_eq!(visitor.current_path, Vec::<&str>::new()); + visitor.keys.sort(); + assert_eq!( + visitor.keys, + vec![ + "allow_amend", + "model_provider", + "openai.api_key", + "openai.model", + "output.lang", + "prompt.commit_summary", + "prompt.commit_title", + "prompt.file_diff", + "prompt.translation", + ] + ); + } + + #[test] + fn test_get_keys() { + let input = toml::to_string_pretty(&Settings::new().unwrap()).unwrap(); + + assert_eq!( + DeepKeysCollector::get_keys(input), + vec![ + "allow_amend", + "model_provider", + "openai.api_key", + "openai.model", + "output.lang", + "prompt.commit_summary", + "prompt.commit_title", + "prompt.file_diff", + "prompt.translation", + ] + ); + } +} From a8c060185511512cfdbf68f6a588debeec7901e2 Mon Sep 17 00:00:00 2001 From: Roger Zurawicki Date: Wed, 22 Feb 2023 22:53:58 +0000 Subject: [PATCH 4/4] Add Dev container --- .devcontainer/devcontainer.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bfe1846 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye", + // Use 'mounts' to make the cargo cache persistent in a Docker Volume. + // "mounts": [ + // { + // "source": "devcontainer-cargo-cache-${devcontainerId}", + // "target": "/usr/local/cargo", + // "type": "volume" + // } + // ] + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cargo install just", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file