feat(pr_handler): Create PR title and description generation rules #1

Merged
tristonarmstrong merged 20 commits from dev into master 2025-01-12 19:06:43 +00:00
12 changed files with 432 additions and 198 deletions

67
Cargo.lock generated
View File

@ -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"

View File

@ -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"

66
README.md Normal file → Executable file
View File

@ -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]
~~~
```

30
src/arg_parser.rs Normal file
View File

@ -0,0 +1,30 @@
use std::env::args;
pub struct ArgParser {}
#[derive(Debug)]
pub enum ParsedArg {
Commit,
PullRequest,
}
impl ArgParser {
pub fn parse() -> Option<ParsedArg> {
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,
}
}
}

20
src/commit_handler.rs Normal file
View File

@ -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!("
<Variables>
change_type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
<Commit Instructions>
create a short commit message from this entire diff, with format <change_type>(<scope>): <commit_message>.
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
<Response Constraints>
Only respond with the commit message.
"));
let prompt = GitGrabber::get_diff();
Some((dirs, prompt))
}
}

View File

@ -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<Repository>,
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)
}
}

View File

@ -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<GenRes, _> = serde_json::from_str(&res_text);
// if response.is_err() {
// panic!("oop something went wrong: {:?}", response.err());
// }
// println!("{:#?}", response.unwrap());
let response: Result<MlResponse, _> = serde_json::from_str(&res_text);
if response.is_err() {
panic!("oop something went wrong: {:?}", response.err());
}
println!("{:?}", response.unwrap().response);
}

View File

@ -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(<task #>): <commit message> making sure to never go over 90 characters"),
options: GenOptions {
temperature: 0.5,
num_predict: 90
}
}
}
}

88
src/ml_interface.rs Normal file
View File

@ -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<reqwest::blocking::Response, &str> {
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())
}
}

39
src/pr_handler.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::git_grabber::GitGrabber;
pub struct PrHandler {}
impl PrHandler {
pub fn new() -> Option<(String, String)> {
let directions = String::from("
<Variables>
change_type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
<PR Title>
create a short PR title from this diff, with format <change_type>(<scope>): <pr_description>.
<PR Description>
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'.
<Response Constraints>
Only respond with the Pr title and Pr description.
Use Emojis in description body only.
");
Some((directions, GitGrabber::generate_repo_desc()))
}
}

View File

@ -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<reqwest::blocking::Response, reqwest::Error> {
let json_body = serde_json::to_string(&gen_data).unwrap();
self.client.post(OLLAMA_ENDP).body(json_body).send()
}
}

125
src/uml.drawio Normal file
View File

@ -0,0 +1,125 @@
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/25.0.1 Chrome/128.0.6613.186 Electron/32.2.6 Safari/537.36" version="25.0.1">
<diagram name="Page-1" id="3ifs8vQQYQUBUYL4we8P">
<mxGraphModel dx="1824" dy="729" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2AVEHZDF9yeEspTbbptQ-7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-1" target="2AVEHZDF9yeEspTbbptQ-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-1" value="IN" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="110" y="30" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-2" value="Out" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="110" y="500" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-3" target="2AVEHZDF9yeEspTbbptQ-4" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="2AVEHZDF9yeEspTbbptQ-3" target="2AVEHZDF9yeEspTbbptQ-5" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-3" value="Arg Parser" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="110" y="140" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="2AVEHZDF9yeEspTbbptQ-5" target="2AVEHZDF9yeEspTbbptQ-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="PAK-VcJjs1wrBpZ3mVEH-2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2AVEHZDF9yeEspTbbptQ-5" target="PAK-VcJjs1wrBpZ3mVEH-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-5" value="pull request handler" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="10" y="280" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-17" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-6" target="2AVEHZDF9yeEspTbbptQ-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-6" value="Ml interface" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="110" y="390" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-4" target="2AVEHZDF9yeEspTbbptQ-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="PAK-VcJjs1wrBpZ3mVEH-4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="2AVEHZDF9yeEspTbbptQ-4" target="PAK-VcJjs1wrBpZ3mVEH-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-4" value="commit handler" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="210" y="280" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-18" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-19" target="2AVEHZDF9yeEspTbbptQ-23" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-19" value="IN" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="710" y="30" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-20" value="Out" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="710" y="500" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-21" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-23" target="2AVEHZDF9yeEspTbbptQ-29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="2AVEHZDF9yeEspTbbptQ-23" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="670" y="240" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-23" value="Arg Parser" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="710" y="140" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" target="2AVEHZDF9yeEspTbbptQ-27" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="670" y="300" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-25" value="pull request handler" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="600" y="240" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-26" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-27" target="2AVEHZDF9yeEspTbbptQ-20" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-27" value="Ml interface" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="710" y="390" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-29" target="2AVEHZDF9yeEspTbbptQ-27" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-29" value="commit handler" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="810" y="240" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-30" value="Options Picker&lt;div&gt;interactive&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="910" y="140" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="2AVEHZDF9yeEspTbbptQ-23" target="2AVEHZDF9yeEspTbbptQ-30" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points" />
</mxGeometry>
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-35" value="No args" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="2AVEHZDF9yeEspTbbptQ-33" vertex="1" connectable="0">
<mxGeometry x="-0.1972" y="1" relative="1" as="geometry">
<mxPoint x="8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-30" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="830" y="189" as="targetPoint" />
<Array as="points">
<mxPoint x="870" y="189" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-43" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="2AVEHZDF9yeEspTbbptQ-42" target="2AVEHZDF9yeEspTbbptQ-19" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="2AVEHZDF9yeEspTbbptQ-42" value="config loader" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="890" y="40" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="PAK-VcJjs1wrBpZ3mVEH-1" value="PR branch diff" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-130" y="290" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="PAK-VcJjs1wrBpZ3mVEH-3" value="staged diff" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="360" y="295" width="130" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>