// 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() { 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(), }; let j = serde_json::to_string(&report).unwrap(); print!("{}", j); } #[derive(Debug)] enum GitError { GitLog(String), Parse(ParseIntError) } 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), } } } 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()); } 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(); 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() { match i%3 { 0 => hash = object.split(" ").next().unwrap().to_string(), 1 => bug_report = serde_json::from_str(&object).unwrap(), 2 => (), _ => todo!(), } if i>=1 && i%3 ==1 { map.insert(hash.clone(), bug_report.clone()); } } for log in logs { 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 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 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 => new_bug(), Cmd::Check=> { let tree = get_current_tree().unwrap(); read_tree(&tree); } } }