diff --git a/Cargo.lock b/Cargo.lock index c108a7c..0039c84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,8 +74,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -147,7 +145,6 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" name = "ferro" version = "0.1.0" dependencies = [ - "git2", "reqwest", "serde", "serde_json", @@ -250,21 +247,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "git2" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "openssl-probe", - "openssl-sys", - "url", -] - [[package]] name = "h2" version = "0.4.7" @@ -563,15 +545,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.76" @@ -588,46 +561,6 @@ version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" -[[package]] -name = "libgit2-sys" -version = "0.17.0+1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" -dependencies = [ - "cc", - "libc", - "libssh2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", -] - -[[package]] -name = "libssh2-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.14" diff --git a/Cargo.toml b/Cargo.toml index e3ddb00..2886f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -git2 = "0.19.0" reqwest = { version = "0.12.9", features = ["blocking"] } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 492238a..b083e25 --- a/README.md +++ b/README.md @@ -1,18 +1,68 @@ -"Ferro" is inspired by: +# Ferro +A rust base llm wrapper for generating git commits and descriptions +## How to read +This readme can be read _As-Is_ but youll be missing some nice visuals. If you want to see those, you need two tools: +1. [graph-easy](http://bloodgate.com/perl/graph/manual/overview.html) +2. [slides](https://github.com/maaslalani/slides) -Scientific Roots +figure out how to get those installed on your OS and then come back, otherwise, continue on... +## Fun Facts +### Scientific Roots 1. Ferrum: Latin for iron, reflecting Rust's iron/oxidation themes. 2. Ferro-: Greek prefix meaning iron or iron-containing. - -Relevant Connections - +### Relevant Connections 1. Ferrocene: An iron-containing compound. 2. Ferroelectric: Materials exhibiting iron-like electrical properties. - -Simple, Modern Sound - +### Simple, Modern Sound "Ferro" is concise, memorable and has a technical feel, fitting for a Rust CLI tool. + +*continue to architecture on next page* + +--- + +## Architecture +I just wanted to split things up in a cohesive way. Where one node is resposible for its own thing and returns whatever +data, it creates, to its caller + +``` +this is a graph demonstrating the mvp architecture + +~~~graph-easy --as=boxart +graph { flow: east; } +[main]->{start:front; end:back}[arg parser] +[arg parser] -> {start:front,0; end:back;} [pr handler] +[arg parser] -> {start:front,0; end:back;} [commit handler] +[pr handler], [commit handler] -> {start:front; end:back,0;} [ml interface] +[ml interface] -> [out] + +[commit handler] <=> [banana] +[pr handler] <=> [apple] +~~~ +``` + +*continue to next slide to see mvp v2 architecture* + +--- + +``` +this is a graph demonstrating the mvp (v2) architecture + +~~~graph-easy --as=boxart +graph { flow: east; } +[main]->{start:front; end:back}[arg parser] +[arg parser] -> {start:front,0; end:back;} [pr handler] +[arg parser] -> {start:front,0; end:back;} [commit handler] +[pr handler], [commit handler] -> {start:front; end:back,0;} [ml interface] +[ml interface] -> [out] + +[commit handler] <=> [banana] +[pr handler] <=> [apple] +[config loader]{ origin: main; offset: 0,-2; } -> {start:right; end:left} [main] +[interactive] { origin: arg parser; offset: 0,-2; } <== no args ==> {start:right; end:left}[arg parser] +~~~ +``` + diff --git a/src/arg_parser.rs b/src/arg_parser.rs new file mode 100644 index 0000000..8129c6b --- /dev/null +++ b/src/arg_parser.rs @@ -0,0 +1,30 @@ +use std::env::args; + +pub struct ArgParser {} + +#[derive(Debug)] +pub enum ParsedArg { + Commit, + PullRequest, +} + +impl ArgParser { + pub fn parse() -> Option { + let arg = args().nth(1); + if arg.is_none() { + // <-- interactive mode will go here + println!("...(future) interactive mode not implimented... exiting"); + return None; + } + let arg = arg.unwrap(); + match arg.as_str() { + "-c" => Some(ParsedArg::Commit), + "-p" => Some(ParsedArg::PullRequest), + "-h" => { + println!("Available Commands: -c [commit] -p [pull request] -h [help]"); + None + } + _ => None, + } + } +} diff --git a/src/commit_handler.rs b/src/commit_handler.rs new file mode 100644 index 0000000..07b516c --- /dev/null +++ b/src/commit_handler.rs @@ -0,0 +1,20 @@ +use crate::git_grabber::GitGrabber; + +pub struct CommitHandler {} + +impl CommitHandler { + pub fn new() -> Option<(String, String)> { + let dirs = String::from(format!(" + + change_type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + + create a short commit message from this entire diff, with format (): . + the commit message should vaguely describe the changes present unless a small change is made that can be + precisely described withtin a short commit message + + Only respond with the commit message. + ")); + let prompt = GitGrabber::get_diff(); + Some((dirs, prompt)) + } +} diff --git a/src/git_grabber.rs b/src/git_grabber.rs index 06f8181..6b1482f 100644 --- a/src/git_grabber.rs +++ b/src/git_grabber.rs @@ -1,25 +1,54 @@ -use git2::Repository; -use std::{env::current_dir, path::PathBuf}; +use core::panic; +use std::{process::Command, str::from_utf8}; -pub struct GitGrabber { - pub repo: Option, - dir: PathBuf, -} +// this is a comment test here +pub struct GitGrabber {} impl GitGrabber { - pub fn new() -> Self { - GitGrabber { - repo: None, - dir: current_dir().unwrap(), - } + pub fn get_current_branch() -> String { + let branch = Command::new("git") + .args(["branch", "--show-current"]) + .output() + .expect("Failed to get branch"); + + let b = from_utf8(&branch.stdout).unwrap(); + String::from(b.strip_suffix("\n").unwrap()) } - pub fn get_repo(&mut self) { - let repo = match Repository::open(self.dir.clone()) { - Ok(repo) => repo, - Err(e) => panic!("Failed to open: {}", e), - }; + pub fn get_diff() -> String { + // just to print + let output = Command::new("git") + .args([ + "diff", + "--staged", + "--ignore-all-space", + "--ignore-blank-lines", + "--ignore-cr-at-eol", + "--minimal", + "--no-prefix", + "--no-renames", + "--word-diff", + "--inter-hunk-context=0", + ]) + .output() + .expect("Failed to get diff"); - self.repo = Some(repo); + if !output.status.success() { + panic!("Did you forget to stage your files?"); + } + + let b = from_utf8(&output.stdout).unwrap(); + String::from(b) + } + + pub fn generate_repo_desc() -> String { + let curr_branch = GitGrabber::get_current_branch(); + let output = Command::new("git") + .args(["reflog", "show", &curr_branch]) + .output() + .expect("Failed to get repo details"); + + let b = from_utf8(&output.stdout).unwrap(); + String::from(b) } } diff --git a/src/main.rs b/src/main.rs index 08a7aec..ed60dd1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,34 @@ +mod arg_parser; +mod commit_handler; mod git_grabber; -mod mind_bridge; -mod transporter; +mod ml_interface; +mod pr_handler; -// use core::panic; -use git_grabber::GitGrabber; -use mind_bridge::*; -// use transporter::Transporter; +use arg_parser::ArgParser; +use commit_handler::CommitHandler; +use ml_interface::{MlBody, MlInterface, MlResponse}; +use pr_handler::PrHandler; fn main() { - // let a = MindGen::new("Some random commit message"); - // let mut transporter = Transporter::new(); + let prompt: Option<(String, String)> = match ArgParser::parse() { + Some(arg_parser::ParsedArg::Commit) => CommitHandler::new(), + Some(arg_parser::ParsedArg::PullRequest) => PrHandler::new(), + None => None, + }; - let mut gg = GitGrabber::new(); - gg.get_repo(); + // 1+2 = 5 + if prompt.is_none() { + return; + } - gg.repo.unwrap().revwalk().unwrap().for_each(|x| { - if x.is_ok() { - println!("{:?}", x.unwrap()) - } else { - println!("No rev") - } - }); + let mut ml = MlInterface::new(); + let (directions, content) = prompt.unwrap(); + let body = MlBody::new(content, directions); + let res_text = ml.make_request(body).unwrap().text().unwrap(); - return (); - - // let res_text = transporter.make_request(a).unwrap().text().unwrap(); - // let response: Result = serde_json::from_str(&res_text); - - // if response.is_err() { - // panic!("oop something went wrong: {:?}", response.err()); - // } - - // println!("{:#?}", response.unwrap()); + let response: Result = serde_json::from_str(&res_text); + if response.is_err() { + panic!("oop something went wrong: {:?}", response.err()); + } + println!("{:?}", response.unwrap().response); } diff --git a/src/mind_bridge.rs b/src/mind_bridge.rs deleted file mode 100644 index 234cade..0000000 --- a/src/mind_bridge.rs +++ /dev/null @@ -1,49 +0,0 @@ - use serde::{Deserialize, Serialize}; - - #[derive(Debug, Deserialize)] - #[allow(unused)] - pub struct GenRes { - model: String, - created_at: String, - response: String, - done: bool, - total_duration: u64, - load_duration: u64, - prompt_eval_count: u64, - prompt_eval_duration: u64, - eval_count: u64, - eval_duration: u64, - } - - #[derive(Debug, Serialize)] - struct GenOptions { - temperature: f32, - num_predict: u8, - } - - #[derive(Debug, Serialize)] - pub struct MindGen { - model: String, - prompt: String, - stream: bool, - raw: bool, - system: String, - options: GenOptions, - } - - impl MindGen { - #[allow(unused)] - pub fn new(input: &str) -> Self { - Self { - model: String::from("llama3.1"), - stream: false, - raw: false, - prompt: String::from(input), - system: String::from("You are a commit message generator. You will generate commit messages following this format Task(): making sure to never go over 90 characters"), - options: GenOptions { - temperature: 0.5, - num_predict: 90 - } - } - } -} diff --git a/src/ml_interface.rs b/src/ml_interface.rs new file mode 100644 index 0000000..cd69e12 --- /dev/null +++ b/src/ml_interface.rs @@ -0,0 +1,88 @@ + +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; + +#[allow(unused)] +pub static OLLAMA_ENDP: &str = "http://localhost:11434/api/generate"; + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct MlResponse { + model: String, + created_at: String, + pub response: String, + done: bool, + total_duration: u64, + load_duration: u64, + prompt_eval_count: u64, + prompt_eval_duration: u64, + eval_count: u64, + eval_duration: u64, +} + +#[derive(Debug, Serialize)] +struct MlOptions { + temperature: f32, + num_predict: u8, + repeat_last_n: u8, + top_k: u8, + top_p: f32, +} + +#[derive(Debug, Serialize)] +pub struct MlBody { + model: String, + prompt: String, + stream: bool, + raw: bool, + system: String, + options: MlOptions, +} + +impl MlBody { + #[allow(unused)] + pub fn new(content: String, directions: String) -> Self { + Self { + model: String::from("llama3.1"), + stream: false, + raw: false, + prompt: content, + system: directions, + options: MlOptions { + temperature: 0.1, + num_predict: 0, + repeat_last_n: 0, + top_k: 10, + top_p: 0.5, + }, + } + } +} + +#[allow(unused)] +pub struct MlInterface { + pub client: Client, +} + +#[allow(unused)] +impl MlInterface { + #[allow(unused)] + pub fn new() -> Self { + Self { + client: Client::new(), + } + } + + #[allow(unused)] + pub fn make_request(&mut self, gen_data: MlBody) -> Result { + if gen_data.prompt.len() < 1 { + panic!("No prompt provided"); + } + let json_body = serde_json::to_string(&gen_data).unwrap(); + let res = self.client.post(OLLAMA_ENDP).body(json_body).send(); + if res.is_err() { + panic!("Failed to send ollama payload"); + } + Ok(res.unwrap()) + } +} diff --git a/src/pr_handler.rs b/src/pr_handler.rs new file mode 100644 index 0000000..9c70d9a --- /dev/null +++ b/src/pr_handler.rs @@ -0,0 +1,39 @@ +use crate::git_grabber::GitGrabber; + +pub struct PrHandler {} +impl PrHandler { + pub fn new() -> Option<(String, String)> { + let directions = String::from(" + + change_type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + + + create a short PR title from this diff, with format (): . + + + create a PR Description to describe the changes made in this diff using the commit messages for the body content. + + follow this format: + ## What? + [what_description] + #[ticket_number] + ## Why? + [why_description] + ## How? + [how_description] + ## Testing? + [testing_description] + ## Screenshots (optional) + [screenshots] + ## Anything Else? + [leftover_details] + + include signature at the end stating 'this is an ai generated pull request description'. + + + Only respond with the Pr title and Pr description. + Use Emojis in description body only. + "); + Some((directions, GitGrabber::generate_repo_desc())) + } +} diff --git a/src/transporter.rs b/src/transporter.rs deleted file mode 100644 index 3f1d803..0000000 --- a/src/transporter.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::MindGen; -use reqwest::blocking::Client; - -#[allow(unused)] -pub static OLLAMA_ENDP: &str = "http://localhost:11434/api/generate"; - -#[allow(unused)] -pub struct Transporter { - pub client: Client, -} - -#[allow(unused)] -impl Transporter { - #[allow(unused)] - pub fn new() -> Self { - Self { - client: Client::new(), - } - } - - #[allow(unused)] - pub fn make_request( - &mut self, - gen_data: MindGen, - ) -> Result { - let json_body = serde_json::to_string(&gen_data).unwrap(); - self.client.post(OLLAMA_ENDP).body(json_body).send() - } -} diff --git a/src/uml.drawio b/src/uml.drawio new file mode 100644 index 0000000..f663125 --- /dev/null +++ b/src/uml.drawio @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +