use async_trait::async_trait;
use super::{Context, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Git;
fn git_err(e: impl std::fmt::Display) -> Result<ExecResult> {
Ok(ExecResult::err(format!("{}\n", e), 128))
}
#[async_trait]
impl super::Builtin for Git {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
#[cfg(feature = "git")]
{
if let Some(git_client) = ctx.git_client {
return execute_git(ctx, git_client).await;
}
}
Ok(ExecResult::err(
"git: not configured\n\
Note: Git operations require the 'git' feature and configuration via Bash::builder().git()\n"
.to_string(),
1,
))
}
}
#[cfg(feature = "git")]
async fn execute_git(ctx: Context<'_>, git_client: &crate::git::GitClient) -> Result<ExecResult> {
if ctx.args.is_empty() {
return Ok(ExecResult::err(
"usage: git <command> [<args>]\n\n\
Available commands:\n\
\tinit Create an empty Git repository\n\
\tconfig Get and set repository options\n\
\tadd Add file contents to the index\n\
\tcommit Record changes to the repository\n\
\tstatus Show the working tree status\n\
\tlog Show commit logs\n\
\tbranch List, create, or delete branches\n\
\tcheckout Switch branches or restore files\n\
\tdiff Show changes (simplified)\n\
\treset Reset current HEAD\n\
\tremote Manage remotes\n\
\tclone Clone a repository (URL validation only)\n\
\tfetch Fetch from remote (URL validation only)\n\
\tpush Push to remote (URL validation only)\n\
\tpull Pull from remote (URL validation only)\n\
\tshow Show commit or file content\n\
\tls-files List tracked files\n\
\trev-parse Resolve refs and repo metadata\n\
\trestore Restore working tree or index files\n\
\tmerge-base Find merge base between commits\n\
\tgrep Search tracked file contents\n"
.to_string(),
1,
));
}
let subcommand = &ctx.args[0];
let subargs = &ctx.args[1..];
match subcommand.as_str() {
"init" => git_init(ctx, git_client, subargs).await,
"config" => git_config(ctx, git_client, subargs).await,
"add" => git_add(ctx, git_client, subargs).await,
"commit" => git_commit(ctx, git_client, subargs).await,
"status" => git_status(ctx, git_client).await,
"log" => git_log(ctx, git_client, subargs).await,
"remote" => git_remote(ctx, git_client, subargs).await,
"clone" => git_clone(ctx, git_client, subargs).await,
"fetch" => git_fetch(ctx, git_client, subargs).await,
"push" => git_push(ctx, git_client, subargs).await,
"pull" => git_pull(ctx, git_client, subargs).await,
"branch" => git_branch(ctx, git_client, subargs).await,
"checkout" => git_checkout(ctx, git_client, subargs).await,
"diff" => git_diff(ctx, git_client, subargs).await,
"reset" => git_reset(ctx, git_client, subargs).await,
"show" => git_show(ctx, git_client, subargs).await,
"ls-files" => git_ls_files(ctx, git_client).await,
"rev-parse" => git_rev_parse(ctx, git_client, subargs).await,
"restore" => git_restore(ctx, git_client, subargs).await,
"merge-base" => git_merge_base(ctx, git_client, subargs).await,
"grep" => git_grep(ctx, git_client, subargs).await,
_ => Ok(ExecResult::err(
format!(
"git: '{}' is not a git command. See 'git --help'.\n",
subcommand
),
1,
)),
}
}
#[cfg(feature = "git")]
async fn git_init(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let path = if args.is_empty() {
ctx.cwd.clone()
} else {
resolve_path(ctx.cwd, &args[0])
};
if !ctx.fs.exists(&path).await? {
ctx.fs.mkdir(&path, true).await?;
}
match git_client.init(&ctx.fs, &path).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_config(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut key: Option<&str> = None;
let mut value: Option<&str> = None;
let mut _global = false;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--global" || arg == "--system" {
_global = true;
} else if key.is_none() {
key = Some(arg);
} else if value.is_none() {
value = Some(arg);
}
i += 1;
}
let Some(key) = key else {
return Ok(ExecResult::err(
"usage: git config [<options>] <name> [<value>]\n".to_string(),
129,
));
};
match value {
Some(value) => {
match git_client.config_set(&ctx.fs, ctx.cwd, key, value).await {
Ok(()) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
None => {
match git_client.config_get(&ctx.fs, ctx.cwd, key).await {
Ok(Some(value)) => Ok(ExecResult::ok(format!("{}\n", value))),
Ok(None) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
}
}
#[cfg(feature = "git")]
async fn git_add(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"Nothing specified, nothing added.\n\
hint: Maybe you wanted to say 'git add .'?\n"
.to_string(),
0,
));
}
let paths: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
match git_client.add(&ctx.fs, ctx.cwd, &paths).await {
Ok(()) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_commit(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut message: Option<String> = None;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "-m" {
if i + 1 < args.len() {
message = Some(args[i + 1].clone());
i += 1;
}
} else if let Some(msg) = arg.strip_prefix("-m") {
message = Some(msg.to_string());
}
i += 1;
}
let Some(message) = message else {
return Ok(ExecResult::err(
"error: switch 'm' requires a value\n".to_string(),
128,
));
};
match git_client.commit(&ctx.fs, ctx.cwd, &message).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => Ok(ExecResult::err(format!("{}\n", e), 1)),
}
}
#[cfg(feature = "git")]
async fn git_status(ctx: Context<'_>, git_client: &crate::git::GitClient) -> Result<ExecResult> {
match git_client.status(&ctx.fs, ctx.cwd).await {
Ok(status) => {
let output = git_client.format_status(&status);
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_log(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut limit: Option<usize> = None;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "-n" {
if i + 1 < args.len() {
limit = args[i + 1].parse().ok();
i += 1;
}
} else if let Some(n) = arg.strip_prefix("-n") {
limit = n.parse().ok();
} else if arg.starts_with('-') && arg[1..].parse::<usize>().is_ok() {
limit = arg[1..].parse().ok();
}
i += 1;
}
match git_client.log(&ctx.fs, ctx.cwd, limit).await {
Ok(entries) => {
if entries.is_empty() {
return Ok(ExecResult::err(
"fatal: your current branch 'master' does not have any commits yet\n"
.to_string(),
128,
));
}
let output = git_client.format_log(&entries);
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_remote(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
match git_client.remote_list(&ctx.fs, ctx.cwd).await {
Ok(remotes) => {
let output: String = remotes.iter().map(|r| format!("{}\n", r.name)).collect();
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
} else {
let subcmd = &args[0];
let subargs = &args[1..];
match subcmd.as_str() {
"-v" | "--verbose" => {
match git_client.remote_list(&ctx.fs, ctx.cwd).await {
Ok(remotes) => {
let output: String = remotes
.iter()
.flat_map(|r| {
vec![
format!("{}\t{} (fetch)\n", r.name, r.url),
format!("{}\t{} (push)\n", r.name, r.url),
]
})
.collect();
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
}
"add" => {
if subargs.len() < 2 {
return Ok(ExecResult::err(
"usage: git remote add <name> <url>\n".to_string(),
129,
));
}
let name = &subargs[0];
let url = &subargs[1];
match git_client.remote_add(&ctx.fs, ctx.cwd, name, url).await {
Ok(()) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
"remove" | "rm" => {
if subargs.is_empty() {
return Ok(ExecResult::err(
"usage: git remote remove <name>\n".to_string(),
129,
));
}
let name = &subargs[0];
match git_client.remote_remove(&ctx.fs, ctx.cwd, name).await {
Ok(()) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
_ => Ok(ExecResult::err(
format!(
"error: Unknown subcommand: {}\n\
usage: git remote [-v | --verbose]\n\
git remote add <name> <url>\n\
git remote remove <name>\n",
subcmd
),
1,
)),
}
}
}
#[cfg(feature = "git")]
async fn git_clone(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"usage: git clone <repository> [<directory>]\n".to_string(),
129,
));
}
let url = &args[0];
let dest = if args.len() > 1 {
resolve_path(ctx.cwd, &args[1])
} else {
let name = url
.rsplit('/')
.next()
.unwrap_or("repo")
.trim_end_matches(".git");
resolve_path(ctx.cwd, name)
};
match git_client.clone(&ctx.fs, url, &dest).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_fetch(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let remote = if args.is_empty() { "origin" } else { &args[0] };
match git_client.fetch(&ctx.fs, ctx.cwd, remote).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_push(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let remote = if args.is_empty() { "origin" } else { &args[0] };
match git_client.push(&ctx.fs, ctx.cwd, remote).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_pull(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let remote = if args.is_empty() { "origin" } else { &args[0] };
match git_client.pull(&ctx.fs, ctx.cwd, remote).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_branch(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
match git_client.branch_list(&ctx.fs, ctx.cwd).await {
Ok(branches) => {
let output = git_client.format_branch_list(&branches);
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
} else if args[0] == "-d" || args[0] == "-D" {
if args.len() < 2 {
return Ok(ExecResult::err(
"error: branch name required\n".to_string(),
129,
));
}
match git_client.branch_delete(&ctx.fs, ctx.cwd, &args[1]).await {
Ok(()) => Ok(ExecResult::ok(format!("Deleted branch {}.\n", args[1]))),
Err(e) => Ok(ExecResult::err(format!("{}\n", e), 1)),
}
} else {
match git_client.branch_create(&ctx.fs, ctx.cwd, &args[0]).await {
Ok(()) => Ok(ExecResult::ok(String::new())),
Err(e) => git_err(e),
}
}
}
#[cfg(feature = "git")]
async fn git_checkout(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"error: you must specify a branch or commit to checkout\n".to_string(),
129,
));
}
if args[0] == "-b" {
if args.len() < 2 {
return Ok(ExecResult::err(
"error: switch 'b' requires a value\n".to_string(),
129,
));
}
if let Err(e) = git_client.branch_create(&ctx.fs, ctx.cwd, &args[1]).await {
return git_err(e);
}
match git_client.checkout(&ctx.fs, ctx.cwd, &args[1]).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => Ok(ExecResult::err(format!("{}\n", e), 1)),
}
} else {
match git_client.checkout(&ctx.fs, ctx.cwd, &args[0]).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => Ok(ExecResult::err(format!("{}\n", e), 1)),
}
}
}
#[cfg(feature = "git")]
async fn git_diff(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let from = args.first().map(|s| s.as_str());
let to = args.get(1).map(|s| s.as_str());
match git_client.diff(&ctx.fs, ctx.cwd, from, to).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_reset(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut mode = "--mixed"; let mut target: Option<&str> = None;
for arg in args {
if arg.starts_with("--") {
mode = arg.as_str();
} else {
target = Some(arg.as_str());
}
}
match git_client.reset(&ctx.fs, ctx.cwd, mode, target).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_show(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
let target = args.first().map(|s| s.as_str());
match git_client.show(&ctx.fs, ctx.cwd, target).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_ls_files(ctx: Context<'_>, git_client: &crate::git::GitClient) -> Result<ExecResult> {
match git_client.ls_files(&ctx.fs, ctx.cwd).await {
Ok(files) => {
let output: String = files.iter().map(|f| format!("{}\n", f)).collect();
Ok(ExecResult::ok(output))
}
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_rev_parse(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"usage: git rev-parse [<options>] [<args>...]\n".to_string(),
129,
));
}
let refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
match git_client.rev_parse(&ctx.fs, ctx.cwd, &refs).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_restore(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"usage: git restore [--staged] <pathspec>...\n".to_string(),
129,
));
}
let staged = args.iter().any(|a| a == "--staged");
let paths: Vec<&str> = args
.iter()
.filter(|a| !a.starts_with('-'))
.map(|s| s.as_str())
.collect();
if paths.is_empty() {
return Ok(ExecResult::err(
"error: you must specify path(s) to restore\n".to_string(),
128,
));
}
match git_client.restore(&ctx.fs, ctx.cwd, &paths, staged).await {
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_merge_base(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.len() < 2 {
return Ok(ExecResult::err(
"usage: git merge-base <commit> <commit>\n".to_string(),
129,
));
}
match git_client
.merge_base(&ctx.fs, ctx.cwd, &args[0], &args[1])
.await
{
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => Ok(ExecResult::err(format!("{}\n", e), 1)),
}
}
#[cfg(feature = "git")]
async fn git_grep(
ctx: Context<'_>,
git_client: &crate::git::GitClient,
args: &[String],
) -> Result<ExecResult> {
if args.is_empty() {
return Ok(ExecResult::err(
"usage: git grep <pattern> [<pathspec>...]\n".to_string(),
129,
));
}
let pattern = &args[0];
let paths: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
match git_client.grep(&ctx.fs, ctx.cwd, pattern, &paths).await {
Ok(output) => {
if output.is_empty() {
Ok(ExecResult::with_code(String::new(), 1))
} else {
Ok(ExecResult::ok(output))
}
}
Err(e) => git_err(e),
}
}