Refactor
This refactors a huge chunk of the code base to make it more maintainable. Main points: * Proper separation between bin and lib. Bin handles argument parsing & validation and (most of) the output. Lib provides interfaces for all opreations. * Before, libgit2 internals were literred throughout the codebase, mainly the `Repository` struct and `git2::Error` in Results. They library is now properly wrapped in `repo.rs`, which exposes only the required functionality. It also standardizes the Error messages (they're just Strings for now) and handles stuff like the copious usage of Options to wrap maybe-invalid-utf-8 values. The program will still panic on non-utf-8 Strings e.g. in git remotes, but I guess this is acceptable. If you actually manage to hit this case, I promise I'll fix it :D * Many unwraps() are now gone and properly handled. * The table printing functionality is now confined to `table.rs`, instead of passing tables as parameters through the whole program.
This commit is contained in:
129
src/grm/cmd.rs
Normal file
129
src/grm/cmd.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use clap::{AppSettings, Parser};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
name = clap::crate_name!(),
|
||||
version = clap::crate_version!(),
|
||||
author = clap::crate_authors!("\n"),
|
||||
about = clap::crate_description!(),
|
||||
long_version = clap::crate_version!(),
|
||||
license = clap::crate_license!(),
|
||||
setting = AppSettings::DeriveDisplayOrder,
|
||||
setting = AppSettings::PropagateVersion,
|
||||
setting = AppSettings::HelpRequired,
|
||||
)]
|
||||
pub struct Opts {
|
||||
#[clap(subcommand)]
|
||||
pub subcmd: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum SubCommand {
|
||||
#[clap(about = "Manage repositories")]
|
||||
Repos(Repos),
|
||||
#[clap(visible_alias = "wt", about = "Manage worktrees")]
|
||||
Worktree(Worktree),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Repos {
|
||||
#[clap(subcommand, name = "action")]
|
||||
pub action: ReposAction,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum ReposAction {
|
||||
#[clap(
|
||||
visible_alias = "run",
|
||||
about = "Synchronize the repositories to the configured values"
|
||||
)]
|
||||
Sync(Sync),
|
||||
#[clap(about = "Generate a repository configuration from an existing file tree")]
|
||||
Find(Find),
|
||||
#[clap(about = "Show status of configured repositories")]
|
||||
Status(OptionalConfig),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
pub struct Sync {
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
default_value = "./config.toml",
|
||||
about = "Path to the configuration file"
|
||||
)]
|
||||
pub config: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
pub struct OptionalConfig {
|
||||
#[clap(short, long, about = "Path to the configuration file")]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Find {
|
||||
#[clap(about = "The path to search through")]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Worktree {
|
||||
#[clap(subcommand, name = "action")]
|
||||
pub action: WorktreeAction,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum WorktreeAction {
|
||||
#[clap(about = "Add a new worktree")]
|
||||
Add(WorktreeAddArgs),
|
||||
#[clap(about = "Add an existing worktree")]
|
||||
Delete(WorktreeDeleteArgs),
|
||||
#[clap(about = "Show state of existing worktrees")]
|
||||
Status(WorktreeStatusArgs),
|
||||
#[clap(about = "Convert a normal repository to a worktree setup")]
|
||||
Convert(WorktreeConvertArgs),
|
||||
#[clap(about = "Clean all worktrees that do not contain uncommited/unpushed changes")]
|
||||
Clean(WorktreeCleanArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeAddArgs {
|
||||
#[clap(about = "Name of the worktree")]
|
||||
pub name: String,
|
||||
|
||||
#[clap(
|
||||
short = 'n',
|
||||
long = "branch-namespace",
|
||||
about = "Namespace of the branch"
|
||||
)]
|
||||
pub branch_namespace: Option<String>,
|
||||
#[clap(short = 't', long = "track", about = "Remote branch to track")]
|
||||
pub track: Option<String>,
|
||||
}
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeDeleteArgs {
|
||||
#[clap(about = "Name of the worktree")]
|
||||
pub name: String,
|
||||
|
||||
#[clap(
|
||||
long = "force",
|
||||
about = "Force deletion, even when there are uncommitted/unpushed changes"
|
||||
)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeStatusArgs {}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeConvertArgs {}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeCleanArgs {}
|
||||
|
||||
pub fn parse() -> Opts {
|
||||
Opts::parse()
|
||||
}
|
||||
278
src/grm/main.rs
Normal file
278
src/grm/main.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
mod cmd;
|
||||
|
||||
use grm::config;
|
||||
use grm::output::*;
|
||||
|
||||
fn main() {
|
||||
let opts = cmd::parse();
|
||||
|
||||
match opts.subcmd {
|
||||
cmd::SubCommand::Repos(repos) => match repos.action {
|
||||
cmd::ReposAction::Sync(sync) => {
|
||||
let config = match config::read_config(&sync.config) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
print_error(&error);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
match grm::sync_trees(config) {
|
||||
Ok(success) => {
|
||||
if !success {
|
||||
process::exit(1)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error syncing trees: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd::ReposAction::Status(args) => match &args.config {
|
||||
Some(config_path) => {
|
||||
let config = match config::read_config(config_path) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
print_error(&error);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
match grm::table::get_status_table(config) {
|
||||
Ok((tables, errors)) => {
|
||||
for table in tables {
|
||||
println!("{}", table);
|
||||
}
|
||||
for error in errors {
|
||||
print_error(&format!("Error: {}", error));
|
||||
}
|
||||
}
|
||||
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let dir = match std::env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
print_error(&format!("Could not open current directory: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match grm::table::show_single_repo_status(&dir) {
|
||||
Ok((table, warnings)) => {
|
||||
println!("{}", table);
|
||||
for warning in warnings {
|
||||
print_warning(&warning);
|
||||
}
|
||||
}
|
||||
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||
}
|
||||
}
|
||||
},
|
||||
cmd::ReposAction::Find(find) => {
|
||||
let path = Path::new(&find.path);
|
||||
if !path.exists() {
|
||||
print_error(&format!("Path \"{}\" does not exist", path.display()));
|
||||
process::exit(1);
|
||||
}
|
||||
if !path.is_dir() {
|
||||
print_error(&format!("Path \"{}\" is not a directory", path.display()));
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let path = match path.canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
print_error(&format!(
|
||||
"Failed to canonicalize path \"{}\". This is a bug. Error message: {}",
|
||||
&path.display(),
|
||||
error
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let found_repos = match grm::find_in_tree(&path) {
|
||||
Ok(repos) => repos,
|
||||
Err(error) => {
|
||||
print_error(&error);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let trees = grm::config::Trees::from_vec(vec![found_repos]);
|
||||
if trees.as_vec_ref().iter().all(|t| match &t.repos {
|
||||
None => false,
|
||||
Some(r) => r.is_empty(),
|
||||
}) {
|
||||
print_warning("No repositories found");
|
||||
} else {
|
||||
let config = trees.to_config();
|
||||
|
||||
let toml = match config.as_toml() {
|
||||
Ok(toml) => toml,
|
||||
Err(error) => {
|
||||
print_error(&format!("Failed converting config to TOML: {}", &error));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
print!("{}", toml);
|
||||
}
|
||||
}
|
||||
},
|
||||
cmd::SubCommand::Worktree(args) => {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|error| {
|
||||
print_error(&format!("Could not open current directory: {}", error));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
match args.action {
|
||||
cmd::WorktreeAction::Add(action_args) => {
|
||||
let track = match &action_args.track {
|
||||
Some(branch) => {
|
||||
let split = branch.split_once('/');
|
||||
|
||||
if split.is_none() ||
|
||||
split.unwrap().0.len() == 0
|
||||
||split.unwrap().1.len() == 0 {
|
||||
print_error("Tracking branch needs to match the pattern <remote>/<branch_name>");
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
// unwrap() here is safe because we checked for
|
||||
// is_none() explictily before
|
||||
let (remote_name, remote_branch_name) = split.unwrap();
|
||||
|
||||
Some((remote_name, remote_branch_name))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
match grm::add_worktree(
|
||||
&cwd,
|
||||
&action_args.name,
|
||||
action_args.branch_namespace.as_deref(),
|
||||
track,
|
||||
) {
|
||||
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
||||
Err(error) => {
|
||||
print_error(&format!("Error creating worktree: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd::WorktreeAction::Delete(action_args) => {
|
||||
let worktree_dir = cwd.join(&action_args.name);
|
||||
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||
print_error(&format!("Error opening repository: {}", error));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
match repo.remove_worktree(&action_args.name, &worktree_dir, action_args.force)
|
||||
{
|
||||
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
||||
Err(error) => {
|
||||
match error {
|
||||
grm::WorktreeRemoveFailureReason::Error(msg) => {
|
||||
print_error(&msg);
|
||||
process::exit(1);
|
||||
}
|
||||
grm::WorktreeRemoveFailureReason::Changes(changes) => {
|
||||
print_warning(&format!(
|
||||
"Changes in worktree: {}. Refusing to delete",
|
||||
changes
|
||||
));
|
||||
}
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd::WorktreeAction::Status(_args) => {
|
||||
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||
print_error(&format!("Error opening repository: {}", error));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
match grm::table::get_worktree_status_table(&repo, &cwd) {
|
||||
Ok((table, errors)) => {
|
||||
println!("{}", table);
|
||||
for error in errors {
|
||||
print_error(&format!("Error: {}", error));
|
||||
}
|
||||
}
|
||||
Err(error) => print_error(&format!("Error getting status: {}", error)),
|
||||
}
|
||||
}
|
||||
cmd::WorktreeAction::Convert(_args) => {
|
||||
// Converting works like this:
|
||||
// * Check whether there are uncommitted/unpushed changes
|
||||
// * Move the contents of .git dir to the worktree directory
|
||||
// * Remove all files
|
||||
// * Set `core.bare` to `true`
|
||||
|
||||
let repo = grm::Repo::open(&cwd, false).unwrap_or_else(|error| {
|
||||
if error.kind == grm::RepoErrorKind::NotFound {
|
||||
print_error("Directory does not contain a git repository");
|
||||
} else {
|
||||
print_error(&format!("Opening repository failed: {}", error));
|
||||
}
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let status = repo.status(false).unwrap_or_else(|error| {
|
||||
print_error(&format!("Failed getting repo changes: {}", error));
|
||||
process::exit(1);
|
||||
});
|
||||
if status.changes.is_some() {
|
||||
print_error("Changes found in repository, refusing to convert");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
match repo.convert_to_worktree(&cwd) {
|
||||
Ok(_) => print_success("Conversion done"),
|
||||
Err(error) => print_error(&format!("Error during conversion: {}", error)),
|
||||
}
|
||||
}
|
||||
cmd::WorktreeAction::Clean(_args) => {
|
||||
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
|
||||
if error.kind == grm::RepoErrorKind::NotFound {
|
||||
print_error("Directory does not contain a git repository");
|
||||
} else {
|
||||
print_error(&format!("Opening repository failed: {}", error));
|
||||
}
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
match repo.cleanup_worktrees(&cwd) {
|
||||
Ok(warnings) => {
|
||||
for warning in warnings {
|
||||
print_warning(&warning);
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
print_error(&format!("Worktree cleanup failed: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
for unmanaged_worktree in
|
||||
repo.find_unmanaged_worktrees(&cwd).unwrap_or_else(|error| {
|
||||
print_error(&format!("Failed finding unmanaged worktrees: {}", error));
|
||||
process::exit(1);
|
||||
})
|
||||
{
|
||||
print_warning(&format!(
|
||||
"Found {}, which is not a valid worktree directory!",
|
||||
&unmanaged_worktree
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user