use async_trait::async_trait;
use crate::builtins::{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 crate::builtins::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: &super::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")]
fn valid_branch_name(name: &str) -> bool {
!name.is_empty()
&& !name.starts_with(['-', '/', '.'])
&& !name.ends_with(['/', '.'])
&& !name.ends_with(".lock")
&& !name.contains("..")
&& !name.contains("@{")
&& !name.contains("//")
&& name != "@"
&& !name
.chars()
.any(|c| c.is_control() || c.is_whitespace() || "~^:?*[\\".contains(c))
}
#[cfg(feature = "git")]
async fn git_init(
ctx: Context<'_>,
git_client: &super::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut quiet = false;
let mut branch = "master".to_string();
let mut dir: Option<&str> = None;
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
match arg {
"-q" | "--quiet" => quiet = true,
"-b" | "--initial-branch" => {
i += 1;
match args.get(i) {
Some(name) => branch = name.clone(),
None => {
return Ok(ExecResult::err(
format!("error: option `{arg}' requires a value\n"),
129,
));
}
}
}
_ if arg.starts_with("--initial-branch=") => {
branch = arg["--initial-branch=".len()..].to_string();
}
"--" => {
dir = args.get(i + 1).map(String::as_str);
break;
}
_ if arg.starts_with('-') => {
return Ok(ExecResult::err(
format!("error: unknown option `{}'\n", arg.trim_start_matches('-')),
129,
));
}
_ => {
if dir.is_some() {
return Ok(ExecResult::err(
"usage: git init [-q | --quiet] [-b <branch-name>] [<directory>]\n"
.to_string(),
129,
));
}
dir = Some(arg);
}
}
i += 1;
}
if !valid_branch_name(&branch) {
return Ok(ExecResult::err(
format!("fatal: invalid initial branch name: '{branch}'\n"),
128,
));
}
let path = match dir {
Some(d) => resolve_path(ctx.cwd, d),
None => ctx.cwd.clone(),
};
if !ctx.fs.exists(&path).await? {
ctx.fs.mkdir(&path, true).await?;
}
match git_client.init_with_branch(&ctx.fs, &path, &branch).await {
Ok(_) if quiet => Ok(ExecResult::ok(String::new())),
Ok(output) => Ok(ExecResult::ok(output)),
Err(e) => git_err(e),
}
}
#[cfg(feature = "git")]
async fn git_config(
ctx: Context<'_>,
git_client: &super::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: &super::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 mut paths: Vec<&str> = Vec::new();
let mut opts_done = false;
for arg in args {
if opts_done || !arg.starts_with('-') || arg == "-" {
paths.push(arg);
} else if arg == "--" {
opts_done = true;
} else if matches!(arg.as_str(), "-A" | "--all" | "-u" | "--update") {
paths.push(".");
}
}
if paths.is_empty() {
return Ok(ExecResult::err(
"Nothing specified, nothing added.\n".to_string(),
0,
));
}
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: &super::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut message: Option<String> = None;
let mut all = false;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--all" {
all = true;
} else if let Some(msg) = arg.strip_prefix("--message=") {
message = Some(msg.to_string());
} else if arg == "--message" {
if i + 1 < args.len() {
message = Some(args[i + 1].clone());
i += 1;
}
} else if arg.starts_with('-') && !arg.starts_with("--") && arg.len() > 1 {
for (pos, c) in arg[1..].char_indices() {
match c {
'a' => all = true,
'm' => {
let rest = &arg[1 + pos + 1..];
if !rest.is_empty() {
message = Some(rest.to_string());
} else if i + 1 < args.len() {
message = Some(args[i + 1].clone());
i += 1;
}
break;
}
_ => {}
}
}
}
i += 1;
}
if all {
let tracked = match git_client.ls_files(&ctx.fs, ctx.cwd).await {
Ok(files) => files,
Err(e) => return git_err(e),
};
let paths: Vec<&str> = tracked.iter().map(String::as_str).collect();
if !paths.is_empty()
&& let Err(e) = git_client.add(&ctx.fs, ctx.cwd, &paths).await
{
return git_err(e);
}
}
let message = match message {
Some(m) => m,
None => match editor_message(&ctx).await? {
Some(Ok(m)) => m,
Some(Err(result)) => return Ok(result),
None => {
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(all(feature = "git", feature = "terminal"))]
const COMMIT_TEMPLATE: &str = "\n# Please enter the commit message for your changes. Lines starting\n\
# with '#' will be ignored, and an empty message aborts the commit.\n";
#[cfg(all(feature = "git", feature = "terminal"))]
async fn editor_message(
ctx: &Context<'_>,
) -> Result<Option<std::result::Result<String, ExecResult>>> {
if ctx.execution_extension::<crate::terminal::Tty>().is_none() {
return Ok(None);
}
let git_dir = crate::fs::vfs_join(ctx.cwd, ".git");
if !ctx.fs.exists(&git_dir).await? {
return Ok(Some(Err(ExecResult::err(
format!("fatal: not a git repository: {}\n", ctx.cwd.display()),
128,
))));
}
let path = crate::fs::vfs_join(&git_dir, "COMMIT_EDITMSG");
ctx.fs.write_file(&path, COMMIT_TEMPLATE.as_bytes()).await?;
match crate::builtins::edit_file(ctx, &path).await? {
None => Ok(None),
Some(Err(msg)) => Ok(Some(Err(ExecResult::err(
format!("error: {msg}\nPlease supply the message using the -m option.\n"),
1,
)))),
Some(Ok(code)) if code != 0 => Ok(Some(Err(ExecResult::err(
"error: There was a problem with the editor 'vi'.\nPlease supply the message using the -m option.\n".to_string(),
1,
)))),
Some(Ok(_)) => {
let raw = ctx.fs.read_file(&path).await?;
let text = String::from_utf8_lossy(&raw);
let message = text
.lines()
.filter(|l| !l.starts_with('#'))
.collect::<Vec<_>>()
.join("\n");
let message = message.trim();
if message.is_empty() {
return Ok(Some(Err(ExecResult::err(
"Aborting commit due to empty commit message.\n".to_string(),
1,
))));
}
Ok(Some(Ok(message.to_string())))
}
}
}
#[cfg(all(feature = "git", not(feature = "terminal")))]
async fn editor_message(
_ctx: &Context<'_>,
) -> Result<Option<std::result::Result<String, ExecResult>>> {
Ok(None)
}
#[cfg(feature = "git")]
async fn git_status(ctx: Context<'_>, git_client: &super::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: &super::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: &super::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: &super::GitClient,
args: &[String],
) -> Result<ExecResult> {
let mut operands: Vec<&String> = Vec::new();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
match arg.as_str() {
"--depth" | "-b" | "--branch" | "-o" | "--origin" => i += 1,
a if a.starts_with('-') && a.len() > 1 => {}
_ => operands.push(arg),
}
i += 1;
}
if operands.is_empty() {
return Ok(ExecResult::err(
"usage: git clone <repository> [<directory>]\n".to_string(),
129,
));
}
let url = operands[0];
if let Some(src) = local_clone_source(url) {
let quiet = args.iter().any(|a| a == "-q" || a == "--quiet");
return git_clone_local(&ctx, src, operands.get(1).map(|s| s.as_str()), quiet).await;
}
let dest = if operands.len() > 1 {
resolve_path(ctx.cwd, operands[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")]
fn local_clone_source(url: &str) -> Option<&str> {
if let Some(path) = url.strip_prefix("file://") {
return Some(path);
}
if url.contains("://") {
return None;
}
let before_slash = url.split('/').next().unwrap_or(url);
(!before_slash.contains(':')).then_some(url)
}
#[cfg(feature = "git")]
async fn git_clone_local(
ctx: &Context<'_>,
src_arg: &str,
dest_arg: Option<&str>,
quiet: bool,
) -> Result<ExecResult> {
let src = resolve_path(ctx.cwd, src_arg);
let src_git = crate::fs::vfs_join(&src, ".git");
if !matches!(ctx.fs.stat(&src_git).await, Ok(m) if m.file_type.is_dir()) {
return Ok(ExecResult::err(
format!("fatal: repository '{src_arg}' does not exist\n"),
128,
));
}
let default_name;
let dest_arg = match dest_arg {
Some(d) => d,
None => {
default_name = src_arg
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or("repo")
.trim_end_matches(".git")
.to_string();
default_name.as_str()
}
};
let dest = resolve_path(ctx.cwd, dest_arg);
if dest.starts_with(&src) {
return Ok(ExecResult::err(
format!("fatal: cannot clone '{src_arg}' into itself ('{dest_arg}')\n"),
128,
));
}
if let Ok(meta) = ctx.fs.stat(&dest).await {
let empty_dir = meta.file_type.is_dir()
&& ctx
.fs
.read_dir(&dest)
.await
.map(|e| e.is_empty())
.unwrap_or(false);
if !empty_dir {
return Ok(ExecResult::err(
format!(
"fatal: destination path '{dest_arg}' already exists and is not an empty directory.\n"
),
128,
));
}
}
if let Some(parent) = dest.parent()
&& !ctx.fs.exists(parent).await?
{
ctx.fs.mkdir(parent, true).await?;
}
if let Err(msg) = crate::builtins::fileops::copy_tree(ctx, &src, &dest, false).await? {
return Ok(ExecResult::err(format!("fatal: {msg}\n"), 128));
}
let remotes = crate::fs::vfs_join(&dest, ".git/remotes");
ctx.fs
.write_file(&remotes, format!("origin|{}\n", src.display()).as_bytes())
.await?;
let mut result = ExecResult::ok(String::new());
if !quiet {
result.stderr = format!("Cloning into '{dest_arg}'...\ndone.\n").into();
}
Ok(result)
}
#[cfg(feature = "git")]
async fn git_fetch(
ctx: Context<'_>,
git_client: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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: &super::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),
}
}