pub mod acquire;
pub mod auth;
pub mod backup;
pub mod ci;
pub mod hf;
pub mod hook;
pub mod pr;
pub mod release;
pub mod sanitize;
pub mod scan;
pub mod server;
use crate::acquire::strategy::AcquireOptions;
use crate::auth::SecureString;
#[allow(unused_imports)]
use crate::cli::args::HfCommands;
use crate::cli::args::{
Commands, ConfigCommands, OutputConfig, RemoteCommands, SnapshotCommands, StackCommands,
StashCommands, WorktreeCommands,
};
use crate::cli::UI;
use crate::ops;
use anyhow::{bail, Result};
use std::path::PathBuf;
pub async fn execute_command(cmd: Commands, output: OutputConfig) -> Result<()> {
let ui = UI::from_config(&output);
match cmd {
Commands::Acquire {
source,
output: out_dir,
strategy,
no_history,
fail_on,
force,
token,
ssh: _,
ssh_key,
recurse_submodules,
no_submodules: _,
lfs,
depth,
single_branch,
branch,
} => {
let opts = AcquireOptions {
token: token.map(SecureString::from),
ssh_key,
depth,
single_branch,
recurse_submodules,
lfs,
branch,
};
acquire::execute(
acquire::AcquireParams {
source,
output: out_dir,
strategy,
no_history,
fail_on,
force,
opts,
},
&ui,
)
.await
}
Commands::Clone {
source,
output: out_dir,
force,
token,
ssh: _,
ssh_key,
recurse_submodules,
lfs,
depth,
single_branch,
branch,
} => {
let opts = AcquireOptions {
token: token.map(SecureString::from),
ssh_key,
depth,
single_branch,
recurse_submodules,
lfs,
branch,
};
acquire::execute(
acquire::AcquireParams {
source,
output: out_dir,
strategy: "zip-with-history".to_string(),
no_history: false,
fail_on: "high".to_string(),
force,
opts,
},
&ui,
)
.await
}
Commands::Scan {
path,
fail_on,
include_git,
create_issues,
issue_threshold,
} => {
scan::execute(
path,
fail_on,
include_git,
create_issues,
issue_threshold,
&ui,
)
.await
}
Commands::Sanitize {
path,
dry_run,
backup,
} => sanitize::execute(path, dry_run, backup, &ui).await,
Commands::Unzip {
archive,
output: out_dir,
} => {
use crate::archive::{ArchiveValidator, SafeExtractor};
use crate::core::{Config, ScanEngine};
let config = Config::default();
let dest = out_dir.unwrap_or_else(|| {
archive
.file_stem()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("extracted"))
});
ui.header("Safe Extraction");
ui.field("Archive", archive.display());
ui.field("Target", dest.display());
ui.blank();
let validator = ArchiveValidator::new(config.archive.clone());
let extractor = SafeExtractor::new(validator);
let spinner = ui.spinner("Extracting archive...");
extractor.extract_safe(&archive, &dest).await?;
ui.finish_progress(&spinner, "Extraction complete");
let spinner = ui.spinner("Running security scan...");
let engine = ScanEngine::new(config);
let report = engine.scan_directory(&dest).await?;
ui.finish_progress(&spinner, "");
if output.json {
let findings: Vec<serde_json::Value> = report
.findings
.iter()
.map(|f| {
serde_json::json!({
"id": f.id,
"title": f.title,
"severity": format!("{:?}", f.severity),
"file": f.file_path.as_ref().map(|p| p.display().to_string()),
"line": f.line_start,
"description": f.description,
})
})
.collect();
ui.json_out(&serde_json::json!({
"scanned_files": report.scanned_files,
"findings_count": report.findings.len(),
"findings": findings,
}));
} else {
ui.field("Files scanned", report.scanned_files);
ui.field("Findings", report.findings.len());
for finding in &report.findings {
ui.finding(finding);
}
ui.result_banner(
report.findings.is_empty(),
if report.findings.is_empty() {
"Extraction and scan complete"
} else {
"Extraction complete with findings"
},
&[],
);
}
Ok(())
}
Commands::Hook { action } => hook::execute(action, &ui).await,
Commands::PreCommit { fail_on } => hook::pre_commit(fail_on, &ui).await,
Commands::PrePush { fail_on } => hook::pre_push(fail_on, &ui).await,
Commands::Auth { action } => auth::execute(action, &ui).await,
Commands::Backup { action } => backup::execute(action, &ui).await,
Commands::Server { action } => server::execute(action, &ui).await,
Commands::Pr { action } => pr::execute(action, &ui).await,
Commands::Release { action } => release::execute(action, &ui).await,
Commands::Ci { action } => ci::execute(action, &ui).await,
Commands::Hf { action } => hf::execute(action, &ui).await,
Commands::Version => {
ui.raw(format!("securegit {}", env!("CARGO_PKG_VERSION")));
Ok(())
}
Commands::Init {
path,
bare,
initial_branch,
object_format: _,
} => {
let target = path.unwrap_or_else(|| PathBuf::from("."));
ops::init::execute(&target, bare, initial_branch.as_deref(), &ui)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::Status { short } => {
if output.compact {
let timer = crate::tracking::Timer::start("status");
let result = ops::status::execute_compact(&PathBuf::from("."))?;
let normal_estimate = "x".repeat(result.len().max(1) * 4);
timer.record(&normal_estimate, &result);
println!("{}", result);
return Ok(());
}
if output.json {
ops::status::execute_json(&PathBuf::from("."))
} else {
ops::status::execute(
&PathBuf::from("."),
short || output.quiet,
output.verbose,
&ui,
)
}
}
Commands::Log {
max_count,
oneline,
author,
since,
until,
all,
} => {
if output.compact {
let timer = crate::tracking::Timer::start("log");
let result = ops::log::execute_compact(
&PathBuf::from("."),
max_count,
author.as_deref(),
since.as_deref(),
until.as_deref(),
all,
)?;
let normal_estimate = "x".repeat(result.len().max(1) * 3);
timer.record(&normal_estimate, &result);
println!("{}", result);
return Ok(());
}
ops::log::execute(
&PathBuf::from("."),
&ops::log::LogOptions {
max_count,
oneline,
author_filter: author.as_deref(),
since: since.as_deref(),
until: until.as_deref(),
all,
verbose: output.verbose,
},
&ui,
)
}
Commands::Diff {
args,
cached,
stat,
name_only,
name_status,
ignore_all_space,
} => {
let (commit_spec, paths) = parse_diff_args(&PathBuf::from("."), args);
if output.compact {
let timer = crate::tracking::Timer::start("diff");
let result = ops::diff::execute_compact(
&PathBuf::from("."),
cached,
commit_spec.as_deref(),
&paths,
ignore_all_space,
)?;
let normal_estimate = "x".repeat(result.len().max(1) * 3);
timer.record(&normal_estimate, &result);
println!("{}", result);
return Ok(());
}
ops::diff::execute(
&PathBuf::from("."),
&ops::diff::DiffDisplayOptions {
cached,
stat_only: stat || output.verbose,
name_only,
name_status,
commit_spec,
paths,
ignore_whitespace: ignore_all_space,
},
&ui,
)
}
Commands::Add {
pathspecs,
all,
update,
} => {
if !all && !update && pathspecs.is_empty() {
bail!("Nothing specified, nothing added.\nUse -A to stage all changes, or specify files.");
}
ops::staging::add(&PathBuf::from("."), &pathspecs, all, update)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::Commit {
message,
allow_empty,
amend,
ai,
} => {
if amend {
let msg = message.as_deref().unwrap_or("");
let oid = ops::commit::execute(&PathBuf::from("."), msg, allow_empty, true, &ui)?;
if output.compact {
println!("ok {}", crate::ops::utils::short_oid(&oid));
return Ok(());
}
} else if ai || message.is_none() {
match ops::ai::generate_commit_message(&PathBuf::from(".")).await {
Ok(suggested) => {
let final_msg = ops::ai::confirm_message(&suggested, &ui)?;
let oid = ops::commit::execute(
&PathBuf::from("."),
&final_msg,
allow_empty,
false,
&ui,
)?;
if output.compact {
println!("ok {}", crate::ops::utils::short_oid(&oid));
return Ok(());
}
}
Err(e) => {
if ai {
return Err(e);
}
bail!("No commit message specified (AI generation failed: {})\nUse -m to provide a message, or set SECUREGIT_AI_API_KEY for AI generation.", e);
}
}
} else {
let msg = message.ok_or_else(|| anyhow::anyhow!("No commit message specified"))?;
let oid = ops::commit::execute(&PathBuf::from("."), &msg, allow_empty, false, &ui)?;
if output.compact {
println!("ok {}", crate::ops::utils::short_oid(&oid));
return Ok(());
}
}
Ok(())
}
Commands::Push {
remote,
branch,
set_upstream,
force,
tags,
all,
token,
ssh_key,
} => {
let tok = token.map(SecureString::from);
let ssh = ssh_key.as_deref();
let push_opts = ops::push::PushOptions {
set_upstream,
force,
tags,
all,
token: tok.as_ref(),
ssh_key: ssh,
};
ops::push::execute(
&PathBuf::from("."),
&remote,
branch.as_deref(),
push_opts,
&ui,
)?;
if output.compact {
println!("ok pushed");
}
Ok(())
}
Commands::Pull {
remote,
tags,
rebase,
ff_only,
token,
ssh_key,
} => {
let tok = token.map(SecureString::from);
let ssh = ssh_key.as_deref();
ops::pull::execute(
&PathBuf::from("."),
&ops::pull::PullOptions {
remote_name: &remote,
tags,
rebase,
ff_only,
token: tok.as_ref(),
ssh_key: ssh,
},
&ui,
)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::Fetch {
remote,
tags,
prune,
token,
ssh_key,
} => {
let tok = token.map(SecureString::from);
let ssh = ssh_key.as_deref();
ops::fetch::execute_with_prune(
&PathBuf::from("."),
&remote,
tags,
prune,
tok.as_ref(),
ssh,
&ui,
)?;
if output.compact {
println!("ok fetched");
}
Ok(())
}
Commands::Merge {
branch,
abort,
no_ff,
squash,
ff_only,
} => {
if abort {
ops::merge::execute(&PathBuf::from("."), "", true, false, false, false, &ui)?;
if output.compact {
println!("ok");
}
return Ok(());
}
let branch_name =
branch.ok_or_else(|| anyhow::anyhow!("branch name required for merge"))?;
ops::merge::execute(
&PathBuf::from("."),
&branch_name,
false,
no_ff,
squash,
ff_only,
&ui,
)?;
if output.compact {
println!("ok merged");
}
Ok(())
}
Commands::Checkout {
target,
create_branch,
force,
pathspecs,
} => {
if !pathspecs.is_empty() {
ops::restore::execute(&PathBuf::from("."), &pathspecs, false, Some(&target))?;
} else {
ops::checkout::execute(&PathBuf::from("."), &target, create_branch, force, &ui)?;
}
if output.compact {
println!("ok {}", target);
}
Ok(())
}
Commands::Branch {
name,
delete,
force_delete,
all,
rename,
force_rename,
show_current,
start_point,
} => {
let path = PathBuf::from(".");
if show_current {
return ops::branch::show_current(&path);
}
if let Some(new_name) = force_rename {
let old = name.unwrap_or_default();
return ops::branch::rename(&path, &old, &new_name, true, &ui);
}
if let Some(new_name) = rename {
let old = name.unwrap_or_default();
return ops::branch::rename(&path, &old, &new_name, false, &ui);
}
if delete || force_delete {
let branch_name = name.ok_or_else(|| anyhow::anyhow!("branch name required"))?;
return ops::branch::delete(&path, &branch_name, force_delete, &ui);
}
if let Some(branch_name) = name {
return ops::branch::create(&path, &branch_name, start_point.as_deref(), &ui);
}
if output.compact {
let result = ops::branch::list_compact(&path, all)?;
println!("{}", result);
return Ok(());
}
ops::branch::list(&path, all, &ui)
}
Commands::Tag {
name,
message,
annotated,
delete,
list_pattern,
push,
sort,
target,
} => {
let path = PathBuf::from(".");
if delete {
let tag_name = name.ok_or_else(|| anyhow::anyhow!("tag name required"))?;
return ops::tag::delete(&path, &tag_name, &ui);
}
if list_pattern.is_some() || (name.is_none() && !annotated) {
return ops::tag::list(&path, sort.as_deref(), list_pattern.as_deref(), &ui);
}
if let Some(ref tag_name) = name {
let msg = if annotated && message.is_none() {
Some(format!("Tag {}", tag_name))
} else {
message.clone()
};
ops::tag::create(&path, tag_name, msg.as_deref(), target.as_deref(), &ui)?;
if push {
let push_opts = ops::push::PushOptions {
set_upstream: false,
force: false,
tags: false,
all: false,
token: None,
ssh_key: None,
};
ops::push::execute(&path, "origin", Some(tag_name), push_opts, &ui)?;
}
if output.compact {
println!("ok tag {}", tag_name);
}
return Ok(());
}
bail!("tag name required")
}
Commands::Remote { action } => {
let path = PathBuf::from(".");
match action {
Some(RemoteCommands::Add { name, url }) => {
ops::remote::add(&path, &name, &url, &ui)
}
Some(RemoteCommands::Remove { name }) => ops::remote::remove(&path, &name, &ui),
Some(RemoteCommands::SetUrl { name, url }) => {
ops::remote::set_url(&path, &name, &url, &ui)
}
None => ops::remote::list(&path, output.verbose, &ui),
}
}
Commands::Stash { action, message } => {
let path = PathBuf::from(".");
match action {
Some(StashCommands::Push { message: msg }) => {
ops::stash::save(&path, msg.as_deref(), &ui)?;
if output.compact {
println!("ok stashed");
}
Ok(())
}
Some(StashCommands::Pop { index }) => {
ops::stash::pop(&path, index, &ui)?;
if output.compact {
println!("ok stash pop");
}
Ok(())
}
Some(StashCommands::Apply { index }) => {
ops::stash::apply(&path, index, &ui)?;
if output.compact {
println!("ok stash apply");
}
Ok(())
}
Some(StashCommands::List) => {
if output.compact {
let result = ops::stash::list_compact(&path)?;
println!("{}", result);
return Ok(());
}
ops::stash::list(&path, &ui)
}
Some(StashCommands::Drop { index }) => {
ops::stash::drop_stash(&path, index, &ui)?;
if output.compact {
println!("ok stash drop");
}
Ok(())
}
None => {
ops::stash::save(&path, message.as_deref(), &ui)?;
if output.compact {
println!("ok stashed");
}
Ok(())
}
}
}
Commands::Reset {
target,
soft,
hard,
files,
} => {
let path = PathBuf::from(".");
if !files.is_empty() {
return ops::staging::reset_file(&path, &files);
}
let mode = if soft {
"soft"
} else if hard {
"hard"
} else {
"mixed"
};
ops::reset::execute(&path, &target, mode, &ui)
}
Commands::Config {
action,
global,
list,
args,
} => {
let path = PathBuf::from(".");
if let Some(subcmd) = action {
return match subcmd {
ConfigCommands::Get { key, global: g } => {
ops::config::get(&path, &key, g || global)
}
ConfigCommands::Set {
key,
value,
global: g,
} => ops::config::set(&path, &key, &value, g || global),
ConfigCommands::List { global: g } => ops::config::list(&path, g || global),
};
}
if list {
return ops::config::list(&path, global);
}
match args.len() {
0 => ops::config::list(&path, global),
1 => ops::config::get(&path, &args[0], global),
2 => ops::config::set(&path, &args[0], &args[1], global),
_ => bail!("Usage: securegit config [--global] <key> [value]"),
}
}
Commands::Show { object, stat } => {
if output.compact {
let timer = crate::tracking::Timer::start("show");
let result = ops::show::execute_compact(&PathBuf::from("."), &object)?;
let normal_estimate = "x".repeat(result.len().max(1) * 3);
timer.record(&normal_estimate, &result);
println!("{}", result);
return Ok(());
}
ops::show::execute(&PathBuf::from("."), &object, stat)
}
Commands::Restore {
pathspecs,
staged,
source,
} => {
if pathspecs.is_empty() {
bail!("you must specify path(s) to restore");
}
ops::restore::execute(&PathBuf::from("."), &pathspecs, staged, source.as_deref())
}
Commands::Switch {
target,
create,
detach,
} => ops::switch::execute(&PathBuf::from("."), &target, create, detach, &ui),
Commands::Rebase {
upstream,
abort,
continue_rebase,
skip,
onto,
} => {
ops::rebase::execute(
&PathBuf::from("."),
upstream.as_deref(),
abort,
continue_rebase,
skip,
onto.as_deref(),
&ui,
)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::CherryPick {
commit,
abort,
continue_pick,
skip,
no_commit,
} => {
let commit_ref = commit.as_deref().unwrap_or("");
if !abort && !continue_pick && !skip && commit_ref.is_empty() {
bail!("commit reference required (or use --abort / --continue / --skip)");
}
ops::cherry_pick::execute(
&PathBuf::from("."),
commit_ref,
abort,
continue_pick,
skip,
no_commit,
&ui,
)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::Revert {
commit,
abort,
continue_revert,
skip,
no_commit,
} => {
let commit_ref = commit.as_deref().unwrap_or("");
if !abort && !continue_revert && !skip && commit_ref.is_empty() {
bail!("commit reference required (or use --abort / --continue / --skip)");
}
ops::revert::execute(
&PathBuf::from("."),
commit_ref,
abort,
continue_revert,
skip,
no_commit,
&ui,
)?;
if output.compact {
println!("ok");
}
Ok(())
}
Commands::Blame { file } => ops::blame::execute(&PathBuf::from("."), &file, &ui),
Commands::Clean {
force,
directories,
dry_run,
remove_ignored,
} => ops::clean::execute(
&PathBuf::from("."),
force,
directories,
dry_run,
remove_ignored,
&ui,
),
Commands::Rm { files, cached } => {
if files.is_empty() {
bail!("No files specified for removal.");
}
ops::rm::execute(&PathBuf::from("."), &files, cached)
}
Commands::Mv {
source,
destination,
} => ops::mv::execute(&PathBuf::from("."), &source, &destination),
Commands::Undo { list, op, count } => {
ops::undo::execute(&PathBuf::from("."), list, op.as_deref(), count, &ui)
}
Commands::Snapshot { action, message } => {
let path = PathBuf::from(".");
match action {
Some(SnapshotCommands::Create { message: msg }) => {
ops::snapshot::create(&path, msg.as_deref(), &ui)?;
Ok(())
}
Some(SnapshotCommands::List { count }) => {
ops::snapshot::list(&path, count, &ui)?;
Ok(())
}
Some(SnapshotCommands::Restore { id }) => ops::snapshot::restore(&path, &id, &ui),
Some(SnapshotCommands::Prune {
older_than,
dry_run,
}) => ops::snapshot::prune(&path, &older_than, dry_run, &ui),
None => {
ops::snapshot::create(&path, message.as_deref(), &ui)?;
Ok(())
}
}
}
Commands::Conflicts => {
ops::conflicts::execute_list(&PathBuf::from("."), output.verbose, &ui)
}
Commands::Resolve { file, accept } => {
ops::conflicts::resolve_file(&PathBuf::from("."), &file, accept.as_deref(), &ui)
}
Commands::Worktree { action } => {
let path = PathBuf::from(".");
match action {
Some(WorktreeCommands::List) | None => {
if output.compact {
let result = ops::worktree::list_compact(&path)?;
println!("{}", result);
return Ok(());
}
ops::worktree::list(&path, &ui)
}
Some(WorktreeCommands::Add {
name,
path: wt_path,
branch,
}) => {
let target = wt_path
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(".worktrees").join(&name));
ops::worktree::add(&path, &name, &target, branch.as_deref(), &ui)?;
if output.compact {
println!("ok worktree {}", name);
}
Ok(())
}
Some(WorktreeCommands::Remove { name, force }) => {
ops::worktree::remove(&path, &name, force, &ui)?;
if output.compact {
println!("ok removed {}", name);
}
Ok(())
}
Some(WorktreeCommands::Lock { name, reason }) => {
ops::worktree::lock(&path, &name, reason.as_deref(), &ui)?;
if output.compact {
println!("ok locked {}", name);
}
Ok(())
}
Some(WorktreeCommands::Unlock { name }) => {
ops::worktree::unlock(&path, &name, &ui)?;
if output.compact {
println!("ok unlocked {}", name);
}
Ok(())
}
Some(WorktreeCommands::Prune { dry_run }) => {
ops::worktree::prune(&path, dry_run, &ui)?;
if output.compact {
println!("ok pruned");
}
Ok(())
}
}
}
Commands::Gain { history } => {
if history {
match crate::tracking::get_history(50) {
Ok(entries) => crate::tracking::display_history(&entries),
Err(e) => ui.error(format!("Failed to read history: {}", e)),
}
} else {
match crate::tracking::get_summary() {
Ok(summary) => crate::tracking::display_summary(&summary),
Err(e) => ui.error(format!("Failed to read statistics: {}", e)),
}
}
Ok(())
}
Commands::RevParse {
abbrev_ref,
show_toplevel,
git_dir,
is_inside_work_tree,
short,
verify,
args,
} => ops::rev_parse::execute(
&PathBuf::from("."),
&ops::rev_parse::RevParseOptions {
abbrev_ref,
show_toplevel,
git_dir,
is_inside_work_tree,
short,
verify,
args: &args,
},
),
Commands::Stack { action } => {
let path = PathBuf::from(".");
match action {
StackCommands::New { name } => ops::stack::new_stack(&path, &name, &ui),
StackCommands::Push { branch } => {
ops::stack::push_branch(&path, branch.as_deref(), &ui)
}
StackCommands::Status => ops::stack::status(&path, &ui),
StackCommands::Rebase => ops::stack::rebase_stack(&path, &ui),
StackCommands::Pop => ops::stack::pop_branch(&path, &ui),
StackCommands::Log => ops::stack::log_stack(&path, &ui),
}
}
}
}
fn parse_diff_args(
repo_path: &std::path::Path,
args: Vec<String>,
) -> (Option<String>, Vec<String>) {
if args.is_empty() {
return (None, vec![]);
}
if let Some(sep_pos) = args.iter().position(|a| a == "--") {
let commit = if sep_pos > 0 {
Some(args[0].clone())
} else {
None
};
let paths = args[sep_pos + 1..].to_vec();
return (commit, paths);
}
let first = &args[0];
if first.contains("..") {
return (Some(first.clone()), args[1..].to_vec());
}
if let Ok(repo) = git2::Repository::discover(repo_path) {
if repo.revparse_single(first).is_ok() {
return (Some(first.clone()), args[1..].to_vec());
}
}
(None, args)
}