From c8b9913718b953c4081eee02845558ead19b436d Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Mon, 6 Dec 2021 11:28:11 -0600 Subject: [PATCH] introducing `gstat`, a new command to get the git status (#443) * wip - preliminary checking * updated to latest pluging * i think it's all working now, except bare words * clippy --- Cargo.lock | 149 ++++++++ Cargo.toml | 1 + crates/nu_plugin_gstat/Cargo.toml | 17 + crates/nu_plugin_gstat/src/gstat.rs | 543 +++++++++++++++++++++++++++ crates/nu_plugin_gstat/src/lib.rs | 4 + crates/nu_plugin_gstat/src/main.rs | 6 + crates/nu_plugin_gstat/src/nu/mod.rs | 28 ++ 7 files changed, 748 insertions(+) create mode 100644 crates/nu_plugin_gstat/Cargo.toml create mode 100644 crates/nu_plugin_gstat/src/gstat.rs create mode 100644 crates/nu_plugin_gstat/src/lib.rs create mode 100644 crates/nu_plugin_gstat/src/main.rs create mode 100644 crates/nu_plugin_gstat/src/nu/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 849858d633..b84e4b7a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,6 +963,21 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +[[package]] +name = "git2" +version = "0.13.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845e007a28f1fcac035715988a234e8ec5458fd825b20a20c7dec74237ef341f" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glob" version = "0.3.0" @@ -1024,6 +1039,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "im" version = "15.0.0" @@ -1193,6 +1219,46 @@ version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" +[[package]] +name = "libgit2-sys" +version = "0.12.25+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68169ef08d6519b2fe133ecc637408d933c0174b23b80bb2f79828966fbaab" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1596,6 +1662,16 @@ dependencies = [ "nu-protocol", ] +[[package]] +name = "nu_plugin_gstat" +version = "0.1.0" +dependencies = [ + "git2", + "nu-engine", + "nu-plugin", + "nu-protocol", +] + [[package]] name = "nu_plugin_inc" version = "0.1.0" @@ -1753,6 +1829,25 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "1.1.1" @@ -1914,6 +2009,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" + [[package]] name = "polars" version = "0.18.0" @@ -2663,6 +2764,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "titlecase" version = "1.1.0" @@ -2736,6 +2852,12 @@ dependencies = [ "version_check 0.9.3", ] +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + [[package]] name = "unicode-linebreak" version = "0.1.2" @@ -2745,6 +2867,15 @@ dependencies = [ "regex", ] +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -2769,6 +2900,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1230ec65f13e0f9b28d789da20d2d419511893ea9dac2c1f4ef67b8b14e5da80" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "utf8-width" version = "0.1.5" @@ -2790,6 +2933,12 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 4228019abb..7d0b3cf430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/nu-protocol", "crates/nu-plugin", "crates/nu_plugin_inc", + "crates/nu_plugin_gstat", "crates/nu_plugin_example", ] diff --git a/crates/nu_plugin_gstat/Cargo.toml b/crates/nu_plugin_gstat/Cargo.toml new file mode 100644 index 0000000000..7d809ae7ef --- /dev/null +++ b/crates/nu_plugin_gstat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "A git status plugin for Nushell" +edition = "2018" +license = "MIT" +name = "nu_plugin_gstat" +version = "0.1.0" + +[lib] +doctest = false + +[dependencies] +nu-plugin = { path="../nu-plugin", version = "0.1.0" } +nu-protocol = { path="../nu-protocol", version = "0.1.0" } +nu-engine = { path="../nu-engine", version = "0.1.0" } + +git2 = "0.13.24" diff --git a/crates/nu_plugin_gstat/src/gstat.rs b/crates/nu_plugin_gstat/src/gstat.rs new file mode 100644 index 0000000000..0c38db1ac2 --- /dev/null +++ b/crates/nu_plugin_gstat/src/gstat.rs @@ -0,0 +1,543 @@ +use git2::{Branch, BranchType, Repository}; +use nu_plugin::LabeledError; +use nu_protocol::{Span, Spanned, Value}; +use std::fmt::Write; +use std::ops::BitAnd; +use std::path::PathBuf; + +// git status +// https://github.com/git/git/blob/9875c515535860450bafd1a177f64f0a478900fa/Documentation/git-status.txt + +// git status borrowed from here and tweaked +// https://github.com/glfmn/glitter/blob/master/lib/git.rs + +#[derive(Default)] +pub struct GStat; + +impl GStat { + pub fn new() -> Self { + Default::default() + } + + pub fn usage() -> &'static str { + "Usage: gstat" + } + + pub fn gstat( + &self, + value: &Value, + path: Option>, + span: &Span, + ) -> Result { + // use std::any::Any; + // eprintln!("input type: {:?} value: {:#?}", &value.type_id(), &value); + // eprintln!("path type: {:?} value: {:#?}", &path.type_id(), &path); + + // This is a flag to let us know if we're using the input value (value) + // or using the path specified (path) + let mut using_input_value = false; + + // let's get the input value as a string + let piped_value = match value.as_string() { + Ok(s) => { + using_input_value = true; + s + } + _ => String::new(), + }; + + // now let's get the path string + let mut a_path = match path { + Some(p) => { + // should we check for input and path? nah. + using_input_value = false; + p + } + None => Spanned { + item: ".".to_string(), + span: Span::unknown(), + }, + }; + + // If there was no path specified and there is a piped in value, let's use the piped in value + if a_path.item == "." && piped_value.chars().count() > 0 { + a_path.item = piped_value; + } + + // This path has to exist + if !std::path::Path::new(&a_path.item).exists() { + return Err(LabeledError { + label: "error with path".to_string(), + msg: format!("path does not exist [{}]", &a_path.item), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + + let metadata = match std::fs::metadata(&a_path.item) { + Ok(md) => md, + Err(e) => { + return Err(LabeledError { + label: "error with metadata".to_string(), + msg: format!( + "unable to get metadata for [{}], error: {}", + &a_path.item, e + ), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + }; + + // This path has to be a directory + if !metadata.is_dir() { + return Err(LabeledError { + label: "error with directory".to_string(), + msg: format!("path is not a directory [{}]", &a_path.item), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + + let repo_path = match PathBuf::from(&a_path.item).canonicalize() { + Ok(p) => p, + Err(e) => { + return Err(LabeledError { + label: format!("error canonicalizing [{}]", a_path.item), + msg: e.to_string(), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + }; + + let stats = Repository::discover(repo_path).map(|mut repo| (Stats::new(&mut repo))); + let stats = match stats { + Ok(s) => s, + Err(_) => { + // Since we really never want this to fail, lets return an empty record so + // that one can check it in a script and do something with it. + return Ok(self.create_empty_git_status(span)); + } + }; + + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("idx_added_staged".into()); + vals.push(Value::Int { + val: stats.idx_added_staged as i64, + span: *span, + }); + cols.push("idx_modified_staged".into()); + vals.push(Value::Int { + val: stats.idx_modified_staged as i64, + span: *span, + }); + cols.push("idx_deleted_staged".into()); + vals.push(Value::Int { + val: stats.idx_deleted_staged as i64, + span: *span, + }); + cols.push("idx_renamed".into()); + vals.push(Value::Int { + val: stats.idx_renamed as i64, + span: *span, + }); + cols.push("idx_type_changed".into()); + vals.push(Value::Int { + val: stats.idx_type_changed as i64, + span: *span, + }); + cols.push("wt_untracked".into()); + vals.push(Value::Int { + val: stats.wt_untracked as i64, + span: *span, + }); + cols.push("wt_modified".into()); + vals.push(Value::Int { + val: stats.wt_modified as i64, + span: *span, + }); + cols.push("wt_deleted".into()); + vals.push(Value::Int { + val: stats.wt_deleted as i64, + span: *span, + }); + cols.push("wt_type_changed".into()); + vals.push(Value::Int { + val: stats.wt_type_changed as i64, + span: *span, + }); + cols.push("wt_renamed".into()); + vals.push(Value::Int { + val: stats.wt_renamed as i64, + span: *span, + }); + cols.push("ignored".into()); + vals.push(Value::Int { + val: stats.ignored as i64, + span: *span, + }); + cols.push("conflicts".into()); + vals.push(Value::Int { + val: stats.conflicts as i64, + span: *span, + }); + cols.push("ahead".into()); + vals.push(Value::Int { + val: stats.ahead as i64, + span: *span, + }); + cols.push("behind".into()); + vals.push(Value::Int { + val: stats.behind as i64, + span: *span, + }); + cols.push("stashes".into()); + vals.push(Value::Int { + val: stats.stashes as i64, + span: *span, + }); + cols.push("branch".into()); + vals.push(Value::String { + val: stats.branch, + span: *span, + }); + cols.push("remote".into()); + vals.push(Value::String { + val: stats.remote, + span: *span, + }); + + // Leave this in case we want to turn it into a table instead of a list + // Ok(Value::List { + // vals: vec![Value::Record { + // cols, + // vals, + // span: *span, + // }], + // span: *span, + // }) + + Ok(Value::Record { + cols, + vals, + span: *span, + }) + } + + fn create_empty_git_status(&self, span: &Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("idx_added_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_modified_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_deleted_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_renamed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_type_changed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_untracked".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_modified".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_deleted".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_type_changed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_renamed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("ignored".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("conflicts".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("ahead".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("behind".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("stashes".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("branch".into()); + vals.push(Value::String { + val: "no_branch".to_string(), + span: *span, + }); + cols.push("remote".into()); + vals.push(Value::String { + val: "no_remote".to_string(), + span: *span, + }); + + Value::Record { + cols, + vals, + span: *span, + } + } +} + +/// Stats which the interpreter uses to populate the gist expression +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct Stats { + /// Number of files to be added + pub idx_added_staged: u16, + /// Number of staged changes to files + pub idx_modified_staged: u16, + /// Number of staged deletions + pub idx_deleted_staged: u16, + /// Number of renamed files + pub idx_renamed: u16, + /// Index file type change + pub idx_type_changed: u16, + + /// Number of untracked files which are new to the repository + pub wt_untracked: u16, + /// Number of modified files which have not yet been staged + pub wt_modified: u16, + /// Number of deleted files + pub wt_deleted: u16, + /// Working tree file type change + pub wt_type_changed: u16, + /// Working tree renamed + pub wt_renamed: u16, + + // Ignored files + pub ignored: u16, + /// Number of unresolved conflicts in the repository + pub conflicts: u16, + + /// Number of commits ahead of the upstream branch + pub ahead: u16, + /// Number of commits behind the upstream branch + pub behind: u16, + /// Number of stashes on the current branch + pub stashes: u16, + /// The branch name or other stats of the HEAD pointer + pub branch: String, + /// The of the upstream branch + pub remote: String, +} + +impl Stats { + /// Populate stats with the status of the given repository + pub fn new(repo: &mut Repository) -> Stats { + let mut st: Stats = Default::default(); + + st.read_branch(repo); + + let mut opts = git2::StatusOptions::new(); + + opts.include_untracked(true) + .recurse_untracked_dirs(true) + .renames_head_to_index(true); + + if let Ok(statuses) = repo.statuses(Some(&mut opts)) { + for status in statuses.iter() { + let flags = status.status(); + + if check(flags, git2::Status::INDEX_NEW) { + st.idx_added_staged += 1; + } + if check(flags, git2::Status::INDEX_MODIFIED) { + st.idx_modified_staged += 1; + } + if check(flags, git2::Status::INDEX_DELETED) { + st.idx_deleted_staged += 1; + } + if check(flags, git2::Status::INDEX_RENAMED) { + st.idx_renamed += 1; + } + if check(flags, git2::Status::INDEX_TYPECHANGE) { + st.idx_type_changed += 1; + } + + if check(flags, git2::Status::WT_NEW) { + st.wt_untracked += 1; + } + if check(flags, git2::Status::WT_MODIFIED) { + st.wt_modified += 1; + } + if check(flags, git2::Status::WT_DELETED) { + st.wt_deleted += 1; + } + if check(flags, git2::Status::WT_TYPECHANGE) { + st.wt_type_changed += 1; + } + if check(flags, git2::Status::WT_RENAMED) { + st.wt_renamed += 1; + } + + if check(flags, git2::Status::IGNORED) { + st.ignored += 1; + } + if check(flags, git2::Status::CONFLICTED) { + st.conflicts += 1; + } + } + } + + let _ = repo.stash_foreach(|_, &_, &_| { + st.stashes += 1; + true + }); + + st + } + + /// Read the branch-name of the repository + /// + /// If in detached head, grab the first few characters of the commit ID if possible, otherwise + /// simply provide HEAD as the branch name. This is to mimic the behaviour of `git status`. + fn read_branch(&mut self, repo: &Repository) { + self.branch = match repo.head() { + Ok(head) => { + if let Some(name) = head.shorthand() { + // try to use first 8 characters or so of the ID in detached HEAD + if name == "HEAD" { + if let Ok(commit) = head.peel_to_commit() { + let mut id = String::new(); + for byte in &commit.id().as_bytes()[..4] { + write!(&mut id, "{:x}", byte).unwrap(); + } + id + } else { + "HEAD".to_string() + } + // Grab the branch from the reference + } else { + let branch = name.to_string(); + // Since we have a branch name, look for the name of the upstream branch + self.read_upstream_name(repo, &branch); + branch + } + } else { + "HEAD".to_string() + } + } + Err(ref err) if err.code() == git2::ErrorCode::BareRepo => "master".to_string(), + Err(_) if repo.is_empty().unwrap_or(false) => "master".to_string(), + Err(_) => "HEAD".to_string(), + }; + } + + /// Read name of the upstream branch + fn read_upstream_name(&mut self, repo: &Repository, branch: &str) { + // First grab branch from the name + self.remote = match repo.find_branch(branch, BranchType::Local) { + Ok(branch) => { + // Grab the upstream from the branch + match branch.upstream() { + // Grab the name of the upstream if it's valid UTF-8 + Ok(upstream) => { + // While we have the upstream branch, traverse the graph and count + // ahead-behind commits. + self.read_ahead_behind(repo, &branch, &upstream); + + match upstream.name() { + Ok(Some(name)) => name.to_string(), + _ => String::new(), + } + } + _ => String::new(), + } + } + _ => String::new(), + }; + } + + /// Read ahead-behind information between the local and upstream branches + fn read_ahead_behind(&mut self, repo: &Repository, local: &Branch, upstream: &Branch) { + if let (Some(local), Some(upstream)) = (local.get().target(), upstream.get().target()) { + if let Ok((ahead, behind)) = repo.graph_ahead_behind(local, upstream) { + self.ahead = ahead as u16; + self.behind = behind as u16; + } + } + } +} + +// impl AddAssign for Stats { +// fn add_assign(&mut self, rhs: Self) { +// self.untracked += rhs.untracked; +// self.added_staged += rhs.added_staged; +// self.modified += rhs.modified; +// self.modified_staged += rhs.modified_staged; +// self.renamed += rhs.renamed; +// self.deleted += rhs.deleted; +// self.deleted_staged += rhs.deleted_staged; +// self.ahead += rhs.ahead; +// self.behind += rhs.behind; +// self.conflicts += rhs.conflicts; +// self.stashes += rhs.stashes; +// } +// } + +/// Check the bits of a flag against the value to see if they are set +#[inline] +fn check(val: B, flag: B) -> bool +where + B: BitAnd + PartialEq + Copy, +{ + val & flag == flag +} diff --git a/crates/nu_plugin_gstat/src/lib.rs b/crates/nu_plugin_gstat/src/lib.rs new file mode 100644 index 0000000000..c13f882478 --- /dev/null +++ b/crates/nu_plugin_gstat/src/lib.rs @@ -0,0 +1,4 @@ +mod gstat; +mod nu; + +pub use gstat::GStat; diff --git a/crates/nu_plugin_gstat/src/main.rs b/crates/nu_plugin_gstat/src/main.rs new file mode 100644 index 0000000000..b15aeefde5 --- /dev/null +++ b/crates/nu_plugin_gstat/src/main.rs @@ -0,0 +1,6 @@ +use nu_plugin::serve_plugin; +use nu_plugin_gstat::GStat; + +fn main() { + serve_plugin(&mut GStat::new()) +} diff --git a/crates/nu_plugin_gstat/src/nu/mod.rs b/crates/nu_plugin_gstat/src/nu/mod.rs new file mode 100644 index 0000000000..a89f9c32d8 --- /dev/null +++ b/crates/nu_plugin_gstat/src/nu/mod.rs @@ -0,0 +1,28 @@ +use crate::GStat; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; + +impl Plugin for GStat { + fn signature(&self) -> Vec { + vec![Signature::build("gstat") + .desc("Get the git status of a repo") + .optional("path", SyntaxShape::String, "path to repo")] + } + + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + if name != "gstat" { + return Ok(Value::Nothing { + span: Span::unknown(), + }); + } + + let repo_path: Option> = call.opt(0)?; + // eprintln!("input value: {:#?}", &input); + self.gstat(input, repo_path, &call.head) + } +}