pub mod git;
use git::{GitContext, GitError};
pub mod print;
pub mod status;
pub mod submodule;
pub mod version;
use status::Status;
use crate::print::{println_error, println_hint, println_info, println_verbose, println_warn};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
#[cfg_attr(
feature = "cli",
command(author, about, version, arg_required_else_help(true))
)]
pub struct Magoo {
#[clap(subcommand)]
pub subcmd: Command,
#[cfg_attr(feature = "cli", clap(long, short('C'), default_value(".")))]
pub dir: String,
#[cfg_attr(feature = "cli", clap(flatten))]
pub common: OtherOptions,
}
impl Magoo {
pub fn run(&self) -> Result<(), GitError> {
self.subcmd.run(&self.dir, &self.common)
}
pub fn set_print_options(&self) {
self.subcmd.set_print_options();
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub enum Command {
Status(StatusCommand),
Install(InstallCommand),
Update(UpdateCommand),
Remove(RemoveCommand),
}
impl Command {
pub fn set_print_options(&self) {
match self {
Command::Status(cmd) => cmd.set_print_options(),
Command::Install(cmd) => cmd.set_print_options(),
Command::Update(cmd) => cmd.set_print_options(),
Command::Remove(cmd) => cmd.set_print_options(),
}
}
pub fn run(&self, dir: &str, common: &OtherOptions) -> Result<(), GitError> {
match self {
Command::Status(cmd) => {
cmd.run(dir, common)?;
}
Command::Install(cmd) => {
cmd.run(dir, common)?;
}
Command::Update(cmd) => {
cmd.run(dir, common)?;
}
Command::Remove(cmd) => {
cmd.run(dir, common)?;
}
}
Ok(())
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct StatusCommand {
#[cfg_attr(feature = "cli", clap(long))]
pub git: bool,
#[cfg_attr(feature = "cli", clap(long, short))]
pub long: bool,
#[cfg_attr(feature = "cli", clap(long, short))]
pub fix: bool,
#[cfg_attr(feature = "cli", clap(long, requires("fix")))]
pub delete: bool,
#[cfg_attr(feature = "cli", clap(flatten))]
pub options: PrintOptions,
}
impl StatusCommand {
pub fn set_print_options(&self) {
self.options.apply();
}
pub fn run(&self, dir: &str, common: &OtherOptions) -> Result<Status, GitError> {
let context = GitContext::try_from(dir)?;
if self.git {
context.check_version(true)?;
return Ok(Status::default());
}
if !common.allow_unsupported {
context.check_version(false)?;
}
let _guard = context.lock()?;
let mut status = Status::read_from(&context)?;
let mut flat_status = status.flattened_mut();
if flat_status.is_empty() {
println!("No submodules found");
return Ok(status);
}
if self.fix {
for submodule in flat_status.iter_mut() {
submodule.fix(&context, self.delete)?;
}
return Ok(status);
}
let dir_switch = if dir == "." {
"".to_string()
} else {
format!(" --dir {dir}")
};
for submodule in &flat_status {
submodule.print(&context, &dir_switch, self.long)?;
}
Ok(status)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct InstallCommand {
pub url: Option<String>,
#[cfg_attr(feature = "cli", arg(requires("url")))]
pub path: Option<String>,
#[cfg_attr(feature = "cli", clap(long, short))]
#[cfg_attr(feature = "cli", arg(requires("url")))]
pub branch: Option<String>,
#[cfg_attr(feature = "cli", clap(long))]
#[cfg_attr(feature = "cli", arg(requires("url")))]
pub name: Option<String>,
#[cfg_attr(feature = "cli", clap(long))]
#[cfg_attr(feature = "cli", arg(requires("url")))]
pub depth: Option<usize>,
#[cfg_attr(feature = "cli", clap(long, short))]
pub force: bool,
#[cfg_attr(feature = "cli", clap(long))]
pub no_recursive: bool,
#[cfg_attr(feature = "cli", clap(flatten))]
pub options: PrintOptions,
}
impl InstallCommand {
pub fn set_print_options(&self) {
self.options.apply();
}
pub fn run(&self, dir: &str, common: &OtherOptions) -> Result<(), GitError> {
let context = GitContext::try_from(dir)?;
if !common.allow_unsupported {
context.check_version(false)?;
}
let _guard = context.lock()?;
let mut status = Status::read_from(&context)?;
for submodule in status.flattened_mut() {
submodule.fix(&context, false)?;
}
match &self.url {
Some(url) => {
println_verbose!("Adding submodule from url: {url}");
context.submodule_add(
url,
self.path.as_deref(),
self.branch.as_deref(),
self.name.as_deref(),
self.depth.as_ref().copied(),
self.force,
)?;
}
None => {
println_verbose!("Installing submodules");
context.submodule_init(None)?;
context.submodule_sync(None, !self.no_recursive)?;
context.submodule_update(None, self.force, false, !self.no_recursive)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct UpdateCommand {
pub name: Option<String>,
#[cfg_attr(feature = "cli", clap(long, short))]
#[cfg_attr(feature = "cli", arg(requires("name")))]
pub branch: Option<String>,
#[cfg_attr(feature = "cli", clap(long))]
#[cfg_attr(feature = "cli", arg(requires("name"), conflicts_with("branch")))]
pub unset_branch: bool,
#[cfg_attr(feature = "cli", clap(long, short))]
#[cfg_attr(feature = "cli", arg(requires("name")))]
pub url: Option<String>,
#[cfg_attr(feature = "cli", clap(long, short))]
pub force: bool,
#[cfg_attr(feature = "cli", clap(long))]
pub bypass: bool,
#[cfg_attr(feature = "cli", clap(flatten))]
pub options: PrintOptions,
}
impl UpdateCommand {
pub fn set_print_options(&self) {
self.options.apply();
}
pub fn run(&self, dir: &str, common: &OtherOptions) -> Result<(), GitError> {
let context = GitContext::try_from(dir)?;
if !common.allow_unsupported {
context.check_version(false)?;
}
let _guard = context.lock()?;
match &self.name {
Some(name) => {
println_verbose!("Updating submodule: {name}");
let status = Status::read_from(&context)?;
let submodule = match status.modules.get(name) {
Some(submodule) => submodule,
None => {
println_error!("Submodule `{name}` not found!");
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 update this submodule, use `magoo update {other_name}`"
);
break;
}
}
}
}
return Err(GitError::NeedFix(false));
}
};
if !submodule.is_healthy(&context)? {
if !self.bypass {
println_error!("Submodule `{name}` is not healthy!");
println_hint!(
" run `magoo status` to investigate. Some issues might be fixable with `magoo status --fix`."
);
println_hint!(
" alternatively, use the `--bypass` flag to ignore and continue anyway."
);
return Err(GitError::NeedFix(false));
}
println_warn!("Bypassing warnings from unhealthy submodule `{name}`");
}
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` and then re-adding it"
);
return Err(GitError::NeedFix(false));
}
};
context.submodule_init(Some(path))?;
if self.unset_branch {
context.submodule_set_branch(path, None)?;
} else if let Some(branch) = &self.branch {
context.submodule_set_branch(path, Some(branch))?;
}
if let Some(url) = &self.url {
context.submodule_set_url(path, url)?;
}
context.submodule_sync(Some(path), false)?;
context.submodule_update(Some(path), self.force, true, false)?;
}
None => {
println_verbose!("Updating submodules");
context.submodule_init(None)?;
context.submodule_sync(None, false)?;
context.submodule_update(None, self.force, true, false)?;
}
}
println_info!();
println_info!("Submodules updated successfully.");
println_hint!(
" run `git status` to check the changes and run `git add ...` to stage them"
);
println_hint!(" run `magoo status` to check the status of the submodules");
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct RemoveCommand {
pub name: String,
#[cfg_attr(feature = "cli", clap(long, short))]
pub force: bool,
#[cfg_attr(feature = "cli", clap(long))]
#[cfg_attr(feature = "cli", arg(conflicts_with("force")))]
pub force_deinit: bool,
#[cfg_attr(feature = "cli", clap(flatten))]
pub options: PrintOptions,
}
impl RemoveCommand {
pub fn set_print_options(&self) {
self.options.apply();
}
pub fn run(&self, dir: &str, common: &OtherOptions) -> Result<(), GitError> {
let context = GitContext::try_from(dir)?;
if !common.allow_unsupported {
context.check_version(false)?;
}
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!");
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(())
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct PrintOptions {
#[cfg_attr(feature = "cli", clap(long))]
pub verbose: bool,
#[cfg_attr(feature = "cli", clap(long, short))]
pub quiet: bool,
#[cfg_attr(feature = "cli", clap(skip))]
pub color: Option<bool>,
}
impl PrintOptions {
pub fn apply(&self) {
print::set_options(self.verbose, self.quiet, self.color);
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct OtherOptions {
#[cfg_attr(feature = "cli", clap(long))]
pub allow_unsupported: bool,
}