// SPDX MIT Bernhard Guillon 2025 // // As this project is currently in research please just ignore // all clones and memory copies ^^ As on all my rust projects // I follow the copy all and fix it later approach. To make // rust a nice prototyping languague. If you don't like that // approach you do yours and I do mine ^^ use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{ env::{temp_dir, var}, fs::File, io::Read, process::Command, process::Stdio, }; use std::fs; use std::fmt; use std::num::ParseIntError; use std::io::Write; use std::collections::HashMap; use chrono::{Utc, TimeZone}; use std::env::args; #[derive(Serialize, Deserialize, Debug, Default, Clone)] struct BugReport { timestamp: String, status: String, title: String, description: Vec, tags: Option, version: String } fn new_bug() -> String { let editor = var("EDITOR").unwrap(); let mut file_path = temp_dir(); // TODO: figgure out how to get .git dir in a save manner file_path.push("NEW_BUG_REPORT"); File::create(&file_path).expect("Could not create file"); Command::new(editor) .arg(&file_path) .status() .expect("Something went wrong"); let mut new_bug_report = String::new(); let _ = File::open(&file_path) .expect("Could not open file") .read_to_string(&mut new_bug_report); let _ = fs::remove_file(&file_path); let mut header = ""; let mut description: Vec:: = Vec::new(); for (i, line) in new_bug_report.lines().enumerate() { match i { 0 => header = line, 1 => (), _ => description.push(line.to_string()), } } let report = BugReport { timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string(), title: header.to_owned(), description: description.to_owned(), status: "new".to_owned(), tags: None, version: "v1".to_owned(), }; serde_json::to_string(&report).unwrap() } #[derive(Debug)] enum GitError { GitLog(String), Parse(ParseIntError), UnknownRef, } impl fmt::Display for GitError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { GitError::GitLog(s) => write!(f, "{}", s), GitError::Parse(s) => write!(f, "{}", s), GitError::UnknownRef => write!(f, "Unknown reference"), } } } impl From for GitError { fn from(err: ParseIntError) -> GitError { GitError::Parse(err) } } impl std::error::Error for GitError {} #[derive(Debug, Default, Clone)] struct GitLog{ timestamp: u64, hash: String, author_name: String, author_email: String, blob_object: String, } fn collect_reachable_objects() -> Result<(Vec, String), GitError> { // TODO: until we have the need for archiving old stuff to archive // we can guarantee that all objects collected exist and are // unchanged. As soon as we support archives we might need to // change this approach a bit. // As we use the first two chars of a git hash as directory name and // the other part as file name we are able to regenerate the hash // from the changed file path. Git log can provide the file path // as well as all other needed information. let mut git_logs: Vec = Vec::new(); let mut blobs = String::default(); let logs = Command::new("git") .arg("log") .arg("--pretty=format:%H#%an#%ae#%at") .arg("--name-only") .arg("refs/notes/devtools/future-me") .output() .expect("Error with git log"); if !logs.status.success() { GitError::GitLog(String::from_utf8_lossy(&logs.stderr).to_string()); //return GitError::GitLog(String::from_utf8_lossy(&logs.stderr).to_string()); } let lines = String::from_utf8_lossy(&logs.stdout); let mut git_log = GitLog::default(); for (i, line) in lines.lines().enumerate() { match i%3 { 0 => { let parts = line.split("#"); for (i, part) in parts.enumerate() { match i%4 { 0 => git_log.hash=part.to_string(), 1 => git_log.author_name = part.to_string(), 2 => git_log.author_email = part.to_string(), 3 => git_log.timestamp = part.parse::()?, _ => todo!(), // TODO: why we need this? } } }, 1 => { git_log.blob_object = line.replace("/", ""); if blobs.len() > 0 { blobs = blobs + "\n" + &git_log.blob_object; } else { blobs = blobs + &git_log.blob_object; } }, 2 => (), // commit seperator _ => todo!(), // TODO: why we need this? } if i>=1 && i%3 ==1 { git_logs.push(git_log.clone()); } } return Ok((git_logs, blobs)); } fn show() { // TODO: split this function to collect the reports and to show them // in the desired format. // For a short log we don't need to do all this extra work ^^ let (logs, blobs) = collect_reachable_objects().unwrap(); //println!("logs {:?}", logs); //println!("blobs {:?}", blobs); let mut files = Command::new("git") .arg("cat-file") .arg("--batch") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Error with git cat-files --batch"); let mut stdin = files.stdin.take().expect("Failed to open stdin"); std::thread::spawn(move || { stdin.write_all(blobs.as_bytes()).expect("Failed to write to stdin"); }); let output = files.wait_with_output().expect("Failed to write to stdout"); let objects = String::from_utf8_lossy(&output.stdout); let mut map: HashMap = HashMap::new(); let mut bug_report: BugReport = BugReport::default(); let mut hash: String = String::default(); for (i, object) in objects.lines().enumerate() { //println!("object |{}|", object); match i%2 { 0 => hash = object.split(" ").next().unwrap().to_string(), 1 => { //println!("|{:?}|", object); bug_report = serde_json::from_str(&object).unwrap(); map.insert(hash.clone(), bug_report.clone()); //println!("INSERT") }, //2 => (), _ => todo!(), } // if i>=1 && i%3 ==1 { // println!("insert bug report"); // map.insert(hash.clone(), bug_report.clone()); // } } for log in logs { //println!("{:?}", log); let entry = map[&log.blob_object].clone(); let datetime = Utc.timestamp_opt(log.timestamp as i64, 0).unwrap(); // TODO: do we really need to be able to convert times? Or should we just // collect the git time with the possibilites git gives us? // I don't like the chrono dependency :/ println!("-------------------------------------------------------------------------"); println!{"{} {} {} {}\t{}", &log.hash[0..7], entry.status, log.author_name, entry.title, datetime.format("%Y-%m-%d %H:%M:%S")}; for line in entry.description { println!("{}", line); } } } //create-new-bug() { // readonly bug="$1" // last_ref=$(git show-ref refs/notes/devtools/future-me | cut -d ' ' -f 1) // echo $last_ref // if [ -n "$last_ref" ] // then // git read-tree $last_ref // else // git read-tree --empty // fi // // object_id=$(echo "$1" | git hash-object -w --stdin) // echo $object_id // file_path=${object_id:0:2}/${object_id:2} // git update-index --add --cacheinfo 100644 $object_id $file_path // tree_id=$(git write-tree) // files_to_unstage=$(git update-index --refresh | cut -d ' ' -f 1 | cut -d ':' -f 1) // git update-index --remove $files_to_unstage // if [ -n "$last_ref" ] // then // add_parent="$last_ref" // commit_id=$(echo 'future-me: created a new bug for you' | git commit-tree $tree_id -p $add_parent) // else // commit_id=$(echo 'future-me: created a new bug for you' | git commit-tree $tree_id) // fi // git update-ref refs/notes/devtools/future-me $commit_id //} // // new) // echo create new bug "$payload" #"${*:2}" // check-git-status // last_tree="$(git --no-pager log -1 --format="%H" | tr --delete '\n')" // echo $last_tree // create-new-bug "$payload" // git read-tree "$last_tree" // ;; fn hash_to_path(hash: &str) -> String { format!("{}/{}", &hash[..2], &hash[2..]) } fn create_new_bug() { // first of all check if there is anything staged as we mess with the trees and staging area // use check_status for that // after that get the current tree get_current_tree and save it. We need to guarantee that // we switch back to that as best as we can if anything went bad. // // get_last_ref (show-ref ref/notes/devtools/future-me) // if empty // call init future-me which creates a new tree // with git read-tree --empty // otherwise read the current tree with git read-tree future-me-hash // // let the user enter the bug report and creat the json string // then // create an git object with git hash-object -w --stdin // collect the output as object hash // create a file path string with dd/fffff // update the index // with git update-index --add --cacheinfo 100644 hash dd/fffff // write a new tree object and collect the hash from the output // git write-tree // reset our changes to the staging area with // git update-index --remove // files_to_unstage=$(git update-index --refresh | cut -d ' ' -f 1 | cut -d ':' -f 1) // git update-index --remove $files_to_unstage // TODO: check if we can just unstage the // file as we know the path? // If we are the first and there is no parent aka future-me ref was empty // commit_id=$(echo 'future-me: created a new bug for you' | git commit-tree $tree_id) // else add the parent // commit_id=$(echo 'future-me: created a new bug for you' | git commit-tree $tree_id -p $add_parent) // cool now update the ref // // git update-ref refs/notes/devtools/future-me $commit_id // // switch back to what ever was the working tree we got from get_current_tree check_status(); let save_current_tree = get_current_tree().unwrap(); let future_me_ref = match get_last_ref() { Ok(hash) => {read_tree(&hash); Some(hash)}, Err(GitError::UnknownRef) => {create_new_tree(); None}, Err(_) => panic!("fixme"), }; let bug_report = new_bug(); let bug_object = create_object(bug_report.clone()).unwrap(); let path = hash_to_path(&bug_object); stage_object(&bug_object, &path); let tree_object = write_tree(); let commit_object = match future_me_ref { Some(parent) => commit(tree_object, Some(parent)).unwrap(), None => commit(tree_object, None).unwrap(), }; let files_to_unstage = get_files_to_unstage(); unstage_object(&files_to_unstage); update_ref(&commit_object); read_tree(&save_current_tree); println!("hello new bug"); } fn get_files_to_unstage() -> String { let cmd = Command::new("git") .arg("update-index") .arg("--refresh") .output() .expect("Error with update-index"); match cmd.status.code() { Some(1) => (), Some(0) => (), Some(s) => panic!("Fixme status code: {}",s), None => panic!("Fixme git update-index"), } let lines = String::from_utf8_lossy(&cmd.stdout).to_string(); let mut files = String::default(); for line in lines.lines() { files = files + " " +line.split(":").next().unwrap(); } files[1..].to_string() } fn unstage_object(path: &str) { let cmd = Command::new("git") .arg("update-index") .arg("--remove") .arg(path) .output() .expect("Error with update-index"); if !cmd.status.success() { panic!("FIXME unstage_object failed"); } } fn update_ref(object: &str) { let cmd = Command::new("git") .arg("update-ref") .arg("refs/notes/devtools/future-me") .arg(object) .output() .expect("Error with update-index"); if !cmd.status.success() { panic!("FIXME: update ref failed"); } } fn stage_object(hash: &str, path: &str) { // with git update-index --add --cacheinfo 100644 hash dd/fffff let cmd = Command::new("git") .arg("update-index") .arg("--add") .arg("--cacheinfo") .arg("100644") .arg(hash) .arg(path) .output() .expect("Error with update-index"); if !cmd.status.success() { panic!("FIXME"); } } fn create_object(object: String) -> Result { // create an git object with git hash-object -w --stdin let mut files = Command::new("git") .arg("hash-object") .arg("-w") .arg("--stdin") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Error with git hash-ojbect"); let mut stdin = files.stdin.take().expect("Failed to open stdin"); std::thread::spawn(move || { stdin.write_all(object.as_bytes()).expect("Failed to write to stdin"); }); let output = files.wait_with_output().expect("Failed to write to stdout"); let lines = String::from_utf8_lossy(&output.stdout).to_string(); Ok(lines.split_whitespace().next().unwrap().to_string()) } fn commit(object: String, parent: Option) -> Result { // commit_id=$(echo 'future-me: created a new bug for you' | git commit-tree $tree_id) let mut files = match parent { Some(parent) => Command::new("git") .arg("commit-tree") .arg(object) .arg("-p") .arg(parent) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Error with git commit-tree"), None => Command::new("git") .arg("commit-tree") .arg(object) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Error with git commit-tree"), }; let mut stdin = files.stdin.take().expect("Failed to open stdin"); std::thread::spawn(move || { stdin.write_all("future-me: created a new bug for you".as_bytes()).expect("Failed to write to stdin"); }); let output = files.wait_with_output().expect("Failed to write to stdout"); let lines = String::from_utf8_lossy(&output.stdout).to_string(); Ok(lines.split_whitespace().next().unwrap().to_string()) } fn get_last_ref() -> Result { let cmd = Command::new("git") .arg("show-ref") .arg("refs/notes/devtools/future-me") .output() .expect("Error with git show-ref"); if !cmd.status.success() { //return GitError::GitLog(String::from_utf8_lossy(&cmd.stderr).to_string()); GitError::GitLog(String::from_utf8_lossy(&cmd.stderr).to_string()); } let lines = String::from_utf8_lossy(&cmd.stdout); match lines.split_whitespace().next() { Some(line) => Ok(line.to_string()), None => Err(GitError::UnknownRef), } } fn get_current_tree() -> Result{ let cmd = Command::new("git") .arg("log") .arg("-1") .arg("--format=%H") .output() .expect("Error with git log"); if !cmd.status.success() { GitError::GitLog(String::from_utf8_lossy(&cmd.stderr).to_string()); } let lines = String::from_utf8_lossy(&cmd.stdout); Ok(lines.trim().to_string()) } fn write_tree() -> String { let cmd = Command::new("git") .arg("write-tree") .output() .expect("Error with git write-tree"); if !cmd.status.success() { panic!("{}", String::from_utf8_lossy(&cmd.stderr)); } let lines = String::from_utf8_lossy(&cmd.stdout); lines.trim().to_string() } fn create_new_tree() { let cmd = Command::new("git") .arg("read-tree") .arg("--empty") .output() .expect("Error with git read-tree"); if !cmd.status.success() { panic!("{}", String::from_utf8_lossy(&cmd.stderr)); } } fn read_tree(tree: &str) { let cmd = Command::new("git") .arg("read-tree") .arg(tree) .output() .expect("Error with git read-tree"); if !cmd.status.success() { panic!("{}", String::from_utf8_lossy(&cmd.stderr)); } } fn check_status() { let logs = Command::new("git") .arg("status") .arg("--porcelain") .output() .expect("Error with git status"); if !logs.status.success() { GitError::GitLog(String::from_utf8_lossy(&logs.stderr).to_string()); } let lines = String::from_utf8_lossy(&logs.stdout); for line in lines.lines() { println!("{}", line); if line.starts_with(['M', 'A']) { panic!("You first need to clean you git staging status to use future-me"); } } } fn print_usage() { println!("usage: future-me \n"); println!("commands:"); println!("show - shows the log"); println!("new - enter new bug"); println!("help - get this help"); } enum Cmd{ Show, New, Check, } fn process_args() -> Cmd { let myargs: Vec = args().collect(); println!("{:?}", myargs); if myargs.len() != 2 { print_usage(); } let unwraped = myargs.get(1).unwrap(); match unwraped.as_str() { "show" => Cmd::Show, "new" => Cmd::New, "check" => Cmd::Check, _ => {print_usage(); todo!()}, } } fn main() { match process_args() { Cmd::Show => show(), Cmd::New => create_new_bug(), Cmd::Check=> { println!("{}", get_files_to_unstage()); //let tree = get_current_tree().unwrap(); //println!("{}", get_last_ref().unwrap()); //read_tree(&tree); } } }