use nomoreide_core::config::{ConfigStore, GitRepoDef};
use nomoreide_core::git_identity;
use nomoreide_core::git_manager::GitManager;
use crate::commands::{CliError, CliResult};
use crate::flags::{parse_flags, positional_args};
const USAGE: &str =
"Usage: nomoreide git [status|branch|switch|create-branch|fetch|diff|staged-diff|log|stage|unstage|commit]";
pub async fn run(subcommand: Option<&str>, args: &[String], store: &ConfigStore) -> CliResult {
let flags = parse_flags(args);
let cwd = match flags.nullish("cwd") {
Some(cwd) => cwd.to_string(),
None => std::env::current_dir()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default(),
};
let positional = positional_args(args);
match subcommand {
Some("status") => status(&cwd).await,
Some("add-repo") => add_repo(&positional, &flags, store).await,
Some("select-repo") => select_repo(&positional, store).await,
Some("diff") => {
println!(
"{}",
GitManager::diff(&cwd, positional.first().map(String::as_str)).await?
);
Ok(())
}
Some("staged-diff") => {
println!(
"{}",
GitManager::staged_diff(&cwd, positional.first().map(String::as_str)).await?
);
Ok(())
}
Some("log") => log(&cwd, &flags).await,
Some("branch") => branch(&cwd).await,
Some("switch") => {
let name = require(positional.first(), "branch name")?;
println!("{}", GitManager::switch_branch(&cwd, name).await?);
Ok(())
}
Some("create-branch") => {
let name = require(positional.first(), "branch name")?;
println!("{}", GitManager::create_branch(&cwd, name, None).await?);
Ok(())
}
Some("fetch") => {
println!("{}", GitManager::fetch(&cwd).await?);
Ok(())
}
Some("stage") => {
GitManager::stage(&cwd, &positional).await?;
println!("Staged {}", positional.join(", "));
Ok(())
}
Some("unstage") => {
GitManager::unstage(&cwd, &positional).await?;
println!("Unstaged {}", positional.join(", "));
Ok(())
}
Some("commit") => commit(&cwd, &flags, store).await,
_ => Err(CliError::usage(USAGE)),
}
}
async fn status(cwd: &str) -> CliResult {
let status = GitManager::status(cwd).await?;
let tracking = if status.ahead != 0 || status.behind != 0 {
let ahead = if status.ahead != 0 {
format!("↑{}", status.ahead)
} else {
String::new()
};
let behind = if status.behind != 0 {
format!("↓{}", status.behind)
} else {
String::new()
};
let upstream = status
.upstream
.as_deref()
.map(|upstream| format!(" vs {upstream}"))
.unwrap_or_default();
format!(" ({ahead}{behind}{upstream})")
} else {
String::new()
};
let branch = if status.branch.is_empty() {
"(detached)"
} else {
&status.branch
};
println!("Branch\t{branch}{tracking}");
for file in &status.files {
println!("{}{}\t{}", file.index, file.working_tree, file.path);
}
Ok(())
}
async fn add_repo(
positional: &[String],
flags: &crate::flags::Flags,
store: &ConfigStore,
) -> CliResult {
let name = require(positional.first(), "repository name")?;
let path = flags
.truthy("path")
.ok_or_else(|| CliError::usage("--path is required"))?;
store
.register_git_repository(GitRepoDef {
name: name.to_string(),
path: path.to_string(),
active_worktree_path: None,
github_credential: None,
provider_projects: None,
legacy_vercel_project_id: None,
})
.await?;
println!("Registered Git repository {name}");
Ok(())
}
async fn select_repo(positional: &[String], store: &ConfigStore) -> CliResult {
let name = require(positional.first(), "repository name")?;
store.select_git_repository(Some(name.to_string())).await?;
println!("Selected Git repository {name}");
Ok(())
}
async fn log(cwd: &str, flags: &crate::flags::Flags) -> CliResult {
let count = match flags.truthy("limit") {
Some(value) => js_number_text(nomoreide_core::js_number::parse(value)),
None => "10".to_string(),
};
for entry in GitManager::log_with_count(cwd, &count).await? {
let short: String = entry.hash.chars().take(8).collect();
println!("{short}\t{}", entry.subject);
}
Ok(())
}
async fn branch(cwd: &str) -> CliResult {
for branch in GitManager::branches(cwd).await? {
let marker = if branch.current { "*" } else { " " };
let scope = if branch.remote { "remote" } else { "local" };
println!(
"{marker}\t{}\t{scope}\t{}",
branch.name,
branch.upstream.as_deref().unwrap_or("-")
);
}
Ok(())
}
async fn commit(cwd: &str, flags: &crate::flags::Flags, store: &ConfigStore) -> CliResult {
let message = flags
.truthy("message")
.ok_or_else(|| CliError::usage("--message is required"))?;
let identity = git_identity::resolve_identity_for_cwd(store, cwd).await?;
println!(
"{}",
GitManager::commit(cwd, message, identity.selected.as_ref()).await?
);
if let Some(selected) = &identity.selected {
println!("Authored as {} <{}>", selected.name, selected.email);
}
Ok(())
}
fn js_number_text(value: f64) -> String {
if value.is_nan() {
return "NaN".to_string();
}
if value.is_infinite() {
return if value.is_sign_negative() {
"-Infinity".to_string()
} else {
"Infinity".to_string()
};
}
if value.fract() == 0.0 && value.abs() < 1e21 {
return format!("{}", value as i64);
}
format!("{value}")
}
fn require<'a>(value: Option<&'a String>, label: &str) -> Result<&'a str, CliError> {
value
.map(String::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| CliError::usage(format!("{label} is required")))
}