2022-01-07 12:44:05 +01:00
|
|
|
use git2::{Branch, BranchType, DescribeOptions, Repository};
|
2024-03-21 12:27:21 +01:00
|
|
|
use nu_protocol::{record, IntoSpanned, LabeledError, Span, Spanned, Value};
|
Add `command_prelude` module (#12291)
# Description
When implementing a `Command`, one must also import all the types
present in the function signatures for `Command`. This makes it so that
we often import the same set of types in each command implementation
file. E.g., something like this:
```rust
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData,
ShellError, Signature, Span, Type, Value,
};
```
This PR adds the `nu_engine::command_prelude` module which contains the
necessary and commonly used types to implement a `Command`:
```rust
// command_prelude.rs
pub use crate::CallExt;
pub use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned,
PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
};
```
This should reduce the boilerplate needed to implement a command and
also gives us a place to track the breadth of the `Command` API. I tried
to be conservative with what went into the prelude modules, since it
might be hard/annoying to remove items from the prelude in the future.
Let me know if something should be included or excluded.
2024-03-26 22:17:30 +01:00
|
|
|
use std::{fmt::Write, ops::BitAnd, path::Path};
|
2021-12-06 18:28:11 +01:00
|
|
|
|
|
|
|
// 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,
|
2024-03-12 12:34:32 +01:00
|
|
|
current_dir: &str,
|
2021-12-06 18:28:11 +01:00
|
|
|
path: Option<Spanned<String>>,
|
2023-07-31 21:47:46 +02:00
|
|
|
span: Span,
|
2021-12-06 18:28:11 +01:00
|
|
|
) -> Result<Value, LabeledError> {
|
|
|
|
// use std::any::Any;
|
|
|
|
// eprintln!("input type: {:?} value: {:#?}", &value.type_id(), &value);
|
|
|
|
// eprintln!("path type: {:?} value: {:#?}", &path.type_id(), &path);
|
|
|
|
|
2024-03-12 12:34:32 +01:00
|
|
|
// If the path isn't set, get it from input, and failing that, set to "."
|
|
|
|
let path = match path {
|
|
|
|
Some(path) => path,
|
|
|
|
None => {
|
|
|
|
if !value.is_nothing() {
|
|
|
|
value.coerce_string()?.into_spanned(value.span())
|
|
|
|
} else {
|
|
|
|
String::from(".").into_spanned(span)
|
|
|
|
}
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-03-12 12:34:32 +01:00
|
|
|
// Make the path absolute based on the current_dir
|
|
|
|
let absolute_path = Path::new(current_dir).join(&path.item);
|
2021-12-06 18:28:11 +01:00
|
|
|
|
|
|
|
// This path has to exist
|
2024-03-12 12:34:32 +01:00
|
|
|
if !absolute_path.exists() {
|
2024-03-21 12:27:21 +01:00
|
|
|
return Err(LabeledError::new("error with path").with_label(
|
|
|
|
format!("path does not exist [{}]", absolute_path.display()),
|
|
|
|
path.span,
|
|
|
|
));
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
2024-03-21 12:27:21 +01:00
|
|
|
let metadata = std::fs::metadata(&absolute_path).map_err(|e| {
|
|
|
|
LabeledError::new("error with metadata").with_label(
|
|
|
|
format!(
|
|
|
|
"unable to get metadata for [{}], error: {}",
|
|
|
|
absolute_path.display(),
|
|
|
|
e
|
|
|
|
),
|
|
|
|
path.span,
|
|
|
|
)
|
2023-01-24 12:23:42 +01:00
|
|
|
})?;
|
2021-12-06 18:28:11 +01:00
|
|
|
|
|
|
|
// This path has to be a directory
|
|
|
|
if !metadata.is_dir() {
|
2024-03-21 12:27:21 +01:00
|
|
|
return Err(LabeledError::new("error with directory").with_label(
|
|
|
|
format!("path is not a directory [{}]", absolute_path.display()),
|
|
|
|
path.span,
|
|
|
|
));
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
|
2024-03-12 12:34:32 +01:00
|
|
|
let repo_path = match absolute_path.canonicalize() {
|
2021-12-06 18:28:11 +01:00
|
|
|
Ok(p) => p,
|
|
|
|
Err(e) => {
|
2024-03-21 12:27:21 +01:00
|
|
|
return Err(LabeledError::new(format!(
|
|
|
|
"error canonicalizing [{}]",
|
|
|
|
absolute_path.display()
|
|
|
|
))
|
|
|
|
.with_label(e.to_string(), path.span));
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-01-07 12:44:05 +01:00
|
|
|
let (stats, repo) = if let Ok(mut repo) = Repository::discover(repo_path) {
|
|
|
|
(Stats::new(&mut repo), repo)
|
|
|
|
} else {
|
|
|
|
return Ok(self.create_empty_git_status(span));
|
|
|
|
};
|
|
|
|
|
|
|
|
let repo_name = repo
|
|
|
|
.path()
|
|
|
|
.parent()
|
|
|
|
.and_then(|p| p.file_name())
|
|
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
|
|
.unwrap_or_else(|| "".to_string());
|
|
|
|
|
|
|
|
let mut desc_opts = DescribeOptions::new();
|
|
|
|
desc_opts.describe_tags();
|
|
|
|
|
|
|
|
let tag = if let Ok(Ok(s)) = repo.describe(&desc_opts).map(|d| d.format(None)) {
|
|
|
|
s
|
|
|
|
} else {
|
|
|
|
"no_tag".to_string()
|
2021-12-06 18:28:11 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
// })
|
|
|
|
|
Create `Record` type (#10103)
# Description
This PR creates a new `Record` type to reduce duplicate code and
possibly bugs as well. (This is an edited version of #9648.)
- `Record` implements `FromIterator` and `IntoIterator` and so can be
iterated over or collected into. For example, this helps with
conversions to and from (hash)maps. (Also, no more
`cols.iter().zip(vals)`!)
- `Record` has a `push(col, val)` function to help insure that the
number of columns is equal to the number of values. I caught a few
potential bugs thanks to this (e.g. in the `ls` command).
- Finally, this PR also adds a `record!` macro that helps simplify
record creation. It is used like so:
```rust
record! {
"key1" => some_value,
"key2" => Value::string("text", span),
"key3" => Value::int(optional_int.unwrap_or(0), span),
"key4" => Value::bool(config.setting, span),
}
```
Since macros hinder formatting, etc., the right hand side values should
be relatively short and sweet like the examples above.
Where possible, prefer `record!` or `.collect()` on an iterator instead
of multiple `Record::push`s, since the first two automatically set the
record capacity and do less work overall.
# User-Facing Changes
Besides the changes in `nu-protocol` the only other breaking changes are
to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
2023-08-24 21:50:29 +02:00
|
|
|
Ok(Value::record(
|
|
|
|
record! {
|
|
|
|
"idx_added_staged" => Value::int(stats.idx_added_staged as i64, span),
|
|
|
|
"idx_modified_staged" => Value::int(stats.idx_modified_staged as i64, span),
|
|
|
|
"idx_deleted_staged" => Value::int(stats.idx_deleted_staged as i64, span),
|
|
|
|
"idx_renamed" => Value::int(stats.idx_renamed as i64, span),
|
|
|
|
"idx_type_changed" => Value::int(stats.idx_type_changed as i64, span),
|
|
|
|
"wt_untracked" => Value::int(stats.wt_untracked as i64, span),
|
|
|
|
"wt_modified" => Value::int(stats.wt_modified as i64, span),
|
|
|
|
"wt_deleted" => Value::int(stats.wt_deleted as i64, span),
|
|
|
|
"wt_type_changed" => Value::int(stats.wt_type_changed as i64, span),
|
|
|
|
"wt_renamed" => Value::int(stats.wt_renamed as i64, span),
|
|
|
|
"ignored" => Value::int(stats.ignored as i64, span),
|
|
|
|
"conflicts" => Value::int(stats.conflicts as i64, span),
|
|
|
|
"ahead" => Value::int(stats.ahead as i64, span),
|
|
|
|
"behind" => Value::int(stats.behind as i64, span),
|
|
|
|
"stashes" => Value::int(stats.stashes as i64, span),
|
|
|
|
"repo_name" => Value::string(repo_name, span),
|
|
|
|
"tag" => Value::string(tag, span),
|
|
|
|
"branch" => Value::string(stats.branch, span),
|
|
|
|
"remote" => Value::string(stats.remote, span),
|
|
|
|
},
|
|
|
|
span,
|
|
|
|
))
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
|
2023-07-31 21:47:46 +02:00
|
|
|
fn create_empty_git_status(&self, span: Span) -> Value {
|
Create `Record` type (#10103)
# Description
This PR creates a new `Record` type to reduce duplicate code and
possibly bugs as well. (This is an edited version of #9648.)
- `Record` implements `FromIterator` and `IntoIterator` and so can be
iterated over or collected into. For example, this helps with
conversions to and from (hash)maps. (Also, no more
`cols.iter().zip(vals)`!)
- `Record` has a `push(col, val)` function to help insure that the
number of columns is equal to the number of values. I caught a few
potential bugs thanks to this (e.g. in the `ls` command).
- Finally, this PR also adds a `record!` macro that helps simplify
record creation. It is used like so:
```rust
record! {
"key1" => some_value,
"key2" => Value::string("text", span),
"key3" => Value::int(optional_int.unwrap_or(0), span),
"key4" => Value::bool(config.setting, span),
}
```
Since macros hinder formatting, etc., the right hand side values should
be relatively short and sweet like the examples above.
Where possible, prefer `record!` or `.collect()` on an iterator instead
of multiple `Record::push`s, since the first two automatically set the
record capacity and do less work overall.
# User-Facing Changes
Besides the changes in `nu-protocol` the only other breaking changes are
to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
2023-08-24 21:50:29 +02:00
|
|
|
Value::record(
|
|
|
|
record! {
|
|
|
|
"idx_added_staged" => Value::int(-1, span),
|
|
|
|
"idx_modified_staged" => Value::int(-1, span),
|
|
|
|
"idx_deleted_staged" => Value::int(-1, span),
|
|
|
|
"idx_renamed" => Value::int(-1, span),
|
|
|
|
"idx_type_changed" => Value::int(-1, span),
|
|
|
|
"wt_untracked" => Value::int(-1, span),
|
|
|
|
"wt_modified" => Value::int(-1, span),
|
|
|
|
"wt_deleted" => Value::int(-1, span),
|
|
|
|
"wt_type_changed" => Value::int(-1, span),
|
|
|
|
"wt_renamed" => Value::int(-1, span),
|
|
|
|
"ignored" => Value::int(-1, span),
|
|
|
|
"conflicts" => Value::int(-1, span),
|
|
|
|
"ahead" => Value::int(-1, span),
|
|
|
|
"behind" => Value::int(-1, span),
|
|
|
|
"stashes" => Value::int(-1, span),
|
|
|
|
"repo_name" => Value::string("no_repository", span),
|
|
|
|
"tag" => Value::string("no_tag", span),
|
|
|
|
"branch" => Value::string("no_branch", span),
|
|
|
|
"remote" => Value::string("no_remote", span),
|
|
|
|
},
|
|
|
|
span,
|
|
|
|
)
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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] {
|
2023-01-30 02:37:54 +01:00
|
|
|
write!(&mut id, "{byte:x}").unwrap();
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
|
|
|
id
|
|
|
|
} else {
|
|
|
|
"HEAD".to_string()
|
|
|
|
}
|
2022-01-07 12:44:05 +01:00
|
|
|
// Grab the branch from the reference
|
2021-12-06 18:28:11 +01:00
|
|
|
} 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
|
2023-01-24 12:23:42 +01:00
|
|
|
self.remote = if let Ok(branch) = repo.find_branch(branch, BranchType::Local) {
|
|
|
|
// Grab the upstream from the branch
|
|
|
|
if let Ok(upstream) = branch.upstream() {
|
|
|
|
// While we have the upstream branch, traverse the graph and count
|
|
|
|
// ahead-behind commits.
|
|
|
|
self.read_ahead_behind(repo, &branch, &upstream);
|
|
|
|
|
|
|
|
if let Ok(Some(name)) = upstream.name() {
|
|
|
|
name.to_string()
|
|
|
|
} else {
|
|
|
|
String::new()
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
2023-01-24 12:23:42 +01:00
|
|
|
} else {
|
|
|
|
String::new()
|
2021-12-06 18:28:11 +01:00
|
|
|
}
|
2023-01-24 12:23:42 +01:00
|
|
|
} else {
|
|
|
|
String::new()
|
2021-12-06 18:28:11 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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<B>(val: B, flag: B) -> bool
|
|
|
|
where
|
|
|
|
B: BitAnd<Output = B> + PartialEq + Copy,
|
|
|
|
{
|
|
|
|
val & flag == flag
|
|
|
|
}
|