From 70de906fe691eb77fbfda8059deb880efe8e751f Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 25 Nov 2023 14:28:11 -0800 Subject: [PATCH] all implemented --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +-- README.txtpp.md | 10 ++-- src/git.rs | 8 ++++ src/lib.rs | 122 +++++++++++++++++++++++++++++++++++++---------- src/status.rs | 39 +++++---------- src/submodule.rs | 47 ++++++++---------- 8 files changed, 146 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6930c52..94ae4d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,7 +157,7 @@ checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "magoo" -version = "0.0.4" +version = "0.1.0" dependencies = [ "clap", "fs4", diff --git a/Cargo.toml b/Cargo.toml index 414fd78..350faf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "magoo" -version = "0.0.4" +version = "0.1.0" edition = "2021" description = "A wrapper for git submodule that simplifies the workflows" repository = "https://github.com/Pistonite/magoo" diff --git a/README.md b/README.md index 9fd7708..05a2998 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![License Badge](https://img.shields.io/github/license/Pistonite/magoo) ![Issue Badge](https://img.shields.io/github/issues/Pistonite/magoo) -**In Development. commands left are: update, remove** +**In Development. commands left are: remove** This ![magoo](https://raw.githubusercontent.com/Pistonite/magoo/main/magoo.webp) is Magoo, he helps you manage git submodules with ease, like `npm` or `cargo`, but for submodules. @@ -78,7 +78,7 @@ It also deletes submodules that are deleted by others (by running `status --fix ### Show submodule status ```bash -magoo status [--long] [--all] [--fix] +magoo status [--long] [--fix] ``` Shows everything ![magoo](https://raw.githubusercontent.com/Pistonite/magoo/main/magoo.webp) knows about submodules in the current repo. @@ -91,7 +91,7 @@ The `--all` option can potentially find more residues. ### Update submodules ![magoo](https://raw.githubusercontent.com/Pistonite/magoo/main/magoo.webp) updates the submodule by fetching and checking out the latest updates from the remote, tracked by -the `BRANCH` specified when you add it. +the `BRANCH` specified when you added it. - Update all submodules to the latest ```bash diff --git a/README.txtpp.md b/README.txtpp.md index 5a39c0f..f18a1af 100644 --- a/README.txtpp.md +++ b/README.txtpp.md @@ -7,8 +7,6 @@ TXTPP#include magoo.txt ![License Badge](https://img.shields.io/github/license/Pistonite/magoo) ![Issue Badge](https://img.shields.io/github/issues/Pistonite/magoo) -**In Development. commands left are: update, remove** - TXTPP#tag MAGOO TXTPP#include magoo.txt This MAGOO is Magoo, he helps you manage git submodules with ease, like `npm` or `cargo`, but for submodules. @@ -106,7 +104,7 @@ It also deletes submodules that are deleted by others (by running `status --fix ### Show submodule status ```bash -magoo status [--long] [--all] [--fix] +magoo status [--long] [--fix] ``` TXTPP#tag MAGOO TXTPP#include magoo.txt @@ -125,7 +123,7 @@ TXTPP#tag MAGOO TXTPP#include magoo.txt MAGOO updates the submodule by fetching and checking out the latest updates from the remote, tracked by -the `BRANCH` specified when you add it. +the `BRANCH` specified when you added it. - Update all submodules to the latest ```bash @@ -143,7 +141,7 @@ the `BRANCH` specified when you add it. ### Remove submodules TXTPP#tag MAGOO TXTPP#include magoo.txt -MAGOO can remove a submodule with ease: +MAGOO will remove every trace of a submodule with a single command: ```bash magoo remove NAME ``` @@ -151,4 +149,4 @@ magoo remove NAME TXTPP#tag MAGOO TXTPP#include magoo.txt Note: Newer versions of git lets you delete a submodule with `git rm`. However, it doesn't delete the content in -`.git/modules`. MAGOO deletes those. +`.git/modules`. MAGOO deletes those as well. diff --git a/src/git.rs b/src/git.rs index 3260874..a4a2b6a 100644 --- a/src/git.rs +++ b/src/git.rs @@ -345,6 +345,14 @@ impl GitContext { Ok(()) } + /// Run `git add` + pub fn add(&self, path: &str) -> Result<(), GitError> { + let top_level_dir = self.top_level_dir()?.display().to_string(); + + self.run_git_command(&["-C", &top_level_dir, "add", path], false)?; + Ok(()) + } + /// Runs `git submodule deinit [-- ]`. Path should be from top level pub fn submodule_deinit(&self, path: Option<&str>, force: bool) -> Result<(), GitError> { let top_level_dir = self.top_level_dir()?.display().to_string(); diff --git a/src/lib.rs b/src/lib.rs index bd1475d..70b0078 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,6 @@ //! //! let command = magoo::StatusCommand { //! git: true, -//! all: false, //! fix: false, //! long: false, //! options: PrintOptions { @@ -46,14 +45,13 @@ //! // for assertion below only //! use magoo::{Command, StatusCommand, PrintOptions}; //! -//! let magoo = Magoo::try_parse_from(["magoo", "--dir", "my/repo", "status", "--all", "--verbose"]).unwrap(); +//! let magoo = Magoo::try_parse_from(["magoo", "--dir", "my/repo", "status", "--long", "--verbose"]).unwrap(); //! //! assert_eq!(magoo, Magoo { //! subcmd: Command::Status(StatusCommand { //! git: false, -//! all: true, //! fix: false, -//! long: false, +//! long: true, //! options: PrintOptions { //! verbose: true, //! quiet: false, @@ -136,8 +134,7 @@ impl Command { Command::Status(cmd) => cmd.set_print_options(), Command::Install(cmd) => cmd.set_print_options(), Command::Update(cmd) => cmd.set_print_options(), - _ => todo!(), - // Command::Remove(cmd) => cmd.set_print_options(), + Command::Remove(cmd) => cmd.set_print_options(), } } @@ -153,8 +150,9 @@ impl Command { Command::Update(cmd) => { cmd.run(dir)?; } - _ => todo!(), - // Command::Remove(cmd) => cmd.run(dir), + Command::Remove(cmd) => { + cmd.run(dir)?; + } } Ok(()) @@ -173,12 +171,6 @@ pub struct StatusCommand { #[cfg_attr(feature = "cli", clap(long, short))] pub long: bool, - /// Show every trace of submodules found. - /// - /// This includes modules found in `.git/modules`, but not in anywhere else. - #[cfg_attr(feature = "cli", clap(long, short))] - pub all: bool, - /// Fix the submodules to be in a consistent state. (CAUTION - you should never have to do this if you let magoo manage the submodules, be sure to read the details in `magoo status --help` before using!) /// /// If any submodule appears to be broken (likely due to changing @@ -212,7 +204,7 @@ impl StatusCommand { return Ok(Status::default()); } - let mut status = Status::read_from(&context, self.all)?; + let mut status = Status::read_from(&context)?; let mut flat_status = status.flattened_mut(); if flat_status.is_empty() { println!("No submodules found"); @@ -231,10 +223,8 @@ impl StatusCommand { format!(" --dir {dir}") }; - let all_switch = if self.all { " --all" } else { "" }; - for submodule in &flat_status { - submodule.print(&context, &dir_switch, all_switch, self.long)?; + submodule.print(&context, &dir_switch, self.long)?; } Ok(status) } @@ -300,7 +290,7 @@ impl InstallCommand { let context = GitContext::try_from(dir)?; let _guard = context.lock()?; - let mut status = Status::read_from(&context, true)?; + let mut status = Status::read_from(&context)?; for submodule in status.flattened_mut() { submodule.fix(&context)?; } @@ -363,6 +353,7 @@ pub struct UpdateCommand { #[cfg_attr(feature = "cli", clap(long))] pub bypass: bool, + /// Print options #[cfg_attr(feature = "cli", clap(flatten))] pub options: PrintOptions, } @@ -381,7 +372,7 @@ impl UpdateCommand { match &self.name { Some(name) => { println_verbose!("Updating submodule: {name}"); - let status = Status::read_from(&context, false)?; + let status = Status::read_from(&context)?; let submodule = match status.modules.get(name) { Some(submodule) => submodule, None => { @@ -393,7 +384,7 @@ impl UpdateCommand { if let Some(path) = submodule.path() { if path == name { println_hint!(" however, there is a submodule \"{other_name}\" with path \"{path}\""); - println_hint!(" if you meant this submodule, use `magoo update {other_name}`"); + println_hint!(" if you meant to update this submodule, use `magoo update {other_name}`"); break; } } @@ -462,11 +453,94 @@ pub struct RemoveCommand { /// The name of the submodule to remove pub name: String, - /// Whether to force the submodule to be removed - /// - /// The submodule will be removed even if it has local changes. (`git submodule deinit -f`) + /// Force remove the submodule. Will delete any local changes to the submodule #[cfg_attr(feature = "cli", clap(long, short))] pub force: bool, + + /// Pass the `--force` flag to `git submobule deinit` + /// + /// Cannot be used together with `--force`, since `--force` skips de-initializing. + #[cfg_attr(feature = "cli", clap(long))] + #[cfg_attr(feature = "cli", arg(conflicts_with("force")))] + pub force_deinit: bool, + + /// Print options + #[cfg_attr(feature = "cli", clap(flatten))] + pub options: PrintOptions, +} + +impl RemoveCommand { + /// Apply the print options + pub fn set_print_options(&self) { + self.options.apply(); + } + + /// Run the command in the given directory + pub fn run(&self, dir: &str) -> Result<(), GitError> { + let context = GitContext::try_from(dir)?; + let _guard = context.lock()?; + + let name = &self.name; + + println_verbose!("Removing submodule: {name}"); + let mut status = Status::read_from(&context)?; + let submodule = match status.modules.get_mut(name) { + Some(submodule) => submodule, + None => { + println_error!("Submodule `{name}` not found!"); + // maybe user passed in path instead of name? + println_verbose!("Trying to search for a path matching `{name}`"); + for submodule in status.flattened() { + if let Some(other_name) = submodule.name() { + if let Some(path) = submodule.path() { + if path == name { + println_hint!(" however, there is a submodule \"{other_name}\" with path \"{path}\""); + println_hint!(" if you meant to remove this submodule, use `magoo remove {other_name}`"); + break; + } + } + } + } + + return Err(GitError::NeedFix(false)); + } + }; + + if self.force { + println_verbose!("Removing (force): {name}"); + submodule.force_delete(&context)?; + } else { + let path = match submodule.path() { + Some(x) => x, + None => { + println_error!("Submodule `{name}` does not have a path!"); + println_hint!(" run `magoo status` to investigate."); + println_hint!(" if you are unsure of the problem, try hard removing the submodule with `magoo remove {name} --force`"); + return Err(GitError::NeedFix(false)); + } + }; + if let Err(e) = context.submodule_deinit(Some(path), self.force_deinit) { + println_error!("Failed to deinitialize submodule `{name}`: {e}"); + println_hint!( + " try running with `--force-deinit` to force deinitialize the module" + ); + println_hint!( + " alternatively, running with `--force` will remove the module anyway." + ); + return Err(GitError::NeedFix(false)); + } + + submodule.force_remove_module_dir(&context)?; + submodule.force_remove_config(&context)?; + submodule.force_remove_from_dot_gitmodules(&context)?; + submodule.force_remove_from_index(&context)?; + } + + println_info!(); + println_info!("Submodules removed successfully."); + println_hint!(" run `git status` to check the changes"); + Ok(()) + } } /// Printing options for all commands diff --git a/src/status.rs b/src/status.rs index b6c0a40..5f5b798 100644 --- a/src/status.rs +++ b/src/status.rs @@ -90,24 +90,13 @@ impl Status { } /// Get the submodule status in the repository. - /// - /// If `all` is false, it will not include submodules that are only in the index and in - /// `.git/modules` - pub fn read_from(context: &GitContext, all: bool) -> Result { + pub fn read_from(context: &GitContext) -> Result { let mut status = Self::default(); status.read_dot_gitmodules(context)?; status.read_dot_git_config(context)?; // read .git/modules - if all { - status.find_all_git_modules(context)?; - } else { - for (name, submodule) in status.modules.iter_mut() { - if let Ok(module) = Self::read_git_module(name, context) { - submodule.in_modules = Some(module); - } - } - }; - status.read_submodules_in_index(context, all)?; + status.find_all_git_modules(context)?; + status.read_submodules_in_index(context)?; Ok(status) } @@ -337,11 +326,7 @@ impl Status { } /// Use `git ls-files` to list submodules stored in the index into self - fn read_submodules_in_index( - &mut self, - context: &GitContext, - all: bool, - ) -> Result<(), GitError> { + fn read_submodules_in_index(&mut self, context: &GitContext) -> Result<(), GitError> { let index_list = context.ls_files(&[r#"--format=%(objectmode) %(objectname) %(path)"#])?; let mut path_to_index_object = BTreeMap::new(); @@ -387,15 +372,13 @@ impl Status { } } - if all { - for index_obj in path_to_index_object.into_values() { - self.nameless.push(Submodule { - in_gitmodules: None, - in_config: None, - in_index: Some(index_obj), - in_modules: None, - }); - } + for index_obj in path_to_index_object.into_values() { + self.nameless.push(Submodule { + in_gitmodules: None, + in_config: None, + in_index: Some(index_obj), + in_modules: None, + }); } Ok(()) } diff --git a/src/submodule.rs b/src/submodule.rs index 2756dad..fbea1ad 100644 --- a/src/submodule.rs +++ b/src/submodule.rs @@ -97,6 +97,7 @@ impl Submodule { None } + /// Get the short version (8 digits) of the commit in the index pub fn index_commit_short(&self) -> Option<&str> { self.index_commit().map(|s| &s[..7]) } @@ -111,15 +112,16 @@ impl Submodule { None } + /// Get the short version (8 digits) of the commit currently checked out pub fn head_commit_short(&self) -> Option<&str> { self.head_commit().map(|s| &s[..7]) } + /// Print status pub fn print( &self, context: &GitContext, dir_switch: &str, - all_switch: &str, long: bool, ) -> Result<(), GitError> { let name = match self.name() { @@ -234,22 +236,16 @@ impl Submodule { if !self.is_module_consistent(context)? { println_error!("! submodule has residue"); - println_hint!( - " run `magoo{dir_switch} status --fix{all_switch}` to fix all submodules" - ); + println_hint!(" run `magoo{dir_switch} status --fix` to fix all submodules"); } if !self.resolved_paths(context)?.is_consistent() { println_error!("! inconsistent paths"); - println_hint!( - " run `magoo{dir_switch} status --fix{all_switch}` to fix all submodules" - ); + println_hint!(" run `magoo{dir_switch} status --fix` to fix all submodules"); } let issue = self.find_issue(); if issue != PartsIssue::None { println_error!("! inconsistent state ({})", issue.describe()); - println_hint!( - " run `magoo{dir_switch} status --fix{all_switch}` to fix all submodules" - ); + println_hint!(" run `magoo{dir_switch} status --fix` to fix all submodules"); } if long { @@ -439,26 +435,21 @@ impl Submodule { } } - /// Deinitialize the submodule by removing the worktree and the git dir, and in .git/config - pub fn deinit(&mut self, context: &GitContext, force: bool) -> Result<(), GitError> { - let path = match &self.in_index { - None => { - return Err(GitError::InvalidIndex( - "submodule can only be deinitialized if it is in the index".to_string(), - )); - } - Some(index) => &index.path, - }; - context.submodule_deinit(Some(path), force)?; - self.force_remove_module_dir(context) - } - /// Delete the submodule by removing the configuration and directories that reference it pub fn force_delete(&mut self, context: &GitContext) -> Result<(), GitError> { self.force_remove_from_index(context)?; self.force_remove_module_dir(context)?; self.force_remove_config(context)?; + self.force_remove_from_dot_gitmodules(context)?; + Ok(()) + } + + /// Delete the submodule section in .gitmodules + pub fn force_remove_from_dot_gitmodules( + &mut self, + context: &GitContext, + ) -> Result<(), GitError> { if let Some(in_gitmodules) = &self.in_gitmodules { let top_level_dir = context.top_level_dir()?; let name = &in_gitmodules.name; @@ -467,16 +458,18 @@ impl Submodule { top_level_dir.join(".gitmodules"), &format!("submodule.{name}"), ); + context.add(".gitmodules")?; } self.in_gitmodules = None; - Ok(()) } + /// Remove the submodule from index pub fn force_remove_from_index(&mut self, context: &GitContext) -> Result<(), GitError> { if let Some(in_index) = &self.in_index { println_info!("Deleting `{}` in index", in_index.path); context.remove_from_index(&in_index.path)?; + context.add(".gitmodules")?; } self.in_index = None; Ok(()) @@ -634,8 +627,8 @@ impl PartsIssue { pub fn describe(&self) -> &'static str { match self { PartsIssue::None => "none", - PartsIssue::Residue => "residue", - PartsIssue::MissingIndex => "index missing", + PartsIssue::Residue => "has residue from removal", + PartsIssue::MissingIndex => "missing in index", PartsIssue::MissingInGitModules => "not in .gitmodules", } }