Files
MIT/src/utils/util.rs
2024-01-04 16:04:23 +08:00

512 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{
collections::HashSet,
fs, io,
path::{Path, PathBuf},
};
use crate::models::{commit::Commit, object::Hash, tree::Tree};
pub const ROOT_DIR: &str = ".mit";
/* tools for mit */
pub fn storage_exist() -> bool {
/*检查是否存在储存库 */
let rt = get_storage_path();
rt.is_ok()
}
pub fn check_repo_exist() {
if !storage_exist() {
println!("fatal: not a mit repository (or any of the parent directories): .mit");
panic!("不是合法的mit仓库");
}
}
// TODO 拆分为PathExt.rs并定义为trait实现PathBuf的扩展方法
/// 获取.mit目录路径
pub fn get_storage_path() -> Result<PathBuf, io::Error> {
/*递归获取储存库 */
let mut current_dir = std::env::current_dir()?;
loop {
let mut git_path = current_dir.clone();
git_path.push(ROOT_DIR);
if git_path.exists() {
return Ok(git_path);
}
if !current_dir.pop() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{:?} is not a git repository", std::env::current_dir()?),
));
}
}
}
/// 获取项目工作区目录, 也就是.mit的父目录
pub fn get_working_dir() -> Option<PathBuf> {
get_storage_path().unwrap().parent().map(|path| path.to_path_buf())
}
/// 检查文件是否在dir内(包括子文件夹) 若不存在则false
pub fn is_inside_dir(file: &Path, dir: &Path) -> bool {
if file.exists() {
let file = get_absolute_path(file);
file.starts_with(dir)
} else {
false
}
}
/// 检测dir是否是file的父目录 (不论文件是否存在) dir可以是一个文件
pub fn is_parent_dir(file: &Path, dir: &Path) -> bool {
let file = get_absolute_path(file);
let dir = get_absolute_path(dir);
file.starts_with(dir)
}
/// 从字符串角度判断path是否是parent的子路径不检测存在性) alias: [is_parent_dir]
pub fn is_sub_path(path: &Path, parent: &Path) -> bool {
is_parent_dir(path, parent)
}
/// 判断文件是否在paths中包括子目录不检查存在性
/// <br>注意如果paths为空则返回false
pub fn include_in_paths<T, U>(path: &Path, paths: U) -> bool
where
T: AsRef<Path>,
U: IntoIterator<Item = T>,
{
for p in paths {
if is_sub_path(path, p.as_ref()) {
return true;
}
}
false
}
/// 过滤列表中的元素,对.iter().filter().cloned().collect()的简化
pub fn filter<'a, I, O, T, F>(items: I, pred: F) -> O
where
T: Clone + 'a,
I: IntoIterator<Item = &'a T>,
O: FromIterator<T>,
F: Fn(&T) -> bool,
{
//items可以是一个引用
items.into_iter().filter(|item| pred(item)).cloned().collect::<O>()
}
/// 对列表中的元素应用func对.iter().map().collect()的简化
pub fn map<'a, I, O, T, F>(items: I, func: F) -> O
where
T: Clone + 'a,
I: IntoIterator<Item = &'a T>,
O: FromIterator<T>,
F: Fn(&T) -> T,
{
//items可以是一个引用
items.into_iter().map(func).collect::<O>()
}
/// 过滤列表中的元素使其在paths中包括子目录不检查存在性
pub fn filter_to_fit_paths<T, O>(items: &Vec<T>, paths: &Vec<T>) -> O
where
T: AsRef<Path> + Clone,
O: FromIterator<T> + IntoIterator<Item = T>,
{
filter::<_, O, _, _>(items, |item| include_in_paths(item.as_ref(), paths))
.into_iter()
.collect::<O>()
}
/// 检查文件是否在.mit内 若不存在则false
pub fn is_inside_repo(file: &Path) -> bool {
is_inside_dir(file, &get_storage_path().unwrap())
}
pub fn format_time(time: &std::time::SystemTime) -> String {
let datetime: chrono::DateTime<chrono::Utc> = (*time).into();
datetime.format("%Y-%m-%d %H:%M:%S.%3f").to_string()
}
/// 递归遍历给定目录及其子目录,列出所有文件,除了.mit
pub fn list_files(path: &Path) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::new();
if path.is_dir() {
if path.file_name().unwrap_or_default() == ROOT_DIR {
// 跳过 .mit 目录
return Ok(files);
}
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
// 递归遍历子目录
files.extend(list_files(&path)?);
} else {
// 将文件的路径添加到列表中
files.push(path);
}
}
}
Ok(files)
}
/** 检查一个dir是否包含.mit考虑.mit嵌套 */
pub fn include_root_dir(dir: &Path) -> bool {
// 检查子文件夹是否有ROOT_DIR
if !dir.is_dir() {
return false;
}
let path = get_absolute_path(dir);
for sub_path in fs::read_dir(path).unwrap() {
let sub_path = sub_path.unwrap().path();
if sub_path.file_name().unwrap() == ROOT_DIR {
return true;
}
}
false
}
/// 级联删除空目录,直到遇到 [工作区根目录 | 当前目录]
pub fn clear_empty_dir(dir: &Path) {
let mut dir = if dir.is_dir() {
dir.to_path_buf()
} else {
dir.parent().unwrap().to_path_buf()
};
// 不能删除工作区根目录 & 当前目录
while !include_root_dir(&dir) && !is_cur_dir(&dir) {
if is_empty_dir(&dir) {
fs::remove_dir(&dir).unwrap();
} else {
break; //一旦发现非空目录,停止级联删除
}
dir.pop();
}
}
pub fn is_empty_dir(dir: &Path) -> bool {
if !dir.is_dir() {
return false;
}
fs::read_dir(dir).unwrap().next().is_none()
}
pub fn is_cur_dir(dir: &Path) -> bool {
get_absolute_path(dir) == cur_dir()
}
pub fn cur_dir() -> PathBuf {
std::env::current_dir().unwrap() //应该是绝对路径吧
}
/// 列出工作区所有文件(包括子文件夹)
pub fn list_workdir_files() -> Vec<PathBuf> {
if let Ok(files) = list_files(&get_working_dir().unwrap()) {
files
} else {
Vec::new()
}
}
/// 获取相对于dir的 规范化 相对路径(不包含../ ./
pub fn get_relative_path_to_dir(path: &Path, dir: &Path) -> PathBuf {
// 先统一为绝对路径
let abs_path = if path.is_relative() {
get_absolute_path(path)
} else {
path.to_path_buf()
};
// 要考虑path在dir的上级目录的情况要输出../../xxx
let common_dir = get_common_dir(&abs_path, dir);
let mut rel_path = PathBuf::new();
let mut _dir = dir.to_path_buf();
while _dir != common_dir {
rel_path.push("..");
_dir.pop();
}
rel_path.join(abs_path.strip_prefix(common_dir).unwrap())
}
/// 获取两个路径的公共目录
pub fn get_common_dir(p1: &Path, p2: &Path) -> PathBuf {
let p1 = get_absolute_path(p1);
let p2 = get_absolute_path(p2);
let mut common_dir = PathBuf::new();
for (c1, c2) in p1.components().zip(p2.components()) {
if c1 == c2 {
common_dir.push(c1);
} else {
break;
}
}
common_dir
}
/// 获取相较于工作区(Working Dir)的相对路径
pub fn to_workdir_relative_path(path: &Path) -> PathBuf {
get_relative_path_to_dir(path, &get_working_dir().unwrap())
}
/// 获取相较于当前目录的 规范化 相对路径(不包含../ ./
pub fn get_relative_path(path: &Path) -> PathBuf {
get_relative_path_to_dir(path, &cur_dir())
}
/// 获取相较于工作区(Working Dir)的绝对路径
pub fn to_workdir_absolute_path(path: &Path) -> PathBuf {
get_absolute_path_to_dir(path, &get_working_dir().unwrap())
}
fn is_executable(path: &str) -> bool {
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
fs::metadata(path)
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(windows)]
{
let path = Path::new(path);
match path.extension().and_then(|s| s.to_str()) {
Some(ext) => ext.eq_ignore_ascii_case("exe") || ext.eq_ignore_ascii_case("bat"),
None => false,
}
}
}
pub fn get_file_mode(path: &Path) -> String {
// if is_executable(path.to_str().unwrap()) {
// "100755".to_string()
// } else {
// "100644".to_string()
// }
if path.is_dir() {
"40000".to_string() // 目录
} else if is_executable(path.to_str().unwrap()) {
"100755".to_string() // 可执行文件
} else {
"100644".to_string()
}
}
/// 获取绝对路径相对于目录current_dir 不论是否存在
pub fn get_absolute_path(path: &Path) -> PathBuf {
get_absolute_path_to_dir(path, &std::env::current_dir().unwrap())
}
/// 获取绝对路径相对于目录dir 不论是否存在
pub fn get_absolute_path_to_dir(path: &Path, dir: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
//相对路径
// 所以决定手动解析相对路径中的../ ./
let mut abs_path = dir.to_path_buf();
// 这里会拆分所有组件,所以会自动统一路径分隔符
for component in path.components() {
match component {
std::path::Component::ParentDir => {
if !abs_path.pop() {
panic!("relative path parse error");
}
}
std::path::Component::Normal(part) => abs_path.push(part),
std::path::Component::CurDir => {}
_ => {}
}
}
abs_path
}
}
/// 整理输入的路径数组相对、绝对、文件、目录返回一个绝对路径的文件数组只包含exist
pub fn integrate_paths(paths: &Vec<PathBuf>) -> HashSet<PathBuf> {
let mut abs_paths = HashSet::new();
for path in paths {
let path = get_absolute_path(path); // 统一转换为绝对路径
if path.is_dir() {
// 包括目录下的所有文件(子文件夹)
let files = list_files(&path).unwrap();
abs_paths.extend(files);
} else {
abs_paths.insert(path);
}
}
abs_paths
}
#[derive(Debug, PartialEq)]
pub enum ObjectType {
Blob,
Tree,
Commit,
Invalid,
}
pub fn check_object_type(hash: Hash) -> ObjectType {
let path = get_storage_path().unwrap().join("objects").join(hash);
if path.exists() {
let data = fs::read_to_string(path).unwrap(); //TODO store::load?
let result: Result<Commit, serde_json::Error> = serde_json::from_str(&data);
if result.is_ok() {
return ObjectType::Commit;
}
let result: Result<Tree, serde_json::Error> = serde_json::from_str(&data);
if result.is_ok() {
return ObjectType::Tree;
}
return ObjectType::Blob;
}
ObjectType::Invalid
}
/// 判断hash对应的文件是否是commit
pub fn is_typeof_commit(hash: Hash) -> bool {
check_object_type(hash) == ObjectType::Commit
}
/// 将内容对应的文件内容(主要是blob)还原到file
pub fn write_workfile(content: String, file: &PathBuf) {
let mut parent = file.clone();
parent.pop();
std::fs::create_dir_all(parent).unwrap();
std::fs::write(file, content).unwrap();
}
/// 从工作区读取文件内容
pub fn read_workfile(file: &Path) -> String {
std::fs::read_to_string(file).unwrap()
}
#[cfg(test)]
mod tests {
use crate::{
models::{blob::Blob, index::Index},
utils::{
test,
util::{self, *},
},
};
#[test]
fn test_get_storage_path() {
let path = get_storage_path();
match path {
Ok(path) => println!("{:?}", path),
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => println!("Not a git repository"),
_ => unreachable!("Unexpected error"),
},
}
}
#[test]
fn test_integrate_paths() {
let paths = ["src/utils", "../test_del.txt", "src/utils/util.rs"]
.iter()
.map(PathBuf::from)
.collect::<Vec<PathBuf>>();
// paths.push(PathBuf::from("."));
let abs_paths = integrate_paths(&paths);
for path in abs_paths {
println!("{}", path.display());
}
}
#[test]
fn test_get_absolute_path() {
let path = Path::new("./mit_test_storage/.././src\\main.rs");
let abs_path = get_absolute_path(path);
println!("{:?}", abs_path);
let mut cur_dir = std::env::current_dir().unwrap();
cur_dir.push("mit_test_storage");
cur_dir.pop();
cur_dir.push("src\\main.rs");
assert_eq!(abs_path, cur_dir); // 只比较组件,不比较分隔符
}
#[test]
fn test_get_relative_path() {
test::setup_with_clean_mit();
let path = Path::new("../../src\\main.rs");
let rel_path = get_relative_path_to_dir(path, &cur_dir());
println!("{:?}", rel_path);
assert_eq!(rel_path, path);
}
#[test]
fn test_to_workdir_absolute_path() {
test::setup_with_clean_mit();
let path = Path::new("./src/../main.rs");
let abs_path = to_workdir_absolute_path(path);
println!("{:?}", abs_path);
let mut cur_dir = get_working_dir().unwrap();
cur_dir.push("main.rs");
assert_eq!(abs_path, cur_dir);
}
#[test]
fn test_format_time() {
let time = std::time::SystemTime::now();
let formatted_time = format_time(&time);
println!("{:?}", time);
println!("{}", formatted_time);
}
#[test]
fn test_list_files() {
test::setup_with_clean_mit();
test::ensure_file(Path::new("test/test.txt"), None);
test::ensure_file(Path::new("a.txt"), None);
test::ensure_file(Path::new("b.txt"), None);
let files = list_files(Path::new("./"));
match files {
Ok(files) => {
for file in files {
println!("{}", file.display());
}
}
Err(err) => println!("{}", err),
}
assert_eq!(list_files(Path::new(".")).unwrap(), list_files(Path::new("./")).unwrap());
}
#[test]
fn test_check_object_type() {
test::setup_with_clean_mit();
assert_eq!(check_object_type("123".into()), ObjectType::Invalid);
test::ensure_file(Path::new("test.txt"), Some("test"));
let content = util::read_workfile(get_working_dir().unwrap().join("test.txt").as_path());
let hash = Blob::new(content).get_hash();
assert_eq!(check_object_type(hash), ObjectType::Blob);
let mut commit = Commit::new(Index::get_instance(), vec![], "test".to_string());
assert_eq!(check_object_type(commit.get_tree_hash()), ObjectType::Tree);
commit.save();
assert_eq!(check_object_type(commit.get_hash()), ObjectType::Commit);
}
#[test]
fn test_check_root_dir() {
test::setup_with_clean_mit();
list_workdir_files().iter().for_each(|f| {
fs::remove_file(f).unwrap();
});
test::list_subdir(Path::new("./")).unwrap().iter().for_each(|f| {
if include_root_dir(f) {
fs::remove_dir_all(f).unwrap();
}
});
assert!(include_root_dir(Path::new("./")));
fs::create_dir("./src").unwrap_or_default();
assert_eq!(include_root_dir(Path::new("./src")), false);
fs::create_dir("./src/.mit").unwrap_or_default();
assert!(include_root_dir(Path::new("./src")));
}
}