From 1e16179e0a3d73fd6e3b1fb5f63422917d241a1b Mon Sep 17 00:00:00 2001 From: mrbeanc Date: Fri, 22 Dec 2023 21:02:22 +0800 Subject: [PATCH] =?UTF-8?q?restore:=20=E5=AE=9E=E7=8E=B0restore=5Fworktree?= =?UTF-8?q?()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.rs | 7 +++- src/commands/restore.rs | 72 ++++++++++++++++++++++++++++++++--------- src/commands/status.rs | 1 + src/commands/switch.rs | 6 ++-- src/head.rs | 1 + src/models/index.rs | 1 + src/models/tree.rs | 14 ++++---- src/store.rs | 7 ++++ src/utils/util.rs | 39 ++++++++++++++++++++-- 9 files changed, 121 insertions(+), 27 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b56f1dd..4216f5d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -147,11 +147,16 @@ pub fn handle_command() { switch(branch, create, detach); } Command::Restore { path, mut source, mut worktree, staged } => { - // 未指定stage和worktree时,默认操作stage + // 未指定stage和worktree时,默认操作worktree + // 指定 --staged 将仅还原index if !staged { worktree = true; } // 未指定source时,默认操作HEAD + /*TODO + If `--source` not specified, the contents are restored from `HEAD` if `--staged` is given, + otherwise from the [index]. + */ if source.is_none() { source = Some("HEAD".to_string()); } diff --git a/src/commands/restore.rs b/src/commands/restore.rs index 1594af7..c70e52c 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -1,49 +1,91 @@ +use std::fs; use std::path::PathBuf; +use crate::models::index::Index; +use crate::utils::util; +use crate::utils::util::get_working_dir; use crate::{ head, models::{commit::Commit, object::Hash}, store::Store, }; -/** 根据filte restore workdir */ -pub fn restore_workdir_into_files(filter: Option, target_blobs: Vec<(PathBuf, Hash)>) { - // TODO - unimplemented!("TODO"); +/** 根据filter restore workdir */ +pub fn restore_worktree(filter: Option<&Vec>, target_blobs: &Vec<(PathBuf, Hash)>) { + let paths: Vec = if let Some(filter) = filter { + filter.clone() + } else { + vec![get_working_dir().unwrap()] //None == all(workdir), '.' == cur_dir + }; + let paths = util::integrate_paths(&paths); // file paths + + let target_blobs = target_blobs // 转为相对路径(cur_dir) 与filter统一 //TODO tree改变路径表示方式后,这里需要修改 + .iter() + .map(|(path, hash)| (util::to_workdir_relative_path(path), hash.clone())) + .collect::>(); + + let index = Index::new(); + let store = Store::new(); + for (path, hash) in &target_blobs { + if path.exists() { + let file_hash = util::calc_file_hash(&path); //TODO tree没有存修改时间,所以这里只能用hash判断 + if file_hash == *hash { + continue; //文件未修改 不需要还原 + } + } + //文件不存在或已修改 + store.restore_to_file(hash, &path); + } + + //处理工作区的新文件 + for path in paths { + if target_blobs.iter().any(|(target_path, _)| target_path == &path) { + //TODO 最好返回HashMap 方便优化 + continue; //已处理 + } + //未找到,则对于target_commit来说是新文件;若已跟踪,则删除;若未跟踪,则保留 + if index.tracked(&path) { + fs::remove_file(&path).unwrap(); + } + } } /** 根据filte restore staged */ -pub fn restore_staged_into_files(filter: Option, target_blobs: Vec<(PathBuf, Hash)>) { +pub fn restore_index(filter: Option<&Vec>, target_blobs: &Vec<(PathBuf, Hash)>) { // TODO unimplemented!("TODO"); } - -pub fn restore(path: Vec, source: String, worktree: bool, staged: bool) { +/** +对于工作区中的新文件,若已跟踪,则删除;若未跟踪,则保留 + */ +pub fn restore(paths: Vec, source: String, worktree: bool, staged: bool) { // TODO - let target_commit = { + let paths = paths.iter().map(PathBuf::from).collect::>(); + let target_commit: Hash = { if source == "HEAD" { + //Default head::current_head_commit() } else if head::list_local_branches().contains(&source) { + // Branch Name, e.g. master head::get_branch_head(&source) } else { + // Commit Hash, e.g. a1b2c3d4 let store = Store::new(); let commit = store.search(&source); - if commit.is_none() { - println!("fatal: 非法的 commit: '{}'", source); + if commit.is_none() || !util::is_typeof_commit(commit.clone().unwrap()) { + println!("fatal: 非法的 commit hash: '{}'", source); return; } commit.unwrap() } }; - // TODO 处理筛选path的互相包含的情况 // 分别处理worktree和staged let tree = Commit::load(&target_commit).get_tree(); - let target_blobs = tree.get_recursive_blobs(); + let target_blobs = tree.get_recursive_blobs(); // 相对路径 if worktree { - unimplemented!("TODO") + restore_worktree(Some(&paths), &target_blobs); } if staged { - unimplemented!("TODO") + restore_index(Some(&paths), &target_blobs); } - unimplemented!("TODO"); } diff --git a/src/commands/status.rs b/src/commands/status.rs index de7e5fa..5a1e3d3 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -113,6 +113,7 @@ pub fn status() { if !staged.is_empty() { println!("Changes to be committed:"); + println!(" use \"mit restore --staged ...\" to unstage"); staged.deleted.iter().for_each(|f| { let str = format!("\tdeleted: {}", f.display()); println!("{}", str.bright_green()); diff --git a/src/commands/switch.rs b/src/commands/switch.rs index b09af49..1aa1118 100644 --- a/src/commands/switch.rs +++ b/src/commands/switch.rs @@ -11,7 +11,7 @@ use crate::{ use super::{ branch, - restore::{restore_staged_into_files, restore_workdir_into_files}, + restore::{restore_index, restore_worktree}, status::{changes_to_be_committed, changes_to_be_staged}, }; @@ -28,9 +28,9 @@ fn switch_to_commit(commit_hash: Hash) { let target_files = tree.get_recursive_blobs(); // 相对路径 // 借用逻辑类似的restore_workdir_into_files - restore_workdir_into_files(None, target_files.clone()); + restore_worktree(None, &target_files); // 同时restore index - restore_staged_into_files(None, target_files); + restore_index(None, &target_files); } fn switch_to(branch: String, detach: bool) -> Result<(), SwitchErr> { diff --git a/src/head.rs b/src/head.rs index a7f7ce9..8ca15b1 100644 --- a/src/head.rs +++ b/src/head.rs @@ -52,6 +52,7 @@ pub fn delete_branch(branch_name: &String) { /**返回当前head指向的commit hash,如果是分支,则返回分支的commit hash */ pub fn current_head_commit() -> String { + //TODO 明确返回Hash let head = current_head(); match head { Head::Branch(branch_name) => { diff --git a/src/models/index.rs b/src/models/index.rs index 0612e0e..b320f52 100644 --- a/src/models/index.rs +++ b/src/models/index.rs @@ -46,6 +46,7 @@ pub struct Index { impl Index { /// 从index文件加载 pub(crate) fn new() -> Index { + //TODO! 设计为单例模式 let mut index = Index { entries: HashMap::new(), working_dir: util::get_working_dir().unwrap(), diff --git a/src/models/tree.rs b/src/models/tree.rs index 7149804..1879c6a 100644 --- a/src/models/tree.rs +++ b/src/models/tree.rs @@ -16,7 +16,7 @@ pub struct TreeEntry { pub name: String, // file name } -/// 相对路径 +/// 相对路径(to workdir) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tree { #[serde(skip)] @@ -80,6 +80,7 @@ impl Tree { .get_tracked_files() .iter_mut() .map(|file| util::to_workdir_relative_path(file)) + //TODO! 改用相对路径(cur_dir)或者绝对路径;相对workdir的路径会在外部造成迷惑,且无法直接使用;在save&load时转换,参照index,转换可抽象为util函数 .collect(); store_path_to_tree(&file_entries, "".into()) @@ -123,17 +124,18 @@ impl Tree { files } - ///注:相对路径 + ///注:相对路径(to workdir) pub fn get_recursive_blobs(&self) -> Vec<(PathBuf, Hash)> { - let mut blob_hashs = Vec::new(); + //TODO 返回HashMap + let mut blob_hashes = Vec::new(); for entry in self.entries.iter() { if entry.filemode.0 == "blob" { - blob_hashs.push((PathBuf::from(entry.name.clone()), entry.object_hash.clone())); + blob_hashes.push((PathBuf::from(entry.name.clone()), entry.object_hash.clone())); } else { let sub_tree = Tree::load(&entry.object_hash); let sub_blobs = sub_tree.get_recursive_blobs(); - blob_hashs.append( + blob_hashes.append( sub_blobs .iter() .map(|(path, blob_hash)| (PathBuf::from(entry.name.clone()).join(path), blob_hash.clone())) @@ -142,7 +144,7 @@ impl Tree { ); } } - blob_hashs + blob_hashes } } diff --git a/src/store.rs b/src/store.rs index 9e4f260..9dbc452 100644 --- a/src/store.rs +++ b/src/store.rs @@ -4,6 +4,7 @@ use crate::models::object::Hash; use super::utils::util; +/// 管理.mit仓库的读写 pub struct Store { store_path: PathBuf, } @@ -32,6 +33,12 @@ impl Store { path.exists() } + /// 将hash对应的文件内容(主要是blob)还原到file + pub fn restore_to_file(&self, hash: &Hash, file: &PathBuf) { + let content = self.load(hash); + std::fs::write(file, content).unwrap(); + } + /** 根据前缀搜索,有歧义时返回 None */ pub fn search(&self, hash: &String) -> Option { if hash.is_empty() { diff --git a/src/utils/util.rs b/src/utils/util.rs index e242b15..2f3ec06 100644 --- a/src/utils/util.rs +++ b/src/utils/util.rs @@ -300,6 +300,23 @@ pub fn get_absolute_path(path: &Path) -> PathBuf { abs_path } } +/// 整理输入的路径数组(相对、绝对、文件、目录、甚至包括不存在),返回一个绝对路径的文件数组 +pub fn integrate_paths(paths: &Vec) -> Vec { + let mut abs_paths = Vec::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.push(path); + } + } + abs_paths.sort(); + abs_paths.dedup(); // 去重 + abs_paths +} #[derive(Debug, PartialEq)] pub enum ObjectType { @@ -309,9 +326,9 @@ pub enum ObjectType { Invalid, } pub fn check_object_type(hash: Hash) -> ObjectType { - let path = get_storage_path().unwrap().join("objects").join(hash.to_string()); + let path = get_storage_path().unwrap().join("objects").join(hash); if path.exists() { - let data = fs::read_to_string(path).unwrap(); + let data = fs::read_to_string(path).unwrap(); //TODO store::load? let result: Result = serde_json::from_str(&data); if result.is_ok() { return ObjectType::Commit; @@ -325,6 +342,11 @@ pub fn check_object_type(hash: Hash) -> ObjectType { ObjectType::Invalid } +/// 判断hash对应的文件是否是commit +pub fn is_typeof_commit(hash: Hash) -> bool { + check_object_type(hash) == ObjectType::Commit +} + #[cfg(test)] mod tests { use crate::models::{blob::Blob, index::Index}; @@ -343,6 +365,19 @@ mod tests { } } + #[test] + fn test_integrate_paths() { + let mut paths = Vec::new(); + paths.push(PathBuf::from("src/utils")); + paths.push(PathBuf::from("../test_del.txt")); + paths.push(PathBuf::from("src/utils/util.rs")); + // 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");