From 20c68a3b14c97be6f68d246c6da8b04c524a1b05 Mon Sep 17 00:00:00 2001 From: Martin Frost Date: Sun, 6 Jul 2025 11:56:28 +0200 Subject: [PATCH] feat: Merge all functionality into one binary This groups all entrypoints into one `git-mob` binary. One of the main questions I've gotten about this library is why the `add-coauthor` (and similar) aren't an option or a subcommand to `git-mob`. I've also come to the conclusion that it's a bit confusing to have to memorize five different binaries rather than one, since they're all related anyway. --- build.rs | 44 ++++---- src/bin/git-add-coauthor.rs | 15 --- src/bin/git-delete-coauthor.rs | 9 -- src/bin/git-edit-coauthor.rs | 22 ---- src/bin/git-mob.rs | 79 ------------- src/bin/git-solo.rs | 16 --- src/cli.rs | 98 ++++++++--------- src/main.rs | 195 +++++++++++++++++++++++++++++++++ 8 files changed, 260 insertions(+), 218 deletions(-) delete mode 100644 src/bin/git-add-coauthor.rs delete mode 100644 src/bin/git-delete-coauthor.rs delete mode 100644 src/bin/git-edit-coauthor.rs delete mode 100644 src/bin/git-mob.rs delete mode 100644 src/bin/git-solo.rs create mode 100644 src/main.rs diff --git a/build.rs b/build.rs index ea8b770..debebc6 100644 --- a/build.rs +++ b/build.rs @@ -1,31 +1,29 @@ -use clap::CommandFactory; -use clap_mangen::Man; -use std::env; -use std::path::Path; +// use clap::CommandFactory; +// use clap_mangen::Man; +// use std::env; +// use std::path::Path; -#[path = "src/cli.rs"] -mod cli; -macro_rules! generate_manpage { - ($struct:ident) => { - let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or("target".to_string()); - let output_dir = Path::new(&target_dir).join(env::var("PROFILE").unwrap()); +// macro_rules! generate_manpage { +// ($struct:ident) => { +// let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or("target".to_string()); +// let output_dir = Path::new(&target_dir).join(env::var("PROFILE").unwrap()); - let cmd = cli::$struct::command(); - let cmd_name = format!("{}.1", cmd.get_name()); - let man = Man::new(cmd); - let mut buffer: Vec = Default::default(); - man.render(&mut buffer)?; - std::fs::write(output_dir.join(cmd_name), buffer)?; - }; -} +// let cmd = cli::$struct::command(); +// let cmd_name = format!("{}.1", cmd.get_name()); +// let man = Man::new(cmd); +// let mut buffer: Vec = Default::default(); +// man.render(&mut buffer)?; +// std::fs::write(output_dir.join(cmd_name), buffer)?; +// }; +// } fn main() -> std::io::Result<()> { - generate_manpage!(GitMob); - generate_manpage!(GitSolo); - generate_manpage!(GitAddCoauthor); - generate_manpage!(GitEditCoauthor); - generate_manpage!(GitDeleteCoauthor); + // generate_manpage!(GitMob); + // generate_manpage!(GitSolo); + // generate_manpage!(GitAddCoauthor); + // generate_manpage!(GitEditCoauthor); + // generate_manpage!(GitDeleteCoauthor); Ok(()) } diff --git a/src/bin/git-add-coauthor.rs b/src/bin/git-add-coauthor.rs deleted file mode 100644 index 7fcdc8e..0000000 --- a/src/bin/git-add-coauthor.rs +++ /dev/null @@ -1,15 +0,0 @@ -use clap::Parser; -use git_mob::{cli, parse_coauthors_file, write_coauthors_file, Author}; - -fn main() { - let opt = cli::GitAddCoauthor::parse(); - let mut authors = parse_coauthors_file().unwrap_or_default(); - let new_author = Author { - name: opt.name, - email: opt.email, - }; - - authors.insert(opt.initials, new_author); - - write_coauthors_file(authors); -} diff --git a/src/bin/git-delete-coauthor.rs b/src/bin/git-delete-coauthor.rs deleted file mode 100644 index b90360c..0000000 --- a/src/bin/git-delete-coauthor.rs +++ /dev/null @@ -1,9 +0,0 @@ -use clap::Parser; -use git_mob::{cli, get_available_coauthors, write_coauthors_file}; - -fn main() { - let opt = cli::GitDeleteCoauthor::parse(); - let mut authors = get_available_coauthors(); - authors.remove(&opt.initials); - write_coauthors_file(authors); -} diff --git a/src/bin/git-edit-coauthor.rs b/src/bin/git-edit-coauthor.rs deleted file mode 100644 index cd6e074..0000000 --- a/src/bin/git-edit-coauthor.rs +++ /dev/null @@ -1,22 +0,0 @@ -use clap::Parser; -use git_mob::{cli, get_available_coauthors, write_coauthors_file, Author}; -use std::process; - -fn main() { - let opt = cli::GitDeleteCoauthor::parse(); - - let mut authors = get_available_coauthors(); - - if let Some(author) = authors.get(&opt.initials) { - let mut updated_author: Author = author.clone(); - updated_author.name = opt.name; - updated_author.email = opt.email; - - authors.insert(opt.initials, updated_author); - - write_coauthors_file(authors); - } else { - eprintln!("No author found with initials {}", &opt.initials); - process::exit(1); - }; -} diff --git a/src/bin/git-mob.rs b/src/bin/git-mob.rs deleted file mode 100644 index a15ab4a..0000000 --- a/src/bin/git-mob.rs +++ /dev/null @@ -1,79 +0,0 @@ -use clap::Parser; -use git_mob::{ - cli, ensure_commit_template_is_set, get_available_coauthors, get_main_author, set_main_author, - with_gitmessage_template_path_or_exit, Author, -}; -use std::fmt::Write; -use std::fs; -use std::process; - -fn main() { - let args = cli::GitMob::parse(); - - if args.list { - list_coauthors(); - process::exit(0); - } - - if let Some(initials) = args.overwrite { - override_main_author(&initials); - } - - write_coauthors_to_gitmessage_file(&args.coauthors); - ensure_commit_template_is_set(); -} - -fn list_coauthors() { - for (abbrev, author) in &get_available_coauthors() { - println!("{abbrev}\t{author}"); - } -} - -fn override_main_author(initials: &str) { - let all_authors = get_available_coauthors(); - match all_authors.get(initials) { - Some(new_main_author) => set_main_author(new_main_author), - None => { - eprintln!("Error: author with initials {initials} not found"); - process::exit(1); - } - } -} - -fn write_coauthors_to_gitmessage_file(coauthor_initials: &[String]) { - let coauthors = select_coauthors(coauthor_initials); - let mut content = String::from("\n\n"); - for author in &coauthors { - _ = writeln!(content, "Co-authored-by: {}", &author.to_string()); - } - - with_gitmessage_template_path_or_exit(|path| match fs::write(path, content) { - Ok(_) => { - println!("{}", get_main_author()); - for author in &coauthors { - println!("{author}"); - } - } - Err(e) => { - eprintln!("Error writing to .gitmessage template: {e}"); - process::exit(1); - } - }); -} - -fn select_coauthors(coauthor_initials: &[String]) -> Vec { - let all_coauthors = get_available_coauthors(); - let mut coauthors: Vec = Vec::new(); - - for initial in coauthor_initials { - match all_coauthors.get(initial) { - Some(coauthor) => coauthors.push(coauthor.clone()), - None => { - eprintln!("Error: author with initials {initial} not found"); - process::exit(1); - } - } - } - - coauthors -} diff --git a/src/bin/git-solo.rs b/src/bin/git-solo.rs deleted file mode 100644 index f5c81a2..0000000 --- a/src/bin/git-solo.rs +++ /dev/null @@ -1,16 +0,0 @@ -use clap::Parser; -use git_mob::{ - cli, ensure_commit_template_is_set, get_main_author, with_gitmessage_template_path_or_exit, -}; -use std::fs::File; - -fn main() { - let _opt = cli::GitSolo::parse(); - let main_author = get_main_author(); - println!("{main_author}"); - - with_gitmessage_template_path_or_exit(|path| { - let _template = File::create(path); - }); - ensure_commit_template_is_set(); -} diff --git a/src/cli.rs b/src/cli.rs index 6bfbfd7..2a4b89f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,56 +1,46 @@ -use clap::Parser; +use clap::{Arg, arg}; -#[derive(Parser, Debug)] -#[clap(version, name = "git-mob")] -/// Assemble a group of co-authors to help you on your coding quest -pub struct GitMob { - /// Prints list of available co-authors - #[clap(short, long)] - pub list: bool, - /// Overwrite the main author - #[clap(short, long)] - pub overwrite: Option, - /// A list of co-author initials - pub coauthors: Vec, +pub fn git_mob_cmd() -> clap::Command { +clap::Command::new("git-mob") + .bin_name("git-mob") + .author("Martin Frost, martin@frost.codes") + .version("0.0.0") + .about("A command-line tool for social coding") + .subcommand( + clap::Command::new("add") + .alias("add-coauthor") + .about("Add a coauthor to the database") + .arg(arg!( "The coauthor's handle")) + .arg(arg!( "The coauthor's name, in quotes")) + .arg(arg!( "The coauthor's email")) + ) + .subcommand( + clap::Command::new("edit") + .alias("edit-coauthor") + .about("Edit a coauthor in the database") + .arg(arg!( "The coauthor's handle")) + .arg(arg!( "The coauthor's name, in quotes")) + .arg(arg!( "The coauthor's email")) + ) + .subcommand( + clap::Command::new("remove") + .alias("remove-coauthor") + .alias("rm") + .about("Remove a coauthor from the database") + .arg(arg!( "The coauthor's handle")) + ) + .subcommand( + clap::Command::new("solo") + .about("Disband the mob. Continue coding on your own") + .arg_required_else_help(false)) + .subcommand( + clap::Command::new("list") + .about("List available coauthors in the database") + .arg_required_else_help(false)) + .subcommand( + clap::Command::new("with") + .about("Choose coauthors to join your mob") + .arg_required_else_help(true) + .arg(Arg::new("override").short('o').long("override").help("Override default author").value_name("handle")) + .arg(arg!([handles] "The coauthors you want in your mob").num_args(1..))) } - -#[derive(Parser, Debug)] -#[clap(name = "git-add-coauthor", version)] -/// Add a co-author to your list of available co-authors -pub struct GitAddCoauthor { - /// Co-author initials - pub initials: String, - /// The name of the co-author, in quotes, e.g. "Foo Bar" - pub name: String, - /// The email of the co-author - pub email: String, -} - -#[derive(Parser, Debug)] -#[clap(name = "git-edit-coauthor", version)] -/// Edit a co-author in your list of available co-authors -pub struct GitEditCoauthor { - /// Co-author initials - pub initials: String, - /// The name of the co-author, in quotes, e.g. "Foo Bar" - pub name: String, - /// The email of the co-author - pub email: String, -} - -#[derive(Parser, Debug)] -#[clap(name = "git-delete-coauthor", version)] -/// Delete a co-author from your list of available co-authors -pub struct GitDeleteCoauthor { - /// Co-author initials - pub initials: String, - /// The name of the co-author, in quotes, e.g. "Foo Bar" - pub name: String, - /// The email of the co-author - pub email: String, -} - -#[derive(Parser, Debug)] -#[clap(name = "git-solo", version)] -/// Disband the mob and continue working solo. -pub struct GitSolo {} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..54d6d0b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,195 @@ +use clap::{Arg, arg}; +use git_mob::{ + ensure_commit_template_is_set, get_available_coauthors, + get_main_author, set_main_author, with_gitmessage_template_path_or_exit, + Author, parse_coauthors_file, write_coauthors_file +}; +use std::fmt::Write; +use std::fs::File; +use std::fs; +use std::process; + +fn main() { + let matches = git_mob_cmd().get_matches(); + + match matches.subcommand_name() { + Some("add") => { + let args = matches.subcommand_matches("add").unwrap(); + let handle = args.get_one("handle").unwrap(); + let name = args.get_one("name").unwrap(); + let email = args.get_one("email").unwrap(); + add_coauthor(handle, name, email); + }, + Some("edit") => { + let args = matches.subcommand_matches("edit").unwrap(); + let handle = args.get_one("handle").unwrap(); + let name = args.get_one("name").unwrap(); + let email = args.get_one("email").unwrap(); + edit_coauthor(handle, name, email); + }, + Some("remove") => { + let handle = matches.subcommand_matches("remove").unwrap() + .get_one("handle").unwrap(); + remove_coauthor(handle); + }, + Some("list") => list_coauthors(), + Some("solo") => solo(), + Some("with") => { + let matches = matches.subcommand_matches("with").unwrap(); + let handles: Vec = + matches.get_many::("handles").unwrap().map(|s| s.clone()).collect(); + + if let Some(handle) = matches.get_one::("override") { + override_main_author(handle); + } + + write_coauthors_to_gitmessage_file(&handles); + }, + _ => println!("Something else"), + } +} + +pub fn git_mob_cmd() -> clap::Command { +clap::Command::new("git-mob") + .bin_name("git-mob") + .author("Martin Frost, martin@frost.codes") + .version("0.0.0") + .about("A command-line tool for social coding") + .subcommand( + clap::Command::new("add") + .alias("add-coauthor") + .about("Add a coauthor to the database") + .arg(arg!( "The coauthor's handle")) + .arg(arg!( "The coauthor's name, in quotes")) + .arg(arg!( "The coauthor's email")) + ) + .subcommand( + clap::Command::new("edit") + .alias("edit-coauthor") + .about("Edit a coauthor in the database") + .arg(arg!( "The coauthor's handle")) + .arg(arg!( "The coauthor's name, in quotes")) + .arg(arg!( "The coauthor's email")) + ) + .subcommand( + clap::Command::new("remove") + .alias("remove-coauthor") + .alias("rm") + .about("Remove a coauthor from the database") + .arg(arg!( "The coauthor's handle")) + ) + .subcommand( + clap::Command::new("solo") + .about("Disband the mob. Continue coding on your own") + .arg_required_else_help(false)) + .subcommand( + clap::Command::new("list") + .about("List available coauthors in the database") + .arg_required_else_help(false)) + .subcommand( + clap::Command::new("with") + .about("Choose coauthors to join your mob") + .arg_required_else_help(true) + .arg(Arg::new("override").short('o').long("override").help("Override default author").value_name("handle")) + .arg(arg!([handles] "The coauthors you want in your mob").num_args(1..))) +} + +fn list_coauthors() { + for (abbrev, author) in &get_available_coauthors() { + println!("{abbrev}\t{author}"); + } +} + +fn add_coauthor(handle: &String, name: &String, email: &String) { + let mut authors = parse_coauthors_file().unwrap_or_default(); + let new_author = Author { + name: name.to_string(), + email: email.to_string(), + }; + + authors.insert(handle.to_string(), new_author); + + write_coauthors_file(authors); +} + +fn edit_coauthor(handle: &String, name: &String, email: &String) { + let mut authors = get_available_coauthors(); + + if let Some(author) = authors.get(handle) { + let mut updated_author: Author = author.clone(); + updated_author.name = name.to_string(); + updated_author.email = email.to_string(); + + authors.insert(handle.to_string(), updated_author); + + write_coauthors_file(authors); + } else { + eprintln!("No author found with initials {}", handle); + process::exit(1); + }; +} + +fn remove_coauthor(handle: &String) { + let mut authors = get_available_coauthors(); + authors.remove(handle); + write_coauthors_file(authors); +} + +fn solo() { + let main_author = get_main_author(); + println!("{main_author}"); + + with_gitmessage_template_path_or_exit(|path| { + let _template = File::create(path); + }); + ensure_commit_template_is_set(); +} + +fn override_main_author(initials: &str) { + let all_authors = get_available_coauthors(); + match all_authors.get(initials) { + Some(new_main_author) => set_main_author(new_main_author), + None => { + eprintln!("Error: author with initials {initials} not found"); + process::exit(1); + } + } +} + +fn write_coauthors_to_gitmessage_file(coauthor_initials: &[String]) { + let coauthors = select_coauthors(coauthor_initials); + let mut content = String::from("\n\n"); + for author in &coauthors { + _ = writeln!(content, "Co-authored-by: {}", &author.to_string()); + } + + with_gitmessage_template_path_or_exit(|path| match fs::write(path, content) { + Ok(_) => { + println!("{}", get_main_author()); + for author in &coauthors { + println!("{author}"); + } + } + Err(e) => { + eprintln!("Error writing to .gitmessage template: {e}"); + process::exit(1); + } + }); +} + +fn select_coauthors(coauthor_initials: &[String]) -> Vec { + let all_coauthors = get_available_coauthors(); + let mut coauthors: Vec = Vec::new(); + + for initial in coauthor_initials { + match all_coauthors.get(initial) { + Some(coauthor) => coauthors.push(coauthor.clone()), + None => { + eprintln!("Error: author with initials {initial} not found"); + process::exit(1); + } + } + } + + coauthors +}