use std::collections::HashMap;
use std::path;
use std::result::Result;
use tokio::io::{AsyncBufReadExt, AsyncReadExt};
use tokio_stream::StreamExt;
use crate::core::error::MonorailError;
use crate::core::{file, tracking, Change};
#[derive(Debug)]
pub(crate) struct GitOptions<'a> {
pub(crate) begin: Option<&'a str>,
pub(crate) end: Option<&'a str>,
pub(crate) git_path: &'a str,
}
impl Default for GitOptions<'_> {
fn default() -> Self {
Self {
begin: None,
end: None,
git_path: "git",
}
}
}
pub(crate) async fn get_filtered_changes(
changes: Vec<Change>,
pending: &HashMap<String, String>,
work_path: &path::Path,
) -> Vec<Change> {
tokio_stream::iter(changes)
.then(|x| async {
if file::checksum_is_equal(pending, work_path, &x.name).await {
None
} else {
Some(x)
}
})
.filter_map(|x| x)
.collect()
.await
}
pub(crate) async fn get_git_diff_changes<'a>(
git_opts: &'a GitOptions<'a>,
checkpoint: &'a tracking::Checkpoint,
work_path: &path::Path,
) -> Result<Vec<Change>, MonorailError> {
let begin = git_opts.begin.or_else(|| {
if checkpoint.id.is_empty() {
None
} else {
Some(&checkpoint.id)
}
});
let end = match begin {
Some(_) => git_opts.end,
None => Some("HEAD"),
};
git_cmd_diff_changes(git_opts.git_path, work_path, begin, end).await
}
pub(crate) async fn get_git_all_changes<'a>(
git_opts: &'a GitOptions<'a>,
checkpoint: &'a tracking::Checkpoint,
work_path: &path::Path,
) -> Result<Vec<Change>, MonorailError> {
let (diff_changes, mut other_changes) = tokio::try_join!(
get_git_diff_changes(git_opts, checkpoint, work_path),
git_cmd_other_changes(git_opts.git_path, work_path)
)?;
other_changes.extend(diff_changes);
let mut filtered_changes = {
match &checkpoint.pending {
Some(pending) => {
if !pending.is_empty() {
get_filtered_changes(other_changes, pending, work_path).await
} else {
other_changes
}
}
None => other_changes,
}
};
filtered_changes.sort();
Ok(filtered_changes)
}
pub(crate) async fn get_git_cmd_child(
git_path: &str,
work_path: &path::Path,
args: &[&str],
) -> Result<tokio::process::Child, MonorailError> {
tokio::process::Command::new(git_path)
.args(args)
.current_dir(work_path)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(MonorailError::from)
}
pub(crate) async fn git_cmd_rev_parse(
git_path: &str,
work_path: &path::Path,
reference: &str,
) -> Result<String, MonorailError> {
let mut child = get_git_cmd_child(git_path, work_path, &["rev-parse", reference]).await?;
let mut stdout_string = String::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_string(&mut stdout_string).await?;
}
let mut stderr_string = String::new();
if let Some(mut stderr) = child.stderr.take() {
stderr.read_to_string(&mut stderr_string).await?;
}
let status = child.wait().await.map_err(|e| {
MonorailError::Generic(format!(
"Error resolving git reference; error: {}, reason: {}",
e, &stderr_string
))
})?;
if status.success() {
Ok(stdout_string.trim().to_string())
} else {
Err(MonorailError::Generic(format!(
"Error resolving git reference: {}",
stderr_string
)))
}
}
pub(crate) async fn git_cmd_other_changes(
git_path: &str,
work_path: &path::Path,
) -> Result<Vec<Change>, MonorailError> {
let mut child = get_git_cmd_child(
git_path,
work_path,
&["ls-files", "--others", "--exclude-standard"],
)
.await?;
let mut out = vec![];
if let Some(stdout) = child.stdout.take() {
let reader = tokio::io::BufReader::new(stdout);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
out.push(Change { name: line });
}
}
let mut stderr_string = String::new();
if let Some(mut stderr) = child.stderr.take() {
stderr.read_to_string(&mut stderr_string).await?;
}
let status = child.wait().await.map_err(|e| {
MonorailError::Generic(format!(
"Error getting git diff; error: {}, reason: {}",
e, &stderr_string
))
})?;
if status.success() {
Ok(out)
} else {
Err(MonorailError::Generic(format!(
"Error getting git diff: {}",
stderr_string
)))
}
}
pub(crate) async fn git_cmd_diff_changes(
git_path: &str,
work_path: &path::Path,
begin: Option<&str>,
end: Option<&str>,
) -> Result<Vec<Change>, MonorailError> {
let mut args = vec!["diff", "--name-only", "--find-renames"];
if let Some(begin) = begin {
args.push(begin);
}
if let Some(end) = end {
args.push(end);
}
let mut child = get_git_cmd_child(git_path, work_path, &args).await?;
let mut out = vec![];
if let Some(stdout) = child.stdout.take() {
let reader = tokio::io::BufReader::new(stdout);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
out.push(Change { name: line });
}
}
let mut stderr_string = String::new();
if let Some(mut stderr) = child.stderr.take() {
stderr.read_to_string(&mut stderr_string).await?;
}
let status = child.wait().await.map_err(|e| {
MonorailError::Generic(format!(
"Error getting git diff; error: {}, reason: {}",
e, &stderr_string
))
})?;
if status.success() {
Ok(out)
} else {
Err(MonorailError::Generic(format!(
"Error getting git diff: {}",
stderr_string
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::testing::*;
#[tokio::test]
async fn test_get_git_diff_changes_ok() -> Result<(), Box<dyn std::error::Error>> {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
let mut git_opts = GitOptions {
begin: None,
end: None,
git_path: "git",
};
commit(repo_path).await;
let begin = get_head(repo_path).await;
assert_eq!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.unwrap(),
vec![]
);
git_opts.begin = Some(&begin);
git_opts.end = Some(&begin);
assert_eq!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.unwrap(),
vec![]
);
let foo_path = &repo_path.join("foo.txt");
let _foo_checksum = write_with_checksum(foo_path, &[1]).await?;
add("foo.txt", repo_path).await;
commit(repo_path).await;
let end = get_head(repo_path).await;
git_opts.begin = Some(&begin);
git_opts.end = Some(&end);
assert_eq!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.unwrap(),
vec![Change {
name: "foo.txt".to_string()
}]
);
git_opts.begin = Some(&end);
git_opts.end = Some(&begin);
assert_eq!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.unwrap(),
vec![Change {
name: "foo.txt".to_string()
}]
);
Ok(())
}
#[tokio::test]
async fn test_get_git_diff_changes_ok_with_checkpoint() -> Result<(), Box<dyn std::error::Error>>
{
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
commit(repo_path).await;
let git_opts = GitOptions {
begin: None,
end: None,
git_path: "git",
};
assert_eq!(
get_git_diff_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: "".to_string(),
pending: None,
},
repo_path
)
.await
.unwrap(),
vec![]
);
commit(repo_path).await;
let first_head = get_head(repo_path).await;
assert_eq!(
get_git_diff_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: first_head.clone(),
pending: None,
},
repo_path
)
.await
.unwrap(),
vec![]
);
let foo_path = &repo_path.join("foo.txt");
let _ = write_with_checksum(foo_path, &[1]).await?;
add("foo.txt", repo_path).await;
commit(repo_path).await;
let second_head = get_head(repo_path).await;
assert_eq!(
get_git_diff_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: first_head.clone(),
pending: None,
},
repo_path
)
.await
.unwrap(),
vec![Change {
name: "foo.txt".to_string()
}]
);
assert_eq!(
get_git_diff_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: second_head.clone(),
pending: None,
},
repo_path
)
.await
.unwrap(),
vec![]
);
assert_eq!(
get_git_diff_changes(
&GitOptions {
begin: Some(&first_head),
end: None,
git_path: "git",
},
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: second_head.clone(),
pending: None,
},
repo_path
)
.await
.unwrap(),
vec![Change {
name: "foo.txt".to_string()
}]
);
Ok(())
}
#[tokio::test]
async fn test_get_git_diff_changes_err() -> Result<(), Box<dyn std::error::Error>> {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
let mut git_opts = GitOptions {
begin: None,
end: None,
git_path: "git",
};
commit(repo_path).await;
let begin = get_head(repo_path).await;
assert!(get_git_diff_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: "test".to_string(),
pending: Some(HashMap::from([(
"foo.txt".to_string(),
"blarp".to_string()
)])),
},
repo_path
)
.await
.is_err());
git_opts.begin = Some("foo");
assert!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.is_err()
);
git_opts.begin = Some(&begin);
git_opts.end = Some("foo");
assert!(
get_git_diff_changes(&git_opts, &Default::default(), repo_path)
.await
.is_err()
);
git_opts.begin = None;
git_opts.end = None;
Ok(())
}
#[tokio::test]
async fn test_get_git_all_changes_ok1() {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
commit(repo_path).await;
assert_eq!(
get_git_all_changes(
&GitOptions {
begin: None,
end: None,
git_path: "git",
},
&Default::default(),
repo_path
)
.await
.unwrap(),
vec![]
);
}
#[tokio::test]
async fn test_get_git_all_changes_ok2() {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
let mut git_opts = GitOptions {
begin: None,
end: None,
git_path: "git",
};
commit(repo_path).await;
let begin = get_head(repo_path).await;
git_opts.begin = Some(&begin);
assert_eq!(
get_git_all_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: begin.clone(),
pending: Some(HashMap::from([(
"foo.txt".to_string(),
"dsfksl".to_string()
)])),
},
repo_path
)
.await
.unwrap()
.len(),
0
);
}
#[tokio::test]
async fn test_get_git_all_changes_ok3() {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
let mut git_opts = GitOptions {
begin: None,
end: None,
git_path: "git",
};
commit(repo_path).await;
let begin = get_head(repo_path).await;
git_opts.begin = Some(&begin);
let foo_path = &repo_path.join("foo.txt");
let foo_checksum = write_with_checksum(foo_path, &[1]).await.unwrap();
add("foo.txt", repo_path).await;
commit(repo_path).await;
assert_eq!(
get_git_all_changes(&git_opts, &Default::default(), repo_path)
.await
.unwrap()
.len(),
1
);
assert_eq!(
get_git_all_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: begin.clone(),
pending: Some(HashMap::from([(
"foo.txt".to_string(),
foo_checksum.clone()
)])),
},
repo_path
)
.await
.unwrap()
.len(),
0
);
let end = get_head(repo_path).await;
git_opts.end = Some(&end);
let bar_path = &repo_path.join("bar.txt");
let bar_checksum = write_with_checksum(bar_path, &[2]).await.unwrap();
assert_eq!(
get_git_all_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: end.clone(),
pending: Some(HashMap::from([(
"foo.txt".to_string(),
foo_checksum.clone()
)])),
},
repo_path
)
.await
.unwrap()
.len(),
1
);
assert_eq!(
get_git_all_changes(
&git_opts,
&tracking::Checkpoint {
path: path::Path::new("x").to_path_buf(),
id: end.clone(),
pending: Some(HashMap::from([
("foo.txt".to_string(), foo_checksum),
("bar.txt".to_string(), bar_checksum)
])),
},
repo_path
)
.await
.unwrap()
.len(),
0
);
}
#[tokio::test]
async fn test_get_filtered_changes() {
let td = new_testdir().unwrap();
let repo_path = &td.path();
init(repo_path, false).await;
let root_path = &repo_path;
let fname1 = "test1.txt";
let fname2 = "test2.txt";
let change1 = Change {
name: fname1.into(),
};
let change2 = Change {
name: fname2.into(),
};
assert_eq!(
get_filtered_changes(
vec![change1.clone()],
&get_pair_map(&[(
fname1,
write_with_checksum(&root_path.join(fname1), &[1, 2, 3])
.await
.unwrap(),
)]),
repo_path
)
.await,
vec![]
);
tokio::fs::write(path::Path::new(&repo_path).join(fname1), &[1, 2, 3])
.await
.unwrap();
assert_eq!(
get_filtered_changes(
vec![change1.clone()],
&get_pair_map(&[(fname1, "foo".into(),)]),
repo_path
)
.await,
vec![change1.clone()]
);
assert_eq!(
get_filtered_changes(vec![], &HashMap::new(), repo_path).await,
vec![]
);
assert_eq!(
get_filtered_changes(
vec![change1.clone(), change2.clone()],
&HashMap::new(),
repo_path
)
.await,
vec![change1.clone(), change2.clone()]
);
assert_eq!(
get_filtered_changes(
vec![],
&get_pair_map(&[(
fname1,
write_with_checksum(&root_path.join(fname1), &[1, 2, 3])
.await
.unwrap(),
)]),
repo_path
)
.await,
vec![]
);
}
}