├── tests └── test.rs ├── src ├── models │ ├── object.rs │ ├── mod.rs │ ├── blob.rs │ ├── commit.rs │ ├── head.rs │ ├── tree.rs │ └── index.rs ├── utils │ ├── mod.rs │ ├── path_ext.rs │ ├── test.rs │ ├── store.rs │ └── util.rs ├── main.rs ├── commands │ ├── mod.rs │ ├── init.rs │ ├── remove.rs │ ├── commit.rs │ ├── add.rs │ ├── log.rs │ ├── merge.rs │ ├── switch.rs │ ├── branch.rs │ ├── status.rs │ └── restore.rs └── cli.rs ├── clippy.toml ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── .gitignore ├── rustfmt.toml ├── README.md └── README_en.md /tests/test.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/object.rs: -------------------------------------------------------------------------------- 1 | pub type Hash = String; 2 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod path_ext; 2 | pub use path_ext::PathExt; 3 | pub mod store; 4 | pub mod test; 5 | pub mod util; 6 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | avoid-breaking-exported-api = false 2 | 3 | # use the various `span_lint_*` methods instead, which also add a link to the docs 4 | disallowed-methods = [ 5 | ] -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blob; 2 | pub use blob::Blob; 3 | pub mod commit; 4 | pub use commit::Commit; 5 | pub mod index; 6 | pub use index::FileMetaData; 7 | pub use index::Index; 8 | pub mod object; 9 | pub use object::Hash; 10 | pub mod head; 11 | pub mod tree; 12 | 13 | pub use tree::Tree; 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::bool_assert_comparison)] // see tree&false directly 2 | #![allow(clippy::bool_comparison)] // see tree&false directly 3 | mod cli; 4 | mod commands; 5 | mod models; 6 | mod utils; 7 | 8 | fn main() { 9 | color_backtrace::install(); // colorize backtrace 10 | cli::handle_command(); 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose -- --test-threads=1 23 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub use add::add; 3 | pub mod branch; 4 | pub use branch::branch; 5 | pub mod commit; 6 | pub use commit::commit; 7 | pub mod init; 8 | pub use init::init; 9 | pub mod log; 10 | pub use log::log; 11 | pub mod merge; 12 | pub use merge::merge; 13 | pub mod remove; 14 | pub use remove::remove as rm; 15 | pub mod restore; 16 | pub use restore::restore; 17 | pub mod status; 18 | pub use status::status; 19 | pub mod switch; 20 | pub use switch::switch; 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mit" # mini_git 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | sha1 = "0.10.6" 10 | hex = "0.4.3" 11 | clap = { version = "4.4.11", features = ["derive"] } 12 | chrono = "0.4.31" 13 | serde = { version = "1.0.193", features = ["derive"] } 14 | serde_json = "1.0.108" 15 | colored = "2.1.0" 16 | rand = "0.8.5" 17 | color-backtrace = "0.6.1" 18 | once_cell = "1.19.0" 19 | backtrace = "0.3.69" 20 | flate2 = "1.0.28" 21 | base64 = "0.21.5" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | 21 | ## IntelliJ IDEA ### 22 | .idea 23 | *.iws 24 | *.iml 25 | *.ipr 26 | 27 | ## Visual Studio Code ### 28 | .vscode 29 | 30 | ## macos finder rubbish 31 | .DS_Store 32 | 33 | ## for app test 34 | mit_test_storage -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Run rustfmt with this config (it should be picked up automatically). 2 | version = "Two" 3 | use_small_heuristics = "Max" 4 | max_width = 120 5 | struct_lit_width = 60 6 | chain_width = 80 7 | single_line_if_else_max_width = 60 8 | single_line_let_else_max_width = 60 9 | merge_derives = true 10 | reorder_imports = true 11 | 12 | unstable_features = true # 使能 unstable 特性 13 | ## unstable features below ## 14 | # 格式化注释代码块:Unstable 15 | format_code_in_doc_comments = true 16 | # 重新排序mod 17 | reorder_modules = true 18 | # 按照 crate 重新排序 19 | imports_granularity = "Crate" 20 | # 过长换行使用大括号 21 | match_arm_blocks = true 22 | # 数组换行 23 | indent_style = "Block" 24 | # 彩色输出:Unstable 25 | color = "Auto" 26 | # 忽略,也是 unstable 特性 27 | ignore = ["tests"] 28 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::util::ROOT_DIR; 2 | use std::{env, fs, io}; 3 | 4 | /** 5 | 初始化mit仓库 创建.mit/objects .mit/refs/heads .mit/HEAD 6 |
并设置 .mit 为隐藏文件夹 7 |
无法重复初始化 8 | */ 9 | pub fn init() -> io::Result<()> { 10 | let dir = env::current_dir()?; 11 | let mit_dir = dir.join(ROOT_DIR); 12 | if mit_dir.exists() { 13 | println!("!Already a mit repo - [{}]", dir.display()); 14 | return Ok(()); 15 | } 16 | 17 | let dirs = [mit_dir.join("objects"), mit_dir.join("refs/heads")]; 18 | // 创建 .git 目录和子目录 19 | for dir in &dirs { 20 | fs::create_dir_all(dir)?; 21 | } 22 | fs::write(mit_dir.join("HEAD"), "ref: refs/heads/master\n")?; 23 | 24 | set_dir_hidden(mit_dir.to_str().unwrap())?; // 设置目录隐藏 (跨平台) 25 | println!("Initialized empty mit repository in {}", dir.display()); 26 | Ok(()) 27 | } 28 | 29 | #[cfg(target_os = "windows")] 30 | fn set_dir_hidden(dir: &str) -> io::Result<()> { 31 | use std::process::Command; 32 | Command::new("attrib").arg("+H").arg(dir).spawn()?.wait()?; // 等待命令执行完成 33 | Ok(()) 34 | } 35 | 36 | #[cfg(not(target_os = "windows"))] 37 | fn set_dir_hidden(dir: &str) -> io::Result<()> { 38 | //类unix系统下'.'开头就已经是隐藏文件(夹)了 39 | let _ = dir; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/remove.rs: -------------------------------------------------------------------------------- 1 | use crate::{models::Index, utils::util}; 2 | use colored::Colorize; 3 | use std::{fs, io, path::PathBuf}; 4 | 5 | /// 从暂存区&|工作区删除文件 6 | pub fn remove(files: Vec, cached: bool, recursive: bool) -> io::Result<()> { 7 | util::check_repo_exist(); 8 | let index = Index::get_instance(); 9 | for file in files.iter() { 10 | let path = PathBuf::from(file); 11 | if !path.exists() { 12 | println!("Warning: {} not exist", file.red()); 13 | continue; 14 | } 15 | if !index.contains(&path) { 16 | //不能删除未跟踪的文件 17 | println!("Warning: {} not tracked", file.red()); 18 | continue; 19 | } 20 | if path.is_dir() && !recursive { 21 | println!("fatal: not removing '{}' recursively without -r", file.bright_blue()); 22 | continue; 23 | } 24 | 25 | if path.is_dir() { 26 | let dir_files = util::list_files(&path)?; 27 | for file in dir_files.iter() { 28 | index.remove(file); 29 | } 30 | if !cached { 31 | fs::remove_dir_all(&path)?; 32 | } 33 | } else { 34 | index.remove(&path); 35 | if !cached { 36 | fs::remove_file(&path)?; 37 | } 38 | } 39 | println!("removed [{}]", file.bright_green()); 40 | } 41 | index.save(); 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/path_ext.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::util; 2 | use std::path::{Path, PathBuf}; 3 | 4 | /** 5 | Path的扩展 基于util 为了解耦,不要再util中使用PathExt 6 | */ 7 | pub trait PathExt { 8 | fn to_absolute(&self) -> PathBuf; 9 | fn to_absolute_workdir(&self) -> PathBuf; 10 | fn to_relative(&self) -> PathBuf; 11 | fn to_relative_workdir(&self) -> PathBuf; 12 | fn is_sub_to(&self, parent: &Path) -> bool; 13 | fn include_in(&self, paths: U) -> bool 14 | where 15 | T: AsRef, 16 | U: IntoIterator; 17 | } 18 | /* 19 | 在 Rust 中,当你调用一个方法时,Rust 会尝试自动解引用和自动引用(auto-deref and auto-ref)来匹配方法签名。 20 | 如果有一个为 Path 实现的方法,你可以在 PathBuf、&PathBuf、&&PathBuf 等上调用这个方法,Rust 会自动进行必要的解引用。 21 | */ 22 | impl PathExt for Path { 23 | /// 转换为绝对路径 24 | fn to_absolute(&self) -> PathBuf { 25 | util::get_absolute_path(self) 26 | } 27 | 28 | /// 转换为绝对路径(from workdir相对路径) 29 | fn to_absolute_workdir(&self) -> PathBuf { 30 | util::to_workdir_absolute_path(self) 31 | } 32 | 33 | /// 转换为相对路径(to cur_dir) 34 | fn to_relative(&self) -> PathBuf { 35 | util::get_relative_path(self) 36 | } 37 | 38 | /// 转换为相对路径(to workdir) 39 | fn to_relative_workdir(&self) -> PathBuf { 40 | util::to_workdir_relative_path(self) 41 | } 42 | 43 | /// 从字符串角度判断path是否是parent的子路径(不检测存在性) 44 | fn is_sub_to(&self, parent: &Path) -> bool { 45 | util::is_sub_path(self, parent) 46 | } 47 | 48 | /// 判断是否在paths中(包括子目录),不检查存在 49 | fn include_in(&self, paths: U) -> bool 50 | where 51 | T: AsRef, 52 | U: IntoIterator, 53 | { 54 | util::include_in_paths(self, paths) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/commit.rs: -------------------------------------------------------------------------------- 1 | use crate::models::*; 2 | 3 | use super::status; 4 | 5 | pub fn commit(message: String, allow_empty: bool) { 6 | let index = Index::get_instance(); 7 | if !allow_empty && status::changes_to_be_committed().is_empty() { 8 | panic!("工作区没有任何改动,不需要提交"); 9 | } 10 | 11 | let current_head = head::current_head(); 12 | let current_commit_hash = head::current_head_commit(); 13 | 14 | let mut commit = { 15 | if current_commit_hash.is_empty() { 16 | Commit::new(index, vec![], message.clone()) 17 | } else { 18 | Commit::new(index, vec![current_commit_hash.clone()], message.clone()) 19 | } 20 | }; 21 | let commit_hash = commit.save(); 22 | head::update_head_commit(&commit_hash); 23 | 24 | match current_head { 25 | head::Head::Branch(branch_name) => { 26 | println!("commit to [{:?}] message{:?}", branch_name, message) 27 | } 28 | head::Head::Detached(commit_hash) => { 29 | println!("Detached HEAD commit {:?} message{:?}", commit_hash[0..7].to_string(), message) 30 | } 31 | } 32 | 33 | println!("commit hash: {:?}", commit_hash); 34 | index.save(); 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | use std::path::Path; 40 | 41 | use crate::models::head; 42 | use crate::{commands as cmd, models, utils::test}; 43 | 44 | #[test] 45 | #[should_panic] 46 | fn test_commit_empty() { 47 | test::setup_with_empty_workdir(); 48 | super::commit("".to_string(), false); 49 | } 50 | 51 | #[test] 52 | fn test_commit() { 53 | test::setup_with_clean_mit(); 54 | let test_file = "a.txt"; 55 | let head_one = head::current_head_commit(); 56 | assert!(head_one.is_empty()); 57 | 58 | test::ensure_file(Path::new(test_file), "test content".into()); 59 | cmd::add(vec![], true, false); 60 | cmd::commit("test commit 1".to_string(), true); 61 | let head_two = head::current_head_commit(); 62 | assert_eq!(head_two.is_empty(), false); 63 | 64 | let commit = models::commit::Commit::load(&head_two); 65 | assert!(commit.get_parent_hash().is_empty()); 66 | assert!(commit.get_message() == "test commit 1"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | MIT: Mini-Git implementation in Rust 3 |

4 | 5 | 中文文档 | **[English](./README_en.md)** 6 | 7 | [项目链接](https://github.com/MrBeanCpp/MIT) 8 | 9 | Git in Rust. 用 `Rust` 实现的mini `Git`. Called `mit`. 10 | 11 | > 旨在简洁易读、高效且安全 12 | 13 | > 学习`Git`的最好方法就是去实现`Git` 14 | > 15 | > 本项目旨在提供一套 [小学二年级] 都能看懂的`Git`实现 16 | > 17 | > `// rm -rf 死板的设计模式 & 复杂的仓库架构` 18 | > 19 | **注意:** 更完善的`Git`实现,可以参考我们的另一个项目: 20 | [Mega-Libra](https://github.com/web3infra-foundation/mega/tree/main/libra) 21 | 22 | ## 良好的跨平台支持 23 | 24 | - [x] Windows 25 | - [x] MacOS 26 | - [x] Linux (Unix-like...) 27 | 28 | ## 主要功能 29 | 30 | - 支持的输入路径(`pathspec`):文件路径、目录路径(绝对或相对,包括`.` `./` `../`) 31 | 32 | 33 | - 支持 `mit init`, `mit add`, `mit rm`, `mit commit` 34 | 35 | - [x] `init`: 初始化(若仓库已存在,则不执行)- `idempotent` 36 | - [x] `add`: 将变更添加至暂存区(包括新建、修改、删除),可指定文件或目录 37 | - `-A(all)` : 暂存工作区中的所有文件(从根目录开始)变更(新建√ 修改√ 删除√) 38 | - `-u(update)`: 仅对暂存区[`index`]中已跟踪的文件进行操作(新建× 修改√ 删除√) 39 | - [x] `rm`: 将文件从暂存区 &| 工作区移除. 40 | - `--cached` : 仅从暂存区移除,取消跟踪 41 | - `-r(recursive)`: 递归删除目录,删除目录时必须指定该参数 42 | - [x] `commit` 43 | - [x] `status`: 显示工作区、暂存区、`HEAD` 的状态,(只包含当前目录);分为三部分: 44 | - **Staged to be committed:** 暂存区与`HEAD`(最后一次`Commit::Tree`)比较,即上次的暂存区 45 | - **Unstaged:** 暂存区与工作区比较,未暂存的工作区变更 46 | - **Untracked:** 暂存区与工作区比较,从未暂存过的文件(即未跟踪的文件) 47 | - [x] `log` 48 | 49 | - 支持分支 `mit branch`, `mit switch`, `mit restore` 50 | 51 | - [x] `branch` 52 | - [x] `switch` 53 | 与 `checkout` 不同,`switch` 需要指明`--detach`,才能切换到一个`commit`,否则只能切换分支。 54 | 同时为里简化实现,有任何未提交的修改,都不能切换分支。 55 | - [x] `restore`: 回滚文件 56 | - 将指定路径(可包含目录)的文件恢复到`--source` 指定的版本,可指定操作暂存区 &| 工作区 57 | - `--source`:可指定`Commit Hash` `HEAD` `Branch Name` 58 | - 若不指定`--source`,且无`--staged`,则恢复到`HEAD`版本,否则从暂存区[`index`]恢复 59 | - 若`--staged`和`--worktree`均未指定,则默认恢复到`--worktree` 60 | - 对于`--source`中不存在的文件,若已跟踪,则删除;否则忽略 61 | 62 | - 支持简单的合并 `mit merge` (fast-forward) 63 | - 64 | - [x] Merge(FF) 65 | 66 | ## 备注 67 | 68 | ### ⚠️测试需要单线程 69 | 70 | ⚠️注意:为了避免冲突,执行测试时请加上`--test-threads=1` 71 | 72 | 如:`cargo test -- --test-threads=1` 73 | 74 | 因为测试需要对同一个文件夹进行IO 75 | 76 | ### 名词释义 77 | 78 | - 暂存区:`index` or `stage`,保存下一次`commit`需要的的文件快照 79 | - 工作区:`worktree`,用户直接操作的文件夹 80 | - 工作目录:`working directory` or `repository`,代码仓库的根目录,即`.mit`所在的目录 81 | - `HEAD`:指向当前`commit`的指针 82 | - 已跟踪:`tracked`,指已经在暂存区[`index`]中的文件(即曾经`add`过的文件) 83 | 84 | ### 介绍视频 85 | 86 | [【Mit】Rust实现的迷你Git - 系统软件开发实践 结课报告_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1p64y1E78W/) 87 | -------------------------------------------------------------------------------- /src/models/blob.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | use flate2::{read::GzDecoder, write::GzEncoder, Compression}; 3 | use std::io::{Read, Write}; 4 | 5 | use crate::{models::Hash, utils::store}; 6 | 7 | /**Blob
8 | git中最基本的对象,他储存一份文件的内容,并使用hash作为标识符。 9 | */ 10 | #[derive(Debug, Clone)] 11 | pub struct Blob { 12 | hash: Hash, 13 | data: String, 14 | } 15 | 16 | impl Blob { 17 | /// 从源文件新建blob对象,并直接保存到/objects/中 18 | pub fn new(data: String) -> Blob { 19 | let mut blob = Blob { hash: "".to_string(), data }; 20 | blob.save(); 21 | blob 22 | } 23 | 24 | /// 从源文件新建blob对象,但不保存到/objects/中 25 | pub fn dry_new(data: String) -> Blob { 26 | let mut blob = Blob { hash: "".to_string(), data }; 27 | let s = store::Store::new(); 28 | let hash: String = s.dry_save(&Blob::encode(blob.data.clone())); 29 | blob.hash = hash; 30 | blob 31 | } 32 | 33 | fn encode(data: String) -> String { 34 | let mut cmopress_encoder = GzEncoder::new(Vec::new(), Compression::default()); 35 | cmopress_encoder.write_all(data.as_bytes()).unwrap(); 36 | let compressed_data = cmopress_encoder.finish().unwrap(); 37 | base64::engine::general_purpose::STANDARD_NO_PAD.encode(compressed_data) 38 | } 39 | fn decode(encoded: String) -> String { 40 | let compressed_data = base64::engine::general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap(); 41 | let mut decompress_decoder = GzDecoder::new(&compressed_data[..]); 42 | let mut data = String::new(); 43 | decompress_decoder.read_to_string(&mut data).unwrap(); 44 | data 45 | } 46 | 47 | pub fn load(hash: &String) -> Blob { 48 | let s = store::Store::new(); 49 | let encoded_data = s.load(hash); 50 | let data = Blob::decode(encoded_data); 51 | Blob { hash: hash.clone(), data } 52 | } 53 | 54 | /// 写入文件 55 | pub fn save(&mut self) -> Hash { 56 | let s = store::Store::new(); 57 | let hash: String = s.save(&Blob::encode(self.data.clone())); 58 | self.hash = hash; 59 | self.hash.clone() 60 | } 61 | 62 | pub fn get_hash(&self) -> String { 63 | self.hash.clone() 64 | } 65 | 66 | pub fn get_content(&self) -> String { 67 | self.data.clone() 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod test { 73 | use crate::utils::test; 74 | 75 | #[test] 76 | fn test_save_and_load() { 77 | test::setup_with_clean_mit(); 78 | let test_data = "hello world"; 79 | let blob = super::Blob::new(test_data.into()); 80 | 81 | let blob2 = super::Blob::load(&blob.hash); 82 | assert_eq!(blob2.get_hash(), blob.get_hash()); 83 | assert_eq!(blob2.data, test_data); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/add.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use colored::Colorize; 4 | 5 | use crate::commands::status; 6 | use crate::models::index::FileMetaData; 7 | use crate::models::*; 8 | use crate::utils::path_ext::PathExt; 9 | use crate::utils::util; 10 | 11 | /// add是对index的操作,不会对工作区产生影响 12 | pub fn add(raw_paths: Vec, all: bool, mut update: bool) { 13 | util::check_repo_exist(); 14 | 15 | let mut paths: Vec = raw_paths.into_iter().map(PathBuf::from).collect(); 16 | if all || update { 17 | println!("{}", "--all || --update 对工作区所有文件进行操作".bright_green()); 18 | paths.push(util::get_working_dir().unwrap()); 19 | } 20 | if all { 21 | update = false; // all 优先级最高 22 | } 23 | 24 | //待暂存的更改: index vs worktree 25 | let changes = status::changes_to_be_staged().filter_relative(&paths); //对paths过滤 26 | let mut files = changes.modified; 27 | //统合所有更改到files再一起处理,其实也可以直接根据changes的分类进行处理 主要是为了错误处理 而且思想上更?简单? 28 | files.extend(changes.deleted); 29 | if !update { 30 | files.extend(changes.new); 31 | } else { 32 | println!("{}", "--update 只对已跟踪文件进行操作 不包含new".bright_green()); 33 | } 34 | 35 | let index = Index::get_instance(); 36 | for file in &files { 37 | add_a_file(file, index); 38 | } 39 | index.save(); 40 | } 41 | 42 | fn add_a_file(file: &Path, index: &mut Index) { 43 | let workdir = util::get_working_dir().unwrap(); 44 | if !file.is_sub_to(&workdir) { 45 | //文件不在工作区内 46 | println!("fatal: '{}' is outside workdir at '{}'", file.display(), workdir.display()); 47 | return; 48 | } 49 | if util::is_inside_repo(file) { 50 | //文件在.mit内 51 | println!("fatal: '{}' is inside '{}' repo", file.display(), util::ROOT_DIR); 52 | return; 53 | } 54 | 55 | let rel_path = file.to_relative(); 56 | if !file.exists() { 57 | //文件被删除 58 | index.remove(file); 59 | println!("removed: {}", rel_path.display()); 60 | } else { 61 | //文件存在 62 | if !index.contains(file) { 63 | //文件未被跟踪 64 | let blob = Blob::new(util::read_workfile(file)); 65 | index.add(file.to_path_buf(), FileMetaData::new(&blob, file)); 66 | println!("add(stage): {}", rel_path.display()); 67 | } else { 68 | //文件已被跟踪,可能被修改 69 | if index.is_modified(file) { 70 | //文件被修改,但不一定内容更改 71 | let blob = Blob::new(util::read_workfile(file)); //到这一步才创建blob是为了优化 72 | if !index.verify_hash(file, &blob.get_hash()) { 73 | //比较hash 确认内容更改 74 | index.update(file.to_path_buf(), FileMetaData::new(&blob, file)); 75 | println!("add(modified): {}", rel_path.display()); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/log.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{head, Commit}; 2 | use colored::Colorize; 3 | 4 | const DEFAULT_LOG_NUMBER: usize = 10; 5 | 6 | pub fn log(all: bool, number: Option) { 7 | println!("log all: {:?}, number: {:?}", all, number); 8 | let _ = __log(all, number); 9 | } 10 | 11 | fn __log(all: bool, number: Option) -> usize { 12 | let mut log_count = 0usize; 13 | 14 | let head = head::current_head(); 15 | let mut branch_name: Option = None; 16 | let mut head_commit = match head { 17 | head::Head::Branch(_branch_name) => { 18 | let commit = head::get_branch_head(&_branch_name); 19 | branch_name = Some(_branch_name.clone()); 20 | if commit.is_empty() { 21 | println!("当前分支{:?}没有任何提交", _branch_name); 22 | return 0; 23 | } 24 | commit 25 | } 26 | head::Head::Detached(commit_hash) => commit_hash, 27 | }; 28 | 29 | let mut number = match number { 30 | Some(number) => number, 31 | None => DEFAULT_LOG_NUMBER, 32 | }; 33 | 34 | let mut first = true; 35 | loop { 36 | log_count += 1; 37 | let commit = Commit::load(&head_commit); 38 | if first { 39 | // TODO: (HEAD -> ttt, ad2) 40 | first = false; 41 | print!("{}{}{}{}", "commit ".yellow(), commit.get_hash().yellow(), "(".yellow(), "HEAD".blue()); 42 | if let Some(ref branch_name) = branch_name { 43 | print!("{}", format!(" -> {}", branch_name).blue()); 44 | } 45 | println!("{}", ")".yellow()); 46 | } else { 47 | println!("{}{}{}{}{}", "commit ".yellow(), head_commit.yellow(), "(".yellow(), "HEAD".blue(), ")".yellow()); 48 | } 49 | println!("Author: {}", commit.get_author()); 50 | println!("Date: {}", commit.get_date()); 51 | println!(); 52 | println!(" {}", commit.get_message()); 53 | println!(); 54 | 55 | if all == false { 56 | if number > 1 { 57 | number -= 1; 58 | } else { 59 | break; 60 | } 61 | } 62 | if commit.get_parent_hash().is_empty() { 63 | break; 64 | } 65 | head_commit = commit.get_parent_hash().first().unwrap().clone(); 66 | } 67 | log_count 68 | } 69 | 70 | #[cfg(test)] 71 | mod test { 72 | use super::super::super::commands; 73 | use crate::utils::test; 74 | #[test] 75 | fn test_log() { 76 | test::setup_with_clean_mit(); 77 | assert_eq!(super::__log(false, None), 0); 78 | commands::commit::commit("test commit 2".into(), true); 79 | assert_eq!(super::__log(false, Some(1)), 1); 80 | commands::commit::commit("test commit 3".into(), true); 81 | assert_eq!(super::__log(false, None), 2); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/models/commit.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::utils::{store, util}; 6 | 7 | use super::*; 8 | /*Commit 9 | * git中版本控制的单位。 10 | * 一份Commit中对应一份版Tree,记录了该版本所包含的文件;parent记录本次commit的来源,形成了版本树; 11 | * 此外,Commit中还包含了作者、提交者、提交信息等。 12 | */ 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | pub struct Commit { 15 | #[serde(skip)] 16 | hash: Hash, 17 | date: SystemTime, 18 | author: String, // unimplemented ignore 19 | committer: String, // unimplemented ignore 20 | message: String, 21 | parent: Vec, // parents commit hash 22 | tree: String, // tree hash 23 | } 24 | 25 | impl Commit { 26 | pub fn get_hash(&self) -> String { 27 | self.hash.clone() 28 | } 29 | pub fn get_date(&self) -> String { 30 | util::format_time(&self.date) 31 | } 32 | #[cfg(test)] 33 | pub fn get_tree_hash(&self) -> String { 34 | self.tree.clone() 35 | } 36 | pub fn get_tree(&self) -> Tree { 37 | Tree::load(&self.tree) 38 | } 39 | pub fn get_parent_hash(&self) -> Vec { 40 | self.parent.clone() 41 | } 42 | pub fn get_message(&self) -> String { 43 | self.message.clone() 44 | } 45 | pub fn get_author(&self) -> String { 46 | self.author.clone() 47 | } 48 | // pub fn get_committer(&self) -> String { 49 | // self.committer.clone() 50 | // } 51 | 52 | pub fn new(index: &Index, parent: Vec, message: String) -> Commit { 53 | let mut tree = Tree::new(index); 54 | let tree_hash = tree.save(); 55 | Commit { 56 | hash: "".to_string(), 57 | date: SystemTime::now(), 58 | author: "mit".to_string(), 59 | committer: "mit-author".to_string(), 60 | message, 61 | parent, 62 | tree: tree_hash, 63 | } 64 | } 65 | 66 | pub fn load(hash: &String) -> Commit { 67 | let s = store::Store::new(); 68 | let commit_data = s.load(hash); 69 | let mut commit: Commit = serde_json::from_str(&commit_data).unwrap(); 70 | commit.hash = hash.clone(); 71 | commit 72 | } 73 | 74 | pub fn save(&mut self) -> String { 75 | // unimplemented!() 76 | let s = store::Store::new(); 77 | let commit_data = serde_json::to_string_pretty(&self).unwrap(); 78 | let hash = s.save(&commit_data); 79 | self.hash = hash.clone(); 80 | hash 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod test { 86 | use crate::utils::test; 87 | 88 | #[test] 89 | fn test_commit() { 90 | test::setup_with_clean_mit(); 91 | 92 | let index = super::Index::get_instance(); 93 | let mut commit = super::Commit::new(index, vec!["123".to_string(), "456".to_string()], "test".to_string()); 94 | assert_eq!(commit.hash.len(), 0); 95 | 96 | let hash = commit.save(); 97 | assert_eq!(commit.hash, hash, "commit hash not equal"); 98 | 99 | let commit = super::Commit::load(&hash); 100 | assert_eq!(commit.hash, hash); 101 | assert_ne!(commit.hash.len(), 0); 102 | assert_eq!(commit.parent.len(), 2); 103 | println!("{:?}", commit) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/utils/test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | pub const TEST_DIR: &str = "mit_test_storage"; 4 | use std::{ 5 | fs, 6 | io::{self, Write}, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use crate::models::Index; 11 | use crate::utils::PathExt; 12 | 13 | // 执行测试的储存库 14 | use super::util; 15 | /* tools for test */ 16 | fn find_cargo_dir() -> PathBuf { 17 | let cargo_path = std::env::var("CARGO_MANIFEST_DIR"); 18 | match cargo_path { 19 | Ok(path) => PathBuf::from(path), 20 | Err(_) => { 21 | // vscode DEBUG test没有CARGO_MANIFEST_DIR宏,手动尝试查找cargo.toml 22 | let mut path = util::cur_dir(); 23 | loop { 24 | path.push("Cargo.toml"); 25 | if path.exists() { 26 | break; 27 | } 28 | if !path.pop() { 29 | panic!("找不到CARGO_MANIFEST_DIR"); 30 | } 31 | } 32 | path.pop(); 33 | path 34 | } 35 | } 36 | } 37 | 38 | /// 准备测试环境,切换到测试目录 39 | fn setup_env() { 40 | color_backtrace::install(); // colorize backtrace 41 | 42 | let mut path = find_cargo_dir(); 43 | path.push(TEST_DIR); 44 | if !path.exists() { 45 | fs::create_dir(&path).unwrap(); 46 | } 47 | std::env::set_current_dir(&path).unwrap(); // 将执行目录切换到测试目录 48 | } 49 | 50 | pub fn init_mit() { 51 | let _ = crate::commands::init(); 52 | Index::reload(); // 重置index, 以防止其他测试修改了index单例 53 | } 54 | 55 | /// with 初始化的干净的mit 56 | pub fn setup_with_clean_mit() { 57 | setup_without_mit(); 58 | init_mit(); 59 | } 60 | 61 | pub fn setup_without_mit() { 62 | // 将执行目录切换到测试目录,并清除测试目录下的.mit目录 63 | setup_env(); 64 | let mut path = util::cur_dir(); 65 | path.push(util::ROOT_DIR); 66 | if path.exists() { 67 | fs::remove_dir_all(&path).unwrap(); 68 | } 69 | } 70 | 71 | pub fn ensure_files>(paths: &Vec) { 72 | for path in paths { 73 | ensure_file(path.as_ref().as_ref(), None); 74 | } 75 | } 76 | 77 | pub fn ensure_empty_dir>(path: P) -> io::Result<()> { 78 | let entries = fs::read_dir(path.as_ref())?; 79 | for entry in entries { 80 | let path = entry?.path(); 81 | if path.is_dir() { 82 | fs::remove_dir_all(&path)?; // 如果是目录,则递归删除 83 | } else { 84 | fs::remove_file(&path)?; // 如果是文件,则直接删除 85 | } 86 | } 87 | Ok(()) 88 | } 89 | 90 | pub fn setup_with_empty_workdir() { 91 | let test_dir = find_cargo_dir().join(TEST_DIR); 92 | ensure_empty_dir(test_dir).unwrap(); 93 | setup_with_clean_mit(); 94 | } 95 | 96 | pub fn ensure_file(path: &Path, content: Option<&str>) { 97 | // 以测试目录为根目录,创建文件 98 | fs::create_dir_all(path.parent().unwrap()).unwrap(); // ensure父目录 99 | let mut file = fs::File::create(util::get_working_dir().unwrap().join(path)) 100 | .unwrap_or_else(|_| panic!("无法创建文件:{:?}", path)); 101 | if let Some(content) = content { 102 | file.write_all(content.as_bytes()).unwrap(); 103 | } else { 104 | // 写入文件名 105 | file.write_all(path.file_name().unwrap().to_str().unwrap().as_bytes()).unwrap(); 106 | } 107 | } 108 | 109 | pub fn ensure_no_file(path: &Path) { 110 | // 以测试目录为根目录,删除文件 111 | if path.exists() { 112 | fs::remove_file(util::get_working_dir().unwrap().join(path)).unwrap(); 113 | } 114 | } 115 | 116 | /** 列出子文件夹 */ 117 | pub fn list_subdir(path: &Path) -> io::Result> { 118 | let mut files = Vec::new(); 119 | let path = path.to_absolute(); 120 | if path.is_dir() { 121 | for entry in fs::read_dir(path)? { 122 | let entry = entry?; 123 | let path = entry.path(); 124 | if path.is_dir() && path.file_name().unwrap_or_default() != util::ROOT_DIR { 125 | files.push(path) 126 | } 127 | } 128 | } 129 | Ok(files) 130 | } 131 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 |

2 | MIT: Mini-Git implementation in Rust 3 |

4 | 5 | **[中文文档](./README.md)** | English 6 | 7 | [Project Link](https://github.com/MrBeanCpp/MIT) 8 | 9 | Git in Rust. A mini Git implementation called`mit`, implemented in `Rust`. 10 | 11 | > Designed to be concise, readable, efficient, and secure. 12 | > 13 | > The best way to learn Git is to implement Git. 14 | > 15 | > This project aims to provide a `Git` implementation that even a second-grader can understand. 16 | > 17 | > `// rm -rf rigid design patterns & complex repository architecture` 18 | > 19 | 20 | **NOTE:** For a more comprehensive implementation of `Git`, please refer to another project of ours: 21 | [Mega-Libra](https://github.com/web3infra-foundation/mega/tree/main/libra) 22 | 23 | ## Cross-Platform Support 24 | 25 | - [x] Windows 26 | - [x] MacOS 27 | - [x] Linux (Unix-like...) 28 | 29 | ## Key Features 30 | 31 | - Supports input paths (pathspec): file paths, directory paths (absolute or relative, including `.`, `./`, `../`) 32 | 33 | - Supports `mit init`, `mit add`, `mit rm`, `mit commit` 34 | 35 | - [x] `init`: Initialize (does nothing if the repository already exists) - `idempotent` 36 | - [x] `add`: Add changes to the staging area (including new, modified, deleted), can specify files or directories 37 | - `-A(all)` : Stage all changes in the working directory (from the root) (new✅ modified✅ deleted✅) 38 | - `-u(update)`: Operate only on tracked files in the staging area [`index`] (new❌ modified✅ deleted✅) 39 | - [x] `rm`: Remove files from the staging area & working directory 40 | - `--cached` : Remove only from the staging area, untrack 41 | - `-r(recursive)`: Recursively delete directories, must specify this parameter when deleting directories 42 | - [x] `commit` 43 | - [x] `status`: Display the status of the working directory, staging area, and `HEAD` (only for the current 44 | directory); divided into three parts: 45 | - **Staged to be committed:** Changes staged in the staging area compared to `HEAD` (last `Commit::Tree`), 46 | i.e., the last staging area 47 | - **Unstaged:** Changes in the working directory not staged in the staging area 48 | - **Untracked:** Files in the working directory not staged or tracked before 49 | - [x] `log` 50 | 51 | - Supports branches`mit branch`, `mit switch`, `mit restore` 52 | 53 | - [x] `branch` 54 | - [x] `switch` 55 | Unlike `checkout`, `switch` requires specifying `--detach` to switch to a `commit`, otherwise, it can only 56 | switch branches. 57 | - [x] `restore`: Rollback files 58 | - Restore files at the specified path (including directories) to the version specified by `--source`, can 59 | specify staging area & working directory 60 | - `--source`: Can specify `Commit Hash`, `HEAD`, or `Branch Name` 61 | - If `--source` is not specified and neither `--staged` nor `--worktree` is specified, restore to the `HEAD` 62 | version, otherwise, restore from the staging area [`index`] 63 | - If neither `--staged` nor `--worktree` is specified, default to restore to `--worktree` 64 | - For files not present in `--source`, if tracked, delete; otherwise, ignore 65 | 66 | - Supports simple merging `mit merge` (fast-forward) 67 | - [x] Merge(FF) 68 | 69 | ## Notes 70 | 71 | ### ⚠️Testing requires single-threading 72 | 73 | ⚠️ Note: To avoid conflicts, please use `--test-threads=1` when executing tests. 74 | 75 | For example:`cargo test -- --test-threads=1` 76 | 77 | This is because testing involves IO on the same folder. 78 | 79 | ### Term Definitions 80 | 81 | - Staging area: `index` or `stage`, stores file snapshots needed for the next `commit` 82 | - Working directory: `worktree`, the folder directly manipulated by the user 83 | - Repository: `working directory` or `repository`, the root directory of the code repository, where `.mit` is located 84 | - `HEAD`:Points to the current`commit` 85 | - Tracked:`tracked`,files already in the staging area [`index`](i.e., files that have been `add`-ed) 86 | 87 | ### Introductory Video 88 | 89 | [【Mit】Rust implementation of Mini-Git - System Software Development Practice Final Report_Bilibili](https://www.bilibili.com/video/BV1p64y1E78W/) 90 | -------------------------------------------------------------------------------- /src/utils/store.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use sha1::{Digest, Sha1}; 4 | 5 | use crate::models::Hash; 6 | 7 | use super::util; 8 | 9 | /// 管理.mit仓库的读写 10 | pub struct Store { 11 | store_path: PathBuf, 12 | } 13 | 14 | /**Store负责管理objects 15 | * 每一个object文件名与内容的hash值相同 16 | */ 17 | impl Store { 18 | fn calc_hash(data: &String) -> String { 19 | let mut hasher = Sha1::new(); 20 | hasher.update(data); 21 | let hash = hasher.finalize(); 22 | hex::encode(hash) 23 | } 24 | 25 | pub fn new() -> Store { 26 | util::check_repo_exist(); 27 | let store_path = util::get_storage_path().unwrap(); 28 | Store { store_path } 29 | } 30 | pub fn load(&self, hash: &String) -> String { 31 | /* 读取文件内容 */ 32 | let mut path = self.store_path.clone(); 33 | path.push("objects"); 34 | path.push(hash); 35 | match std::fs::read_to_string(path) { 36 | Ok(content) => content, 37 | Err(_) => panic!("储存库疑似损坏,无法读取文件"), 38 | } 39 | } 40 | 41 | /** 根据前缀搜索,有歧义时返回 None */ 42 | pub fn search(&self, hash: &String) -> Option { 43 | if hash.is_empty() { 44 | return None; 45 | } 46 | let objects = util::list_files(self.store_path.join("objects").as_path()).unwrap(); 47 | // 转string 48 | let objects = objects 49 | .iter() 50 | .map(|x| x.file_name().unwrap().to_str().unwrap().to_string()) 51 | .collect::>(); 52 | let mut result = None; 53 | for object in objects { 54 | if object.starts_with(hash) { 55 | if result.is_some() { 56 | return None; 57 | } 58 | result = Some(object); 59 | } 60 | } 61 | result 62 | } 63 | 64 | pub fn save(&self, content: &String) -> Hash { 65 | /* 保存文件内容 */ 66 | let hash = Self::calc_hash(content); 67 | let mut path = self.store_path.clone(); 68 | path.push("objects"); 69 | path.push(&hash); 70 | // println!("Saved to: [{}]", path.display()); 71 | if path.exists() { 72 | // IO优化,文件已存在,不再写入 73 | return hash; 74 | } 75 | match std::fs::write(path, content) { 76 | Ok(_) => hash, 77 | Err(_) => panic!("储存库疑似损坏,无法写入文件"), 78 | } 79 | } 80 | 81 | pub fn dry_save(&self, content: &String) -> Hash { 82 | /* 不实际保存文件,返回Hash */ 83 | #[warn(clippy::let_and_return)] 84 | let hash = Self::calc_hash(content); 85 | // TODO more such as check 86 | hash 87 | } 88 | } 89 | #[cfg(test)] 90 | mod tests { 91 | use std::fs; 92 | 93 | use super::*; 94 | use crate::utils::test; 95 | 96 | #[test] 97 | fn test_new_success() { 98 | test::setup_with_clean_mit(); 99 | let _ = Store::new(); 100 | } 101 | 102 | #[test] 103 | #[should_panic] 104 | fn test_new_fail() { 105 | test::setup_without_mit(); 106 | let _ = Store::new(); 107 | } 108 | 109 | #[test] 110 | fn test_save_and_load() { 111 | test::setup_with_clean_mit(); 112 | let store = Store::new(); 113 | let content = "hello world".to_string(); 114 | let hash = store.save(&content); 115 | let content2 = store.load(&hash); 116 | assert_eq!(content, content2, "内容不一致"); 117 | } 118 | 119 | #[test] 120 | fn test_search() { 121 | test::setup_with_clean_mit(); 122 | let hashs = vec!["1234567890".to_string(), "1235467891".to_string(), "4567892".to_string()]; 123 | for hash in hashs.iter() { 124 | let mut path = util::get_storage_path().unwrap(); 125 | path.push("objects"); 126 | path.push(hash); 127 | fs::write(path, "hello world").unwrap(); 128 | } 129 | let store = Store::new(); 130 | assert!(store.search(&"123".to_string()).is_none()); // 有歧义 131 | assert!(store.search(&"1234".to_string()).is_some()); // 精确 132 | assert!(store.search(&"4".to_string()).is_some()); // 精确 133 | assert!(store.search(&"1234567890123".to_string()).is_none()); // 不匹配 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/commands/merge.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::{self, status::*}, 3 | models::{head, Commit, Hash}, 4 | utils::{store, util}, 5 | }; 6 | 7 | enum MergeErr { 8 | NoFastForward, 9 | NoClean, 10 | } 11 | 12 | fn check_ff(current: &Hash, target: Hash) -> Result { 13 | let target_commit = Commit::load(&target); 14 | // 检查current是否是target的祖先 15 | if *current == target_commit.get_hash() { 16 | return Ok(true); 17 | } 18 | for parent in target_commit.get_parent_hash() { 19 | let result = check_ff(current, parent); 20 | if result.is_ok() { 21 | return result; 22 | } 23 | } 24 | Err(MergeErr::NoFastForward) 25 | } 26 | 27 | /** commit 以fast forward到形式合并到当前分支 */ 28 | fn merge_ff(commit_hash: String) -> Result<(), MergeErr> { 29 | // 检查更改 30 | if !changes_to_be_staged().is_empty() { 31 | println!("fatal: 你有未暂存的更改,切换分支会导致更改丢失"); 32 | return Err(MergeErr::NoClean); 33 | } else if !changes_to_be_committed().is_empty() { 34 | println!("fatal: 你有未提交的更改,无法切换分支"); 35 | return Err(MergeErr::NoClean); 36 | } 37 | 38 | // 检查当前分支是否可以fast forward到commit 39 | let current_commit = head::current_head_commit(); 40 | let check = check_ff(¤t_commit, commit_hash.clone()); 41 | check?; 42 | 43 | // 执行fast forward 44 | let head = head::current_head(); 45 | match head { 46 | head::Head::Branch(branch) => { 47 | head::update_branch(&branch, &commit_hash.clone()); 48 | commands::restore::restore(vec![], Some(commit_hash.clone()), true, true) 49 | } 50 | head::Head::Detached(_) => { 51 | // 相当于切换到了commit_hash,什么都没有发生 52 | commands::switch::switch(Some(commit_hash.clone()), None, true); 53 | } 54 | } 55 | Ok(()) 56 | } 57 | 58 | /** merge,暂时只支持fast forward */ 59 | pub fn merge(branch: String) { 60 | let merge_commit = { 61 | if head::list_local_branches().contains(&branch) { 62 | // Branch Name, e.g. master 63 | head::get_branch_head(&branch) 64 | } else { 65 | // Commit Hash, e.g. a1b2c3d4 66 | let store = store::Store::new(); 67 | let commit = store.search(&branch); 68 | if commit.is_none() || !util::is_typeof_commit(commit.clone().unwrap()) { 69 | println!("fatal: 非法的 commit hash: '{}'", branch); 70 | return; 71 | } 72 | commit.unwrap() 73 | } 74 | }; 75 | // 暂时只支持fast forward 76 | let _ = merge_ff(merge_commit); 77 | } 78 | 79 | #[cfg(test)] 80 | mod test { 81 | use super::*; 82 | use crate::{ 83 | commands::{commit, switch::switch}, 84 | utils::test, 85 | }; 86 | 87 | #[test] 88 | fn test_check_ff() { 89 | test::setup_with_empty_workdir(); 90 | commit::commit("init".to_string(), true); 91 | let commit1 = head::current_head_commit(); 92 | let origin_branch = match head::current_head() { 93 | head::Head::Branch(branch) => branch, 94 | _ => panic!("current head is not a branch"), 95 | }; 96 | 97 | let new_branch = "new_branch".to_string(); 98 | switch(None, Some(new_branch.clone()), false); 99 | commit::commit("new_branch commit 1".to_string(), true); 100 | commit::commit("new_branch commit 2".to_string(), true); 101 | assert_ne!(head::current_head_commit(), commit1); 102 | assert_eq!(head::get_branch_head(&origin_branch.clone()), commit1); 103 | let commit2 = head::current_head_commit(); 104 | println!("[info] success create new branch: {}", new_branch); 105 | 106 | // test success merge 107 | switch(Some(origin_branch.clone()), None, false); 108 | assert_eq!(head::current_head_commit(), commit1); 109 | 110 | let result = merge_ff(commit2.clone()); 111 | assert!(result.is_ok()); 112 | assert_eq!(head::current_head_commit(), commit2); 113 | assert_eq!(head::get_branch_head(&origin_branch.clone()), commit2); 114 | println!("[info] success merge ff"); 115 | 116 | // test no fast forward 117 | commit::commit("master commit 2".to_string(), true); 118 | let result = merge_ff(commit1.clone()); 119 | assert!(result.is_err()); 120 | assert!(matches!(result.unwrap_err(), MergeErr::NoFastForward)); 121 | print!("success detect no fast forward"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgGroup, Parser, Subcommand}; 2 | use super::commands as cmd; 3 | /// Rust实现的简易版本的Git,用于学习Rust语言 4 | #[derive(Parser)] 5 | #[command(author, version, about, long_about = None)] 6 | struct Cli { 7 | /// The subcommand to run. 8 | #[clap(subcommand)] 9 | command: Command, 10 | } 11 | /// @see Rust Clap库学习 - 掘金 12 | #[derive(Subcommand)] 13 | enum Command { 14 | /// 初始化仓库 15 | Init, 16 | /// 添加文件到暂存区 17 | /// @see git add .,git add -A,git add -u,git add * 的区别与联系 18 | Add { 19 | /// 要添加的文件 20 | #[clap(required = true)] 21 | files: Vec, 22 | 23 | /// 将工作区中所有的文件改动提交至暂存区(包括新增、修改和删除) 24 | #[clap(short = 'A', long)] 25 | all: bool, 26 | 27 | /// 将工作区中已跟踪的文件(tracked)更新到暂存区(修改 & 删除);But不包含新增 28 | #[clap(short, long)] 29 | update: bool, 30 | }, 31 | /// 删除文件 32 | Rm { 33 | /// 要删除的文件 34 | files: Vec, 35 | /// flag 删除暂存区的文件 36 | #[clap(long, action)] 37 | cached: bool, 38 | /// flag 递归删除目录 39 | #[clap(short, long)] 40 | recursive: bool, 41 | }, 42 | /// 提交暂存区的文件 43 | Commit { 44 | #[clap(short, long)] 45 | message: String, 46 | 47 | #[clap(long, action)] 48 | allow_empty: bool, 49 | }, 50 | /// 查看当前状态 51 | Status, 52 | /// log 现实提交历史 53 | #[clap(group = ArgGroup::new("sub").required(false))] 54 | Log { 55 | #[clap(short = 'A', long)] 56 | all: bool, 57 | 58 | #[clap(short, long)] 59 | number: Option, 60 | }, 61 | /// branch 62 | Branch { 63 | /// 新分支名 64 | #[clap(group = "sub")] 65 | new_branch: Option, 66 | 67 | /// 基于某个commit创建分支 68 | #[clap(requires = "new_branch")] 69 | commit_hash: Option, 70 | 71 | /// 列出所有分支 72 | #[clap(short, long, action, group = "sub", default_value = "true")] 73 | list: bool, 74 | 75 | /// 删除制定分支,不能删除当前所在分支 76 | #[clap(short = 'D', long, group = "sub")] 77 | delete: Option, 78 | 79 | /// 显示当前分支 80 | #[clap(long, action, group = "sub")] 81 | show_current: bool, 82 | }, 83 | 84 | /// 切换分支 85 | Switch { 86 | /// 要切换的分支 87 | #[clap(required_unless_present("create"))] 88 | branch: Option, 89 | 90 | /// 创建并切换到新分支 91 | #[clap(long, short, group = "sub")] 92 | create: Option, 93 | 94 | /// 是否允许切换到commit 95 | #[clap(long, short, action, default_value = "false", group = "sub")] 96 | detach: bool, 97 | }, 98 | /// restore 99 | Restore { 100 | /// 要恢复的文件 101 | #[clap(required = true)] 102 | path: Vec, 103 | 104 | /// source 105 | #[clap(long, short)] 106 | source: Option, 107 | 108 | /// worktree 109 | #[clap(long, short = 'W', action)] 110 | worktree: bool, 111 | 112 | /// staged 113 | #[clap(long, short = 'S', action)] 114 | staged: bool, 115 | }, 116 | /// merge 117 | Merge { 118 | /// 要合并的分支 119 | #[clap(required = true)] 120 | branch: String, 121 | }, 122 | } 123 | pub fn handle_command() { 124 | let cli = Cli::parse(); 125 | match cli.command { 126 | Command::Init => { 127 | cmd::init().expect("初始化失败"); 128 | } 129 | Command::Add { files, all, update } => { 130 | cmd::add(files, all, update); 131 | } 132 | Command::Rm { files, cached, recursive } => { 133 | cmd::rm(files, cached, recursive).expect("删除失败"); 134 | } 135 | Command::Commit { message, allow_empty } => { 136 | cmd::commit(message, allow_empty); 137 | } 138 | Command::Status => { 139 | cmd::status(); 140 | } 141 | Command::Log { all, number } => { 142 | cmd::log(all, number); 143 | } 144 | Command::Branch { list, delete, new_branch, commit_hash, show_current } => { 145 | cmd::branch(new_branch, commit_hash, list, delete, show_current); 146 | } 147 | Command::Switch { branch, create, detach } => { 148 | cmd::switch(branch, create, detach); 149 | } 150 | Command::Restore { path, source, mut worktree, staged } => { 151 | // 未指定stage和worktree时,默认操作worktree 152 | // 指定 --staged 将仅还原index 153 | if !staged { 154 | worktree = true; 155 | } 156 | // 未指定source 且 --staged,默认操作HEAD,否则从index中恢复(就近原则) 157 | /* 158 | If `--source` not specified, the contents are restored from `HEAD` if `--staged` is given, 159 | otherwise from the [index]. 160 | */ 161 | cmd::restore(path, source, worktree, staged); 162 | } 163 | Command::Merge { branch } => { 164 | cmd::merge(branch); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/models/head.rs: -------------------------------------------------------------------------------- 1 | use crate::{models::Hash, utils::util}; 2 | 3 | pub enum Head { 4 | Detached(String), 5 | Branch(Hash), 6 | } 7 | 8 | pub fn current_head() -> Head { 9 | let mut head = util::get_storage_path().unwrap(); 10 | head.push("HEAD"); 11 | let head_content = std::fs::read_to_string(head).expect("HEAD文件损坏").trim_end().to_string(); //去除末尾\n 12 | if head_content.starts_with("ref: refs/heads/") { 13 | let branch_name = head_content.trim_start_matches("ref: refs/heads/"); 14 | Head::Branch(branch_name.to_string()) 15 | } else { 16 | Head::Detached(head_content) 17 | } 18 | } 19 | pub fn update_branch(branch_name: &String, commit_hash: &String) { 20 | // 更新分支head 21 | let mut branch = util::get_storage_path().unwrap(); 22 | branch.push("refs"); 23 | branch.push("heads"); 24 | branch.push(branch_name); 25 | std::fs::write(&branch, commit_hash) 26 | .unwrap_or_else(|_| panic!("无法写入branch in {:?} with {}", branch, commit_hash)); 27 | } 28 | 29 | pub fn get_branch_head(branch_name: &String) -> String { 30 | // 返回当前分支的commit hash 31 | let mut branch = util::get_storage_path().unwrap(); 32 | branch.push("refs"); 33 | branch.push("heads"); 34 | branch.push(branch_name); 35 | if branch.exists() { 36 | std::fs::read_to_string(branch).expect("无法读取branch") 37 | } else { 38 | "".to_string() // 分支不存在或者没有commit 39 | } 40 | } 41 | pub fn delete_branch(branch_name: &String) { 42 | let mut branch = util::get_storage_path().unwrap(); 43 | branch.push("refs"); 44 | branch.push("heads"); 45 | branch.push(branch_name); 46 | if branch.exists() { 47 | std::fs::remove_file(branch).expect("无法删除branch"); 48 | } else { 49 | panic!("branch file not exist"); 50 | } 51 | } 52 | 53 | /**返回当前head指向的commit hash,如果是分支,则返回分支的commit hash */ 54 | pub fn current_head_commit() -> String { 55 | //TODO 明确返回Hash 56 | let head = current_head(); 57 | match head { 58 | Head::Branch(branch_name) => { 59 | get_branch_head(&branch_name) 60 | } 61 | Head::Detached(commit_hash) => commit_hash, 62 | } 63 | } 64 | 65 | /** 将当前的head指向commit_hash,根据当前的head类型,更新不同的文件 */ 66 | pub fn update_head_commit(commit_hash: &String) { 67 | let head = current_head(); 68 | match head { 69 | Head::Branch(branch_name) => { 70 | update_branch(&branch_name, commit_hash); 71 | } 72 | Head::Detached(_) => { 73 | let mut head = util::get_storage_path().unwrap(); 74 | head.push("HEAD"); 75 | std::fs::write(head, commit_hash).expect("无法写入HEAD"); 76 | } 77 | } 78 | } 79 | 80 | /** 列出本地的branch */ 81 | pub fn list_local_branches() -> Vec { 82 | let mut branches = Vec::new(); 83 | let mut branch_dir = util::get_storage_path().unwrap(); 84 | branch_dir.push("refs"); 85 | branch_dir.push("heads"); 86 | if branch_dir.exists() { 87 | let entries = std::fs::read_dir(branch_dir).expect("无法读取branch"); 88 | for entry in entries { 89 | let entry = entry.unwrap(); 90 | let branch_name = entry.file_name().into_string().unwrap(); 91 | branches.push(branch_name); 92 | } 93 | } 94 | branches 95 | } 96 | 97 | /** 切换head到branch */ 98 | pub fn change_head_to_branch(branch_name: &String) { 99 | let mut head = util::get_storage_path().unwrap(); 100 | head.push("HEAD"); 101 | let branch_head = get_branch_head(branch_name); 102 | std::fs::write(head, format!("ref: refs/heads/{}", branch_name)).expect("无法写入HEAD"); 103 | update_head_commit(&branch_head); 104 | } 105 | 106 | /** 切换head到非branchcommit */ 107 | pub fn change_head_to_commit(commit_hash: &String) { 108 | let mut head = util::get_storage_path().unwrap(); 109 | head.push("HEAD"); 110 | std::fs::write(head, commit_hash).expect("无法写入HEAD"); 111 | } 112 | 113 | #[cfg(test)] 114 | mod test { 115 | use crate::models::head; 116 | use crate::utils::test; 117 | 118 | #[test] 119 | fn test_edit_branch() { 120 | test::setup_with_clean_mit(); 121 | let branch_name = "test_branch".to_string() + &rand::random::().to_string(); 122 | let branch_head = super::get_branch_head(&branch_name); 123 | assert!(branch_head.is_empty()); 124 | 125 | let commit_hash = "1234567890".to_string(); 126 | super::update_branch(&branch_name, &commit_hash); 127 | let branch_head = super::get_branch_head(&branch_name); 128 | assert!(!branch_head.is_empty()); 129 | assert!(branch_head == commit_hash); 130 | } 131 | 132 | #[test] 133 | fn test_list_local_branches() { 134 | test::setup_with_clean_mit(); 135 | let branch_one = "test_branch".to_string() + &rand::random::().to_string(); 136 | let branch_two = "test_branch".to_string() + &rand::random::().to_string(); 137 | head::update_branch(&branch_one, &"1234567890".to_string()); 138 | head::update_branch(&branch_two, &"1234567890".to_string()); 139 | 140 | let branches = super::list_local_branches(); 141 | assert!(branches.contains(&branch_one)); 142 | assert!(branches.contains(&branch_two)); 143 | } 144 | 145 | #[test] 146 | fn test_change_head_to_branch() { 147 | test::setup_with_clean_mit(); 148 | let branch_name = "test_branch".to_string() + &rand::random::().to_string(); 149 | head::update_branch(&branch_name, &"1234567890".to_string()); 150 | super::change_head_to_branch(&branch_name); 151 | assert!( 152 | match super::current_head() { 153 | super::Head::Branch(head_commit) => head_commit == branch_name, 154 | _ => false, 155 | }, 156 | "当前不在分支上" 157 | ); 158 | } 159 | 160 | #[test] 161 | fn test_change_head_to_commit() { 162 | test::setup_with_clean_mit(); 163 | let commit_hash = "1234567890".to_string(); 164 | super::change_head_to_commit(&commit_hash); 165 | assert!( 166 | match super::current_head() { 167 | super::Head::Detached(head_commit) => head_commit == commit_hash, 168 | _ => false, 169 | }, 170 | "当前不在分支上" 171 | ); 172 | } 173 | 174 | #[test] 175 | fn test_update_branch_head() { 176 | test::setup_with_clean_mit(); 177 | let branch_name = "test_branch".to_string() + &rand::random::().to_string(); 178 | let commit_hash = "1234567890".to_string(); 179 | super::update_branch(&branch_name, &commit_hash); 180 | let branch_head = super::get_branch_head(&branch_name); 181 | assert!(!branch_head.is_empty()); 182 | assert!(branch_head == commit_hash); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/commands/switch.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | 3 | use crate::{ 4 | models::{head, Commit, Hash}, 5 | utils::{store, util}, 6 | }; 7 | 8 | use super::{ 9 | branch, 10 | restore::{restore_index, restore_worktree}, 11 | status, 12 | }; 13 | 14 | enum SwitchErr { 15 | NoClean, 16 | InvalidBranch, 17 | InvalidObject, 18 | } 19 | 20 | /** 将工作区域的文件更改为commit_hash的版本,可以指定filter未特定文件或路径 */ 21 | fn switch_to_commit(commit_hash: Hash) { 22 | let commit = Commit::load(&commit_hash); 23 | let tree = commit.get_tree(); 24 | let target_files = tree.get_recursive_blobs(); // 相对路径 25 | 26 | // 借用逻辑类似的restore_workdir_into_files 27 | restore_worktree(None, &target_files); 28 | // 同时restore index 29 | restore_index(None, &target_files); 30 | } 31 | 32 | fn switch_to(branch: String, detach: bool) -> Result<(), SwitchErr> { 33 | // 检查更改 34 | let unstaged = status::changes_to_be_staged(); // unstaged.new是未跟踪 不需要处理 35 | if !unstaged.deleted.is_empty() || !unstaged.modified.is_empty() { 36 | status::status(); 37 | println!("fatal: 你有未暂存的更改,切换分支会导致更改丢失"); 38 | return Err(SwitchErr::NoClean); 39 | } else if !status::changes_to_be_committed().is_empty() { 40 | status::status(); 41 | println!("fatal: 你有未提交的更改,无法切换分支"); 42 | return Err(SwitchErr::NoClean); 43 | } 44 | 45 | let store = store::Store::new(); 46 | if head::list_local_branches().contains(&branch) { 47 | // 切到分支 48 | let branch_commit = head::get_branch_head(&branch); 49 | switch_to_commit(branch_commit.clone()); 50 | head::change_head_to_branch(&branch); // 更改head 51 | println!("切换到分支: '{}'", branch.green()) 52 | } else if detach { 53 | let commit = store.search(&branch); 54 | if commit.is_none() || util::check_object_type(commit.clone().unwrap()) != util::ObjectType::Commit { 55 | println!("fatal: 非法的 commit: '{}'", branch); 56 | return Err(SwitchErr::InvalidObject); 57 | } 58 | 59 | // 切到commit 60 | let commit = commit.unwrap(); 61 | switch_to_commit(commit.clone()); 62 | head::change_head_to_commit(&commit); // 更改head 63 | println!("切换到 detach commit: '{}'", commit.yellow()) 64 | } else { 65 | println!("fatal: 不存在分支 '{}'", branch); 66 | return Err(SwitchErr::InvalidBranch); 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | pub fn switch(target_branch: Option, create: Option, detach: bool) { 73 | match create { 74 | Some(new_branch) => { 75 | // 以target_branch为基础创建新分支create 76 | println!("create new branch: {:?}", new_branch); 77 | branch::branch(Some(new_branch.clone()), target_branch.clone(), false, None, false); 78 | let _ = switch_to(new_branch, true); 79 | } 80 | None => { 81 | println!("switch to branch: {:?}", target_branch.as_ref().unwrap()); 82 | let _ = switch_to(target_branch.unwrap(), detach); 83 | } 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod test { 89 | use super::*; 90 | use crate::{ 91 | commands::{self as cmd}, 92 | utils::test, 93 | }; 94 | use std::path::PathBuf; 95 | #[test] 96 | fn test_switch() { 97 | test::setup_with_empty_workdir(); 98 | 99 | cmd::commit("init".to_string(), true); 100 | let test_branch_1 = "test_branch_1".to_string(); 101 | cmd::branch(Some(test_branch_1.clone()), None, false, None, false); 102 | 103 | /* test 1: NoClean */ 104 | let test_file_1 = PathBuf::from("test_file_1"); 105 | test::ensure_file(&test_file_1, None); 106 | cmd::add(vec![], true, false); // add all 107 | let result = switch_to(test_branch_1.clone(), false); 108 | assert!(result.is_err()); 109 | assert!(matches!(result.unwrap_err(), SwitchErr::NoClean)); 110 | 111 | cmd::commit("add file 1".to_string(), true); 112 | let test_branch_2 = "test_branch_2".to_string(); 113 | cmd::branch(Some(test_branch_2.clone()), None, false, None, false); // branch2: test_file_1 exists 114 | 115 | /* test 2: InvalidBranch */ 116 | let result = switch_to("invalid_branch".to_string(), false); 117 | assert!(result.is_err()); 118 | assert!(matches!(result.unwrap_err(), SwitchErr::InvalidBranch)); 119 | 120 | /* test 3: InvalidObject*/ 121 | let result = switch_to("invalid_commit".to_string(), true); 122 | assert!(result.is_err()); 123 | assert!(matches!(result.unwrap_err(), SwitchErr::InvalidObject)); 124 | 125 | let tees_file_2 = PathBuf::from("test_file_2"); 126 | test::ensure_file(&tees_file_2, None); 127 | cmd::add(vec![], true, false); // add all 128 | cmd::commit("add file 2".to_string(), false); 129 | let history_commit = head::current_head_commit(); // commit: test_file_1 exists, test_file_2 exists 130 | 131 | test::ensure_no_file(&test_file_1); 132 | cmd::add(vec![], true, false); // add all 133 | assert!(!test_file_1.exists()); 134 | cmd::commit("delete file 1".to_string(), false); 135 | let branch_master = match head::current_head()/* master: test_file_1 not exists, test_file_2 exists */{ 136 | head::Head::Branch(branch) => branch, 137 | _ => panic!("current head is not branch"), 138 | }; 139 | 140 | /* test 4: switch to branch */ 141 | let result = switch_to(test_branch_2.clone(), false); 142 | assert!(result.is_ok()); 143 | assert!(status::changes_to_be_staged().is_empty()); 144 | assert!(status::changes_to_be_committed().is_empty()); 145 | assert!(match head::current_head() { 146 | head::Head::Branch(branch) => branch == test_branch_2, 147 | _ => false, 148 | }); 149 | assert!(test_file_1.exists()); 150 | assert!(!tees_file_2.exists()); 151 | 152 | /* test 5: switch to commit */ 153 | let result = switch_to(history_commit.clone(), true); 154 | assert!(result.is_ok()); 155 | assert!(status::changes_to_be_staged().is_empty() && status::changes_to_be_committed().is_empty()); 156 | assert!(match head::current_head() { 157 | head::Head::Detached(commit) => commit == history_commit, 158 | _ => false, 159 | }); 160 | assert!(test_file_1.exists()); 161 | assert!(tees_file_2.exists()); 162 | assert!(match head::current_head() { 163 | head::Head::Detached(commit) => commit == history_commit, 164 | _ => false, 165 | }); 166 | 167 | /* test 6: switch to master */ 168 | let result = switch_to(branch_master.clone(), false); 169 | assert!(result.is_ok()); 170 | assert!(match head::current_head() { 171 | head::Head::Branch(branch) => branch == branch_master, 172 | _ => false, 173 | }); 174 | assert!(!test_file_1.exists()); 175 | assert!(tees_file_2.exists()); 176 | assert!(status::changes_to_be_staged().is_empty() && status::changes_to_be_committed().is_empty()); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/commands/branch.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | 3 | use crate::{ 4 | models::*, 5 | utils::{store, util}, 6 | }; 7 | 8 | // branch error 9 | enum BranchErr { 10 | BranchExist, 11 | InvalidObject, 12 | 13 | BranchNoExist, 14 | BranchCheckedOut, 15 | } 16 | // 从分支名、commit hash中搜索commit 17 | fn search_hash(commit_hash: Hash) -> Option { 18 | // 分支名 19 | if head::list_local_branches().contains(&commit_hash) { 20 | let commit_hash = head::get_branch_head(&commit_hash); 21 | return Some(commit_hash); 22 | } 23 | // commit hash 24 | let store = store::Store::new(); 25 | store.search(&commit_hash) 26 | } 27 | 28 | fn create_branch(branch_name: String, _base_commit: Hash) -> Result<(), BranchErr> { 29 | // 找到正确的base_commit_hash 30 | let base_commit = search_hash(_base_commit.clone()); 31 | if base_commit.is_none() || util::check_object_type(base_commit.clone().unwrap()) != util::ObjectType::Commit { 32 | println!("fatal: 非法的 commit: '{}'", _base_commit); 33 | return Err(BranchErr::InvalidObject); 34 | } 35 | 36 | let base_commit = Commit::load(&base_commit.unwrap()); 37 | 38 | let exist_branches = head::list_local_branches(); 39 | if exist_branches.contains(&branch_name) { 40 | println!("fatal: 分支 '{}' 已存在", branch_name); 41 | return Err(BranchErr::BranchExist); 42 | } 43 | 44 | head::update_branch(&branch_name, &base_commit.get_hash()); 45 | Ok(()) 46 | } 47 | 48 | fn delete_branch(branch_name: String) -> Result<(), BranchErr> { 49 | let branches = head::list_local_branches(); 50 | if !branches.contains(&branch_name) { 51 | println!("error: 分支 '{}' 不存在", branch_name); 52 | return Err(BranchErr::BranchNoExist); 53 | } 54 | 55 | // 仅在当前分支为删除分支时,不允许删除(在历史commit上允许删除) 56 | let current_branch = match head::current_head() { 57 | head::Head::Branch(branch_name) => branch_name, 58 | _ => "".to_string(), 59 | }; 60 | if current_branch == branch_name { 61 | println!("error: 不能删除当前所在分支 {:?}", branch_name); 62 | return Err(BranchErr::BranchCheckedOut); 63 | } 64 | 65 | head::delete_branch(&branch_name); // 删除refs/heads/branch_name,不删除任何commit 66 | Ok(()) 67 | } 68 | 69 | fn show_current_branch() { 70 | println!("show_current_branch"); 71 | let head = head::current_head(); 72 | if let head::Head::Branch(branch_name) = head { 73 | println!("{}", branch_name); 74 | } 75 | } 76 | 77 | fn list_branches() { 78 | println!("list_branches"); 79 | let branches = head::list_local_branches(); 80 | match head::current_head() { 81 | head::Head::Branch(branch_name) => { 82 | println!("* {}", branch_name.green()); 83 | for branch in branches { 84 | if branch != branch_name { 85 | println!(" {}", branch); 86 | } 87 | } 88 | } 89 | head::Head::Detached(commit_hash) => { 90 | println!("* (HEAD detached at {}) {}", commit_hash.green(), commit_hash[0..7].green()); 91 | for branch in branches { 92 | println!(" {}", branch); 93 | } 94 | } 95 | } 96 | } 97 | 98 | pub fn branch( 99 | new_branch: Option, 100 | commit_hash: Option, 101 | list: bool, 102 | delete: Option, 103 | show_current: bool, 104 | ) { 105 | if new_branch.is_some() { 106 | let basic_commit = commit_hash.unwrap_or_else(head::current_head_commit); // 默认使用当前commit 107 | let _ = create_branch(new_branch.unwrap(), basic_commit); 108 | } else if delete.is_some() { 109 | let _ = delete_branch(delete.unwrap()); 110 | } else if show_current { 111 | show_current_branch(); 112 | } else if list { 113 | // 兜底list 114 | list_branches(); 115 | } else { 116 | panic!("should not reach here") 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod test { 122 | use super::*; 123 | use crate::{commands, utils::test}; 124 | #[test] 125 | fn test_create_branch() { 126 | test::setup_with_clean_mit(); 127 | 128 | // no commit: invalid object 129 | let result = create_branch("test_branch".to_string(), head::current_head_commit()); 130 | assert!(result.is_err()); 131 | assert!(matches!(result.unwrap_err(), BranchErr::InvalidObject)); 132 | assert!(head::list_local_branches().is_empty()); 133 | 134 | commands::commit::commit("test commit 1".to_string(), true); 135 | let commit_hash_one = head::current_head_commit(); 136 | commands::commit::commit("test commit 2".to_string(), true); 137 | let commit_hash_two = head::current_head_commit(); 138 | 139 | // success, use part of commit hash 140 | let new_branch_one = "test_branch".to_string() + &rand::random::().to_string(); 141 | let result = create_branch(new_branch_one.clone(), commit_hash_one[0..7].to_string()); 142 | assert!(result.is_ok()); 143 | assert!(head::list_local_branches().contains(&new_branch_one), "new branch not in list"); 144 | assert!(head::get_branch_head(&new_branch_one) == commit_hash_one, "new branch head error"); 145 | 146 | // branch exist 147 | let result = create_branch(new_branch_one.clone(), commit_hash_two.clone()); 148 | assert!(result.is_err()); 149 | assert!(matches!(result.unwrap_err(), BranchErr::BranchExist)); 150 | 151 | // use branch name as commit hash, success 152 | let new_branch_two = "test_branch".to_string() + &rand::random::().to_string(); 153 | let result = create_branch(new_branch_two.clone(), new_branch_one.clone()); 154 | assert!(result.is_ok()); 155 | assert!(head::list_local_branches().contains(&new_branch_two), "new branch not in list"); 156 | assert!(head::get_branch_head(&new_branch_two) == commit_hash_one, "new branch head error"); 157 | } 158 | 159 | #[test] 160 | fn test_delete_branch() { 161 | test::setup_with_clean_mit(); 162 | 163 | // no commit: invalid object 164 | let result = delete_branch("test_branch".to_string()); 165 | assert!(result.is_err()); 166 | assert!(matches!(result.unwrap_err(), BranchErr::BranchNoExist)); 167 | assert!(head::list_local_branches().is_empty()); 168 | 169 | commands::commit::commit("test commit 1".to_string(), true); 170 | let commit_hash = head::current_head_commit(); 171 | 172 | // success 173 | let new_branch = "test_branch".to_string() + &rand::random::().to_string(); 174 | let result = create_branch(new_branch.clone(), commit_hash.clone()); 175 | assert!(result.is_ok()); 176 | assert!(head::list_local_branches().contains(&new_branch), "new branch not in list"); 177 | assert!(head::get_branch_head(&new_branch) == commit_hash, "new branch head error"); 178 | 179 | // branch exist 180 | let result = delete_branch(new_branch.clone()); 181 | assert!(result.is_ok()); 182 | assert!(!head::list_local_branches().contains(&new_branch), "new branch not in list"); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/models/tree.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, path::PathBuf}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::utils::PathExt; 6 | use crate::utils::{store, util}; 7 | 8 | use super::{Hash, Index}; 9 | /*Tree 10 | * Tree是一个版本中所有文件的集合。从根目录还是,每个目录是一个Tree,每个文件是一个Blob。Tree之间互相嵌套表示文件的层级关系。 11 | * 每一个Tree对象也是对应到git储存仓库的一个文件,其内容是一个或多个TreeEntry。 12 | */ 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | pub struct TreeEntry { 15 | pub filemode: (String, String), // (type, mode), type: blob or tree; mode: 100644 or 04000 16 | pub object_hash: Hash, // blob hash or tree hash 17 | pub name: String, // file name 18 | } 19 | 20 | /// 相对路径(to workdir) 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct Tree { 23 | #[serde(skip)] 24 | pub hash: Hash, 25 | pub entries: Vec, 26 | } 27 | 28 | /** 将文件列表保存为Tree Object,并返回最上层的Tree */ 29 | fn store_path_to_tree(index: &Index, current_root: PathBuf) -> Tree { 30 | let get_blob_entry = |path: &PathBuf| { 31 | let mete = index.get(path).unwrap().clone(); 32 | let filename = path.file_name().unwrap().to_str().unwrap().to_string(); 33 | 34 | TreeEntry { 35 | filemode: (String::from("blob"), mete.mode), 36 | object_hash: mete.hash, 37 | name: filename, 38 | } 39 | }; 40 | let mut tree = Tree { hash: "".to_string(), entries: Vec::new() }; 41 | let mut processed_path: HashSet = HashSet::new(); 42 | let path_entries: Vec = index 43 | .get_tracked_files() 44 | .iter() 45 | .map(|file| file.to_relative_workdir()) 46 | .filter(|path| path.starts_with(¤t_root)) 47 | .collect(); 48 | for path in path_entries.iter() { 49 | // 判断是不是直接在根目录下 50 | let in_path = path.parent().unwrap() == current_root; 51 | // 一定是文件,不会是目录 52 | if in_path { 53 | let entry = get_blob_entry(path); 54 | tree.entries.push(entry); 55 | } else { 56 | if path.components().count() == 1 { 57 | continue; 58 | } 59 | // 拿到下一级别目录 60 | let process_path = path 61 | .components() 62 | .nth(current_root.components().count()) 63 | .unwrap() 64 | .as_os_str() 65 | .to_str() 66 | .unwrap(); 67 | // TODO 函数整体逻辑错误,等待修复@houxiaoxuan 68 | if processed_path.contains(process_path) { 69 | continue; 70 | } 71 | processed_path.insert(process_path.to_string()); 72 | 73 | let sub_tree = store_path_to_tree(index, current_root.clone().join(process_path)); 74 | let mode = util::get_file_mode(&util::get_working_dir().unwrap().join(process_path)); 75 | tree.entries.push(TreeEntry { 76 | filemode: (String::from("tree"), mode), 77 | object_hash: sub_tree.get_hash(), 78 | name: process_path.to_string(), 79 | }); 80 | } 81 | } 82 | tree.save(); 83 | tree 84 | } 85 | 86 | impl Tree { 87 | pub fn get_hash(&self) -> String { 88 | self.hash.clone() 89 | } 90 | 91 | pub fn new(index: &Index) -> Tree { 92 | store_path_to_tree(index, "".into()) 93 | } 94 | 95 | pub fn load(hash: &String) -> Tree { 96 | let s = store::Store::new(); 97 | let tree_data = s.load(hash); 98 | let mut tree: Tree = serde_json::from_str(&tree_data).unwrap(); 99 | tree.hash = hash.clone(); 100 | tree 101 | } 102 | 103 | pub fn save(&mut self) -> Hash { 104 | let s = store::Store::new(); 105 | let tree_data = serde_json::to_string_pretty(&self).unwrap(); 106 | let hash = s.save(&tree_data); 107 | self.hash = hash.clone(); 108 | hash 109 | } 110 | 111 | ///注:相对路径(to workdir) 112 | pub fn get_recursive_blobs(&self) -> Vec<(PathBuf, Hash)> { 113 | //TODO 返回HashMap 114 | let mut blob_hashes = Vec::new(); 115 | for entry in self.entries.iter() { 116 | if entry.filemode.0 == "blob" { 117 | blob_hashes.push((PathBuf::from(entry.name.clone()), entry.object_hash.clone())); 118 | } else { 119 | let sub_tree = Tree::load(&entry.object_hash); 120 | let sub_blobs = sub_tree.get_recursive_blobs(); 121 | 122 | blob_hashes.append( 123 | sub_blobs 124 | .iter() 125 | .map(|(path, blob_hash)| (PathBuf::from(entry.name.clone()).join(path), blob_hash.clone())) 126 | .collect::>() 127 | .as_mut(), 128 | ); 129 | } 130 | } 131 | blob_hashes 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use std::path::PathBuf; 138 | 139 | use crate::{ 140 | models::*, 141 | utils::{test, util}, 142 | }; 143 | 144 | #[test] 145 | fn test_new() { 146 | test::setup_with_clean_mit(); 147 | let index = Index::get_instance(); 148 | for test_file in ["b.txt", "mit_src/a.txt", "test/test.txt"] { 149 | let test_file = PathBuf::from(test_file); 150 | test::ensure_file(&test_file, None); 151 | index.add(test_file.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&test_file)), &test_file)); 152 | } 153 | 154 | let tree = Tree::new(index); 155 | assert!(tree.entries.len() == 3); 156 | assert_eq!(tree.hash.is_empty(), false); 157 | } 158 | 159 | #[test] 160 | fn test_load() { 161 | test::setup_with_clean_mit(); 162 | let index = Index::get_instance(); 163 | let test_files = vec!["b.txt", "mit_src/a.txt"]; 164 | for test_file in test_files.clone() { 165 | let test_file = PathBuf::from(test_file); 166 | test::ensure_file(&test_file, None); 167 | index.add(test_file.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&test_file)), &test_file)); 168 | } 169 | 170 | let tree = Tree::new(index); 171 | let tree_hash = tree.get_hash(); 172 | 173 | let loaded_tree = Tree::load(&tree_hash); 174 | assert!(loaded_tree.entries.len() == tree.entries.len()); 175 | assert!(tree.entries[0].name == loaded_tree.entries[0].name); 176 | assert!(tree.entries[1].name == loaded_tree.entries[1].name); 177 | } 178 | 179 | #[test] 180 | fn test_get_recursive_blobs() { 181 | test::setup_with_clean_mit(); 182 | let index = Index::get_instance(); 183 | let test_files = vec!["b.txt", "mit_src/a.txt"]; 184 | let mut test_blobs = vec![]; 185 | for test_file in test_files.clone() { 186 | let test_file = PathBuf::from(test_file); 187 | test::ensure_file(&test_file, None); 188 | let blob = Blob::new(util::read_workfile(&test_file)); 189 | test_blobs.push(blob.clone()); 190 | index.add(test_file.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&test_file)), &test_file)); 191 | } 192 | 193 | let tree = Tree::new(index); 194 | let tree_hash = tree.get_hash(); 195 | 196 | let loaded_tree = Tree::load(&tree_hash); 197 | let blobs = loaded_tree.get_recursive_blobs(); 198 | assert!(blobs.len() == test_files.len()); 199 | assert!(blobs.contains(&(PathBuf::from(test_files[0]), test_blobs[0].get_hash()))); 200 | assert!(blobs.contains(&(PathBuf::from(test_files[1]), test_blobs[1].get_hash()))); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/models/index.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::path_ext::PathExt; 2 | use crate::{models::*, utils::util}; 3 | use once_cell::unsync::Lazy; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | collections::HashMap, 7 | fs, 8 | path::{Path, PathBuf}, 9 | time::SystemTime, 10 | }; 11 | 12 | // 文件元数据结构 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct FileMetaData { 15 | pub hash: Hash, // SHA-1 哈希值 16 | pub size: u64, // 文件大小 17 | pub created_time: SystemTime, // 创建时间 18 | pub modified_time: SystemTime, // 修改时间 19 | pub mode: String, // 文件模式 20 | } 21 | 22 | impl Default for FileMetaData { 23 | fn default() -> Self { 24 | FileMetaData { 25 | hash: Default::default(), 26 | size: Default::default(), 27 | created_time: SystemTime::now(), // 或者使用 UNIX_EPOCH 28 | modified_time: SystemTime::now(), // 或者使用 UNIX_EPOCH 29 | mode: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | impl FileMetaData { 35 | pub fn new(blob: &Blob, file: &Path) -> FileMetaData { 36 | let meta = file.metadata().unwrap(); 37 | FileMetaData { 38 | hash: blob.get_hash(), 39 | size: meta.len(), 40 | created_time: meta.created().unwrap(), 41 | modified_time: meta.modified().unwrap(), 42 | mode: util::get_file_mode(file), 43 | } 44 | } 45 | } 46 | 47 | /** Index 48 | 注意:逻辑处理均为绝对路径,但是存储时为相对路径(to workdir)
49 | 理解 Git index 文件 50 | */ 51 | #[derive(Serialize, Deserialize, Debug, Default)] 52 | pub struct Index { 53 | entries: HashMap, 54 | working_dir: PathBuf, 55 | } 56 | 57 | impl Index { 58 | /// 从index文件加载 59 | fn new() -> Index { 60 | let mut index = Index::default(); 61 | index.load(); 62 | index 63 | } 64 | 65 | /// 单例模式,线程不安全,但是本程序默认单线程 66 | pub fn get_instance() -> &'static mut Index { 67 | static mut INSTANCE: Lazy = Lazy::new(Index::new); //延迟初始化,线程不安全 68 | unsafe { &mut INSTANCE } 69 | } 70 | 71 | /// 重置index,主要用于测试,防止单例模式的影响 72 | #[cfg(test)] 73 | pub fn reload() { 74 | let index = Index::get_instance(); 75 | index.load(); 76 | } 77 | 78 | /// 预处理路径,统一形式为绝对路径 79 | fn preprocess(path: &Path) -> PathBuf { 80 | path.to_absolute() 81 | } 82 | 83 | // 添加文件 84 | pub fn add(&mut self, mut path: PathBuf, data: FileMetaData) { 85 | path = Index::preprocess(&path); 86 | self.entries.insert(path, data); 87 | } 88 | 89 | // 删除文件 90 | pub fn remove(&mut self, path: &Path) { 91 | let path = Index::preprocess(path); 92 | self.entries.remove(&path); 93 | } 94 | 95 | // 获取文件元数据 96 | pub fn get(&self, path: &Path) -> Option { 97 | let path = Index::preprocess(path); 98 | self.entries.get(&path).cloned() 99 | } 100 | 101 | pub fn get_hash(&self, file: &Path) -> Option { 102 | Option::from(self.get(file)?.hash.clone()) 103 | } 104 | 105 | /// 验证文件的hash是否与index中的一致 106 | pub fn verify_hash(&self, file: &Path, hash: &Hash) -> bool { 107 | &self.get_hash(file).unwrap_or_default() == hash 108 | } 109 | 110 | pub fn contains(&self, path: &Path) -> bool { 111 | let path = Index::preprocess(path); 112 | self.entries.contains_key(&path) 113 | } 114 | 115 | /// 检查文件是否被跟踪, same as [Index::contains] 116 | pub fn tracked(&self, path: &Path) -> bool { 117 | self.contains(path) 118 | } 119 | 120 | // /// 与暂存区比较,获取工作区中被删除的文件 121 | // pub fn get_deleted_files(&self, dir: &Path) -> Vec { 122 | // let mut files = Vec::new(); 123 | // self.entries.keys().for_each(|file| { 124 | // if !file.exists() && util::is_sub_path(file, dir) { 125 | // files.push(file.clone()); 126 | // } 127 | // }); 128 | // files 129 | // } 130 | 131 | /// 与暂存区比较,确定文件自上次add以来是否被编辑(内容不一定修改,还需要算hash) 132 | pub fn is_modified(&self, file: &Path) -> bool { 133 | if let Some(self_data) = self.get(file) { 134 | if let Ok(meta) = file.metadata() { 135 | let same = self_data.created_time == meta.created().unwrap_or(SystemTime::now()) 136 | && self_data.modified_time == meta.modified().unwrap_or(SystemTime::now()) 137 | && self_data.size == meta.len(); 138 | 139 | !same 140 | } else { 141 | true 142 | } 143 | } else { 144 | true 145 | } 146 | } 147 | 148 | pub fn update(&mut self, mut path: PathBuf, data: FileMetaData) { 149 | path = Index::preprocess(&path); 150 | self.entries.insert(path, data); 151 | } 152 | 153 | /// 从index文件加载数据 154 | fn load(&mut self) { 155 | self.entries.clear(); 156 | self.working_dir = util::get_working_dir().unwrap(); 157 | 158 | let path = Index::get_path(); 159 | if path.exists() { 160 | let json = fs::read_to_string(path).expect("无法读取index"); 161 | let relative_index: HashMap = serde_json::from_str(&json).expect("无法解析index"); 162 | self.entries = relative_index 163 | .into_iter() 164 | .map(|(path, value)| { 165 | let abs_path = self.working_dir.join(path); 166 | (abs_path, value) 167 | }) 168 | .collect(); 169 | } else { 170 | // println!("index文件不存在,创建空index"); 171 | } 172 | } 173 | 174 | /// 获取.mit/index文件绝对路径 175 | pub fn get_path() -> PathBuf { 176 | let mut path = util::get_storage_path().unwrap(); 177 | path.push("index"); 178 | path 179 | } 180 | 181 | /// 保存到文件 182 | pub fn save(&mut self) { 183 | //要先转化为相对路径 184 | let relative_index: HashMap = self 185 | .entries 186 | .iter() 187 | .map(|(path, value)| { 188 | let relative_path = util::get_relative_path_to_dir(path, &self.working_dir); 189 | (relative_path, value.clone()) 190 | }) 191 | .collect(); 192 | let json = serde_json::to_string_pretty(&relative_index).unwrap(); 193 | 194 | fs::write(Index::get_path(), json).expect("无法写入index"); 195 | } 196 | 197 | /** 获取跟踪的文件列表 */ 198 | pub fn get_tracked_files(&self) -> Vec { 199 | self.entries.keys().cloned().collect() 200 | } 201 | 202 | pub fn get_tracked_entries(&self) -> HashMap { 203 | self.entries.clone() 204 | } 205 | 206 | #[cfg(test)] 207 | fn is_empty(&self) -> bool { 208 | self.entries.is_empty() 209 | } 210 | } 211 | 212 | #[cfg(test)] 213 | mod tests { 214 | use super::*; 215 | use crate::utils::test; 216 | use std::fs; 217 | 218 | #[test] 219 | fn test_meta_get() { 220 | test::setup_with_clean_mit(); 221 | let metadata = fs::metadata(".mit/HEAD").unwrap(); 222 | println!("{:?}", util::format_time(&metadata.created().unwrap())); 223 | println!("{:?}", util::format_time(&metadata.modified().unwrap())); 224 | println!("{:?}", metadata.len()); 225 | } 226 | 227 | #[test] 228 | fn test_load() { 229 | test::setup_with_clean_mit(); 230 | let index = Index::get_instance(); 231 | println!("{:?}", index); 232 | } 233 | 234 | #[test] 235 | fn test_save() { 236 | test::setup_with_clean_mit(); 237 | let index = Index::get_instance(); 238 | let path = PathBuf::from("../mit_test_storage/.mit/HEAD"); //测试../相对路径的处理 239 | index.add(path.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&path)), &path)); 240 | 241 | let 中文路径 = "中文路径.txt"; 242 | test::ensure_file(Path::new(中文路径), None); 243 | let path = PathBuf::from(中文路径); 244 | index.add(path.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&path)), &path)); 245 | index.save(); 246 | println!("{:?}", index.entries); 247 | } 248 | 249 | #[test] 250 | fn test_save_load() { 251 | test::setup_with_empty_workdir(); 252 | let index = Index::get_instance(); 253 | let path = PathBuf::from(".mit/HEAD"); 254 | index.add(path.clone(), FileMetaData::new(&Blob::new(util::read_workfile(&path)), &path)); 255 | assert!(Index::new().is_empty()); //未保存前,新读取的index应该是空的 256 | index.save(); 257 | assert!(!Index::new().is_empty()); //保存后,新读取的index不是空的 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/commands/status.rs: -------------------------------------------------------------------------------- 1 | use crate::models::head; 2 | use crate::utils::path_ext::PathExt; 3 | use crate::{ 4 | models::{Blob, Commit, Index}, 5 | utils::util, 6 | }; 7 | use colored::Colorize; 8 | use std::path::PathBuf; 9 | 10 | /** 获取需要commit的更改(staged) 11 | 注:相对路径(to workdir) 12 | */ 13 | #[derive(Debug, Default, Clone)] 14 | pub struct Changes { 15 | pub new: Vec, 16 | pub modified: Vec, 17 | pub deleted: Vec, 18 | } 19 | 20 | impl Changes { 21 | pub fn is_empty(&self) -> bool { 22 | self.new.is_empty() && self.modified.is_empty() && self.deleted.is_empty() 23 | } 24 | 25 | /// 使用paths过滤,返回绝对路径 26 | pub fn filter_abs(&self, paths: &Vec) -> Changes { 27 | let mut change = Changes::default(); 28 | let abs_self = self.to_absolute(); //先要转换为绝对路径 29 | change.new = util::filter_to_fit_paths(&abs_self.new, paths); 30 | change.modified = util::filter_to_fit_paths(&abs_self.modified, paths); 31 | change.deleted = util::filter_to_fit_paths(&abs_self.deleted, paths); 32 | change 33 | } 34 | 35 | /// 使用paths过滤,返回相对路径(to cur_dir) 36 | ///
注意,如果paths为空,则返回空 37 | pub fn filter_relative(&self, paths: &Vec) -> Changes { 38 | self.filter_abs(paths).to_relative_from_abs() 39 | } 40 | 41 | /// 转换为绝对路径(from workdir相对路径) 42 | pub fn to_absolute(&self) -> Changes { 43 | let mut change = self.clone(); 44 | // change.new = util::map(&self.new, |p| util::to_workdir_absolute_path(p)); 45 | // change.modified = util::map(&self.modified, |p| util::to_workdir_absolute_path(p)); 46 | // change.deleted = util::map(&self.deleted, |p| util::to_workdir_absolute_path(p)); 47 | //离谱子 48 | [&mut change.new, &mut change.modified, &mut change.deleted] 49 | .iter_mut() 50 | .for_each(|paths| { 51 | **paths = util::map(&**paths, |p| p.to_absolute_workdir()); 52 | }); 53 | change 54 | } 55 | 56 | /// 转换为相对路径(to cur_dir)注意:要先转换为绝对路径 57 | fn to_relative_from_abs(&self) -> Changes { 58 | let mut change = self.clone(); 59 | let cur_dir = util::cur_dir(); 60 | [&mut change.new, &mut change.modified, &mut change.deleted] 61 | .iter_mut() 62 | .for_each(|paths| { 63 | **paths = util::map(&**paths, |p| util::get_relative_path_to_dir(p, &cur_dir)); 64 | }); 65 | change 66 | } 67 | 68 | ///转换为相对路径(to cur_dir) 69 | pub fn to_relative(&self) -> Changes { 70 | self.to_absolute().to_relative_from_abs() 71 | } 72 | } 73 | 74 | /** 比较暂存区与HEAD(最后一次Commit::Tree)的差异 75 | 注:相对路径(to workdir) 76 | */ 77 | pub fn changes_to_be_committed() -> Changes { 78 | let mut change = Changes::default(); 79 | let index = Index::get_instance(); 80 | let head_hash = head::current_head_commit(); 81 | let tracked_files = index 82 | .get_tracked_files() 83 | .iter() 84 | .map(|f| f.to_relative_workdir()) 85 | .collect::>(); 86 | if head_hash.is_empty() { 87 | // 初始提交 88 | change.new = tracked_files; 89 | return change; 90 | } 91 | 92 | let commit = Commit::load(&head_hash); 93 | let tree = commit.get_tree(); 94 | let tree_files = tree.get_recursive_blobs(); //相对路径 95 | let index_files: Vec = tracked_files; 96 | 97 | for (tree_file, blob_hash) in tree_files.iter() { 98 | let index_file = index_files.iter().find(|&f| f == tree_file); 99 | if let Some(index_file) = index_file { 100 | let index_path = index_file.to_absolute_workdir(); 101 | if !index.verify_hash(&index_path, blob_hash) { 102 | change.modified.push(tree_file.clone()); 103 | } 104 | } else { 105 | change.deleted.push(tree_file.clone()); 106 | } 107 | } 108 | for index_file in index_files.iter() { 109 | let tree_item = tree_files.iter().find(|f| f.0 == *index_file); 110 | if tree_item.is_none() { 111 | change.new.push(index_file.clone()); 112 | } 113 | } 114 | change 115 | } 116 | 117 | /// 比较工作区与暂存区的差异,返回相对路径(to workdir),不筛选 118 | pub fn changes_to_be_staged() -> Changes { 119 | let mut change = Changes::default(); 120 | let index = Index::get_instance(); 121 | for file in index.get_tracked_files() { 122 | if !file.exists() { 123 | change.deleted.push(file.to_relative_workdir()); 124 | } else if index.is_modified(&file) { 125 | // 若文件元数据被修改,才需要比较暂存区与文件的hash来判别内容修改 126 | let dry_blob = Blob::dry_new(util::read_workfile(&file)); 127 | if !index.verify_hash(&file, &dry_blob.get_hash()) { 128 | change.modified.push(file.to_relative_workdir()); 129 | } 130 | } 131 | } 132 | let files = util::list_workdir_files(); // all the files 133 | for file in files { 134 | if !index.tracked(&file) { 135 | //文件未被跟踪 136 | change.new.push(file.to_relative_workdir()); 137 | } 138 | } 139 | change 140 | } 141 | 142 | /** 分为两个部分 143 | 1. unstaged: 暂存区与工作区比较 144 | 2. staged to be committed: 暂存区与HEAD(最后一次Commit::Tree)比较,即上次的暂存区 145 | */ 146 | pub fn status() { 147 | util::check_repo_exist(); 148 | match head::current_head() { 149 | head::Head::Detached(commit) => { 150 | println!("HEAD detached at {}", &commit[0..7]); 151 | } 152 | head::Head::Branch(branch) => { 153 | println!("On branch {}", branch); 154 | } 155 | } 156 | 157 | // 对当前目录进行过滤 & 转换为相对路径 158 | let staged = changes_to_be_committed().to_relative(); 159 | let unstaged = changes_to_be_staged().to_relative(); 160 | if staged.is_empty() && unstaged.is_empty() { 161 | println!("nothing to commit, working tree clean"); 162 | return; 163 | } 164 | 165 | if !staged.is_empty() { 166 | println!("Changes to be committed:"); 167 | println!(" use \"mit restore --staged ...\" to unstage"); 168 | staged.deleted.iter().for_each(|f| { 169 | let str = format!("\tdeleted: {}", f.display()); 170 | println!("{}", str.bright_green()); 171 | }); 172 | staged.modified.iter().for_each(|f| { 173 | let str = format!("\tmodified: {}", f.display()); 174 | println!("{}", str.bright_green()); 175 | }); 176 | staged.new.iter().for_each(|f| { 177 | let str = format!("\tnew file: {}", f.display()); 178 | println!("{}", str.bright_green()); 179 | }); 180 | } 181 | 182 | if !unstaged.deleted.is_empty() || !unstaged.modified.is_empty() { 183 | println!("Changes not staged for commit:"); 184 | println!(" use \"mit add ...\" to update what will be committed"); 185 | unstaged.deleted.iter().for_each(|f| { 186 | let str = format!("\tdeleted: {}", f.display()); 187 | println!("{}", str.bright_red()); 188 | }); 189 | unstaged.modified.iter().for_each(|f| { 190 | let str = format!("\tmodified: {}", f.display()); 191 | println!("{}", str.bright_red()); 192 | }); 193 | } 194 | if !unstaged.new.is_empty() { 195 | println!("Untracked files:"); 196 | println!(" use \"mit add ...\" to include in what will be committed"); 197 | unstaged.new.iter().for_each(|f| { 198 | let str = format!("\t{}", f.display()); 199 | println!("{}", str.bright_red()); 200 | }); 201 | } 202 | } 203 | 204 | #[cfg(test)] 205 | mod tests { 206 | use super::*; 207 | use crate::{commands as cmd, utils::test}; 208 | use std::path::Path; 209 | 210 | #[test] 211 | fn test_changes_to_be_committed() { 212 | test::setup_with_empty_workdir(); 213 | let test_file = "a.txt"; 214 | test::ensure_file(Path::new(test_file), None); 215 | 216 | cmd::commit("test commit".to_string(), true); 217 | cmd::add(vec![test_file.to_string()], false, false); 218 | let change = changes_to_be_committed(); 219 | assert_eq!(change.new.len(), 1); 220 | assert_eq!(change.modified.len(), 0); 221 | assert_eq!(change.deleted.len(), 0); 222 | 223 | println!("{:?}", change.to_absolute()); 224 | 225 | cmd::commit("test commit".to_string(), true); 226 | test::ensure_file(Path::new(test_file), Some("new content")); 227 | cmd::add(vec![test_file.to_string()], false, false); 228 | let change = changes_to_be_committed(); 229 | assert_eq!(change.new.len(), 0); 230 | assert_eq!(change.modified.len(), 1); 231 | assert_eq!(change.deleted.len(), 0); 232 | 233 | println!("{:?}", change); 234 | 235 | cmd::commit("test commit".to_string(), true); 236 | let _ = cmd::rm(vec![test_file.to_string()], false, false); 237 | let change = changes_to_be_committed(); 238 | assert_eq!(change.new.len(), 0); 239 | assert_eq!(change.modified.len(), 0); 240 | assert_eq!(change.deleted.len(), 1); 241 | 242 | println!("{:?}", change); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/commands/restore.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | fs, 4 | path::PathBuf, 5 | }; 6 | 7 | use crate::utils::path_ext::PathExt; 8 | use crate::{ 9 | models::*, 10 | utils::{store, util}, 11 | }; 12 | 13 | fn restore_to_file(hash: &Hash, path: &PathBuf) { 14 | let blob = Blob::load(hash); 15 | util::write_workfile(blob.get_content(), path); 16 | } 17 | 18 | /// 统计[工作区]中相对于target_blobs已删除的文件(根据filters进行过滤) 19 | fn get_worktree_deleted_files_in_filters( 20 | filters: &Vec, 21 | target_blobs: &HashMap, 22 | ) -> HashSet { 23 | target_blobs //统计所有目录中(包括None & '.'),删除的文件 24 | .iter() 25 | .filter(|(path, _)| { 26 | assert!(path.is_absolute()); // 27 | !path.exists() && path.include_in(filters) 28 | }) 29 | .map(|(path, _)| path.clone()) 30 | .collect() //HashSet自动去重 31 | } 32 | 33 | /// 统计[暂存区index]中相对于target_blobs已删除的文件(根据filters进行过滤) 34 | fn get_index_deleted_files_in_filters( 35 | index: &Index, 36 | filters: &Vec, 37 | target_blobs: &HashMap, 38 | ) -> HashSet { 39 | target_blobs //统计index中相对target已删除的文件,且包含在指定dir内 40 | .iter() 41 | .filter(|(path, _)| { 42 | assert!(path.is_absolute()); 43 | !index.contains(path) && path.include_in(filters) 44 | }) 45 | .map(|(path, _)| path.clone()) 46 | .collect() //HashSet自动去重 47 | } 48 | 49 | /// 将None转化为workdir 50 | fn preprocess_filters(filters: Option<&Vec>) -> Vec { 51 | if let Some(filter) = filters { 52 | filter.clone() 53 | } else { 54 | vec![util::get_working_dir().unwrap()] //None == all(workdir), '.' == cur_dir 55 | } 56 | } 57 | 58 | /// 转化为绝对路径(to workdir)的HashMap 59 | fn preprocess_blobs(blobs: &[(PathBuf, Hash)]) -> HashMap { 60 | blobs // 转为绝对路径 //TODO tree改变路径表示方式后,这里需要修改 61 | .iter() 62 | .map(|(path, hash)| (path.to_absolute_workdir(), hash.clone())) 63 | .collect() //to HashMap 64 | } 65 | 66 | /** 根据filter restore workdir */ 67 | pub fn restore_worktree(filter: Option<&Vec>, target_blobs: &[(PathBuf, Hash)]) { 68 | let input_paths = preprocess_filters(filter); //预处理filter 将None转化为workdir 69 | let target_blobs = preprocess_blobs(target_blobs); //预处理target_blobs 转化为绝对路径HashMap 70 | 71 | let deleted_files = get_worktree_deleted_files_in_filters(&input_paths, &target_blobs); //统计已删除的文件 72 | 73 | let mut file_paths = util::integrate_paths(&input_paths); //根据用户输入整合存在的文件(绝对路径) 74 | file_paths.extend(deleted_files); //已删除的文件 75 | 76 | let index = Index::get_instance(); 77 | 78 | for path in &file_paths { 79 | assert!(path.is_absolute()); // 绝对路径 80 | if !path.exists() { 81 | //文件不存在于workdir 82 | if target_blobs.contains_key(path) { 83 | //文件存在于target_commit (deleted),需要恢复 84 | restore_to_file(&target_blobs[path], path); 85 | } else { 86 | //在target_commit和workdir中都不存在(非法路径), 用户输入 87 | println!("fatal: pathspec '{}' did not match any files", path.display()); 88 | } 89 | } else { 90 | //文件存在,有两种情况:1.修改 2.新文件 91 | if target_blobs.contains_key(path) { 92 | //文件已修改(modified) 93 | let dry_blob = Blob::dry_new(util::read_workfile(path)); //TODO tree没有存修改时间,所以这里只能用hash判断 94 | if dry_blob.get_hash() != target_blobs[path] { 95 | restore_to_file(&target_blobs[path], path); 96 | } 97 | } else { 98 | //新文件,也分两种情况:1.已跟踪,需要删除 2.未跟踪,保留 99 | if index.tracked(path) { 100 | //文件已跟踪 101 | fs::remove_file(path).unwrap(); 102 | util::clear_empty_dir(path); // 级联删除 清理空目录 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | /** 根据filter restore staged */ 110 | pub fn restore_index(filter: Option<&Vec>, target_blobs: &[(PathBuf, Hash)]) { 111 | let input_paths = preprocess_filters(filter); //预处理filter 将None转化为workdir 112 | let target_blobs = preprocess_blobs(target_blobs); //预处理target_blobs 转化为绝对路径HashMap 113 | 114 | let index = Index::get_instance(); 115 | let deleted_files_index = get_index_deleted_files_in_filters(index, &input_paths, &target_blobs); //统计已删除的文件 116 | 117 | //1.获取index中包含于input_path的文件(使用paths进行过滤) 118 | let mut file_paths: HashSet = util::filter_to_fit_paths(&index.get_tracked_files(), &input_paths); 119 | 120 | // 2.补充index中已删除的文件(相较于target_blobs) 121 | file_paths.extend(deleted_files_index); //已删除的文件 122 | 123 | for path in &file_paths { 124 | assert!(path.is_absolute()); // 绝对路径 125 | if !index.contains(path) { 126 | //文件不存在于index 127 | if target_blobs.contains_key(path) { 128 | //文件存在于target_commit (deleted),需要恢复 129 | index.add(path.clone(), FileMetaData { hash: target_blobs[path].clone(), ..Default::default() }); 130 | } else { 131 | //在target_commit和index中都不存在(非法路径) 132 | println!("fatal: pathspec '{}' did not match any files", path.display()); 133 | } 134 | } else { 135 | //文件存在于index,有两种情况:1.修改 2.新文件 136 | if target_blobs.contains_key(path) { 137 | if !index.verify_hash(path, &target_blobs[path]) { 138 | //文件已修改(modified) 139 | index.update(path.clone(), FileMetaData { hash: target_blobs[path].clone(), ..Default::default() }); 140 | } 141 | } else { 142 | //新文件 需要从index中删除 143 | index.remove(path); 144 | } 145 | } 146 | } 147 | index.save(); 148 | } 149 | /** 150 | 对于工作区中的新文件,若已跟踪,则删除;若未跟踪,则保留
151 | 对于暂存区中被删除的文件,同样会恢复
152 | 注意:不会删除空文件夹 153 | */ 154 | pub fn restore(paths: Vec, source: Option, worktree: bool, staged: bool) { 155 | let paths = paths.iter().map(PathBuf::from).collect::>(); 156 | let target_commit: Hash = { 157 | match source { 158 | None => { 159 | /*If `--source` not specified, the contents are restored from `HEAD` if `--staged` is given, 160 | otherwise from the [index].*/ 161 | if staged { 162 | head::current_head_commit() // `HEAD` 163 | } else { 164 | Hash::default() //index 165 | } 166 | } 167 | Some(ref src) => { 168 | if src == "HEAD" { 169 | //Default Source 170 | head::current_head_commit() // "" if not exist 171 | } else if head::list_local_branches().contains(src) { 172 | // Branch Name, e.g. master 173 | head::get_branch_head(src) // "" if not exist 174 | } else { 175 | // [Commit Hash, e.g. a1b2c3d4] || [Wrong Branch Name] 176 | let store = store::Store::new(); 177 | let commit = store.search(src); 178 | if commit.is_none() || !util::is_typeof_commit(commit.clone().unwrap()) { 179 | println!("fatal: 非法的 commit hash: '{}'", src); 180 | return; 181 | } 182 | commit.unwrap() 183 | } 184 | } 185 | } 186 | }; 187 | 188 | let target_blobs = { 189 | /*If `--source` not specified, the contents are restored from `HEAD` if `--staged` is given, 190 | otherwise from the [index].*/ 191 | if source.is_none() && !staged { 192 | // 没有指定source,且没有指定--staged,从[index]中恢复到worktree //只有这种情况是从[index]恢复 193 | let entries = Index::get_instance().get_tracked_entries(); 194 | entries.into_iter().map(|(p, meta)| (p, meta.hash)).collect() 195 | } else { 196 | //从[target_commit]中恢复 197 | if target_commit.is_empty() { 198 | //target_commit不存在 无法从目标恢复 199 | if source.is_some() { 200 | // 如果指定了source,说明source解析失败,报错 201 | println!("fatal: could not resolve {}", source.unwrap()); 202 | return; 203 | } 204 | Vec::new() //否则使用[空]来恢复 代表default status 205 | } else { 206 | //target_commit存在,最正常的情况,谢天谢地 207 | let tree = Commit::load(&target_commit).get_tree(); 208 | tree.get_recursive_blobs() // 相对路径 209 | } 210 | } 211 | }; 212 | // 分别处理worktree和staged 213 | if worktree { 214 | restore_worktree(Some(&paths), &target_blobs); 215 | } 216 | if staged { 217 | restore_index(Some(&paths), &target_blobs); 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod test { 223 | use std::fs; 224 | //TODO 写测试! 225 | use crate::{commands as cmd, commands::status, models::Index, utils::test}; 226 | use std::path::PathBuf; 227 | 228 | #[test] 229 | fn test_restore_stage() { 230 | test::setup_with_empty_workdir(); 231 | let path = PathBuf::from("a.txt"); 232 | test::ensure_no_file(&path); 233 | cmd::add(vec![], true, false); //add -A 234 | cmd::restore(vec![".".to_string()], Some("HEAD".to_string()), false, true); 235 | let index = Index::get_instance(); 236 | assert!(index.get_tracked_files().is_empty()); 237 | } 238 | 239 | #[test] 240 | fn test_restore_worktree() { 241 | test::setup_with_empty_workdir(); 242 | let files = vec!["a.txt", "b.txt", "c.txt", "test/in.txt"]; 243 | test::ensure_files(&files); 244 | 245 | cmd::add(vec![], true, false); 246 | assert_eq!(status::changes_to_be_committed().new.len(), 4); 247 | 248 | cmd::restore(vec!["c.txt".to_string()], None, false, true); //restore c.txt --staged 249 | assert_eq!(status::changes_to_be_committed().new.len(), 3); 250 | assert_eq!(status::changes_to_be_staged().new.len(), 1); 251 | 252 | fs::remove_file("a.txt").unwrap(); //删除a.txt 253 | fs::remove_dir_all("test").unwrap(); //删除test文件夹 254 | assert_eq!(status::changes_to_be_staged().deleted.len(), 2); 255 | 256 | cmd::restore(vec![".".to_string()], None, true, false); //restore . //from index 257 | assert_eq!(status::changes_to_be_committed().new.len(), 3); 258 | assert_eq!(status::changes_to_be_staged().new.len(), 1); 259 | assert_eq!(status::changes_to_be_staged().deleted.len(), 0); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/utils/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | fs, io, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use crate::models::{commit::Commit, object::Hash, tree::Tree}; 8 | 9 | pub const ROOT_DIR: &str = ".mit"; 10 | 11 | /* tools for mit */ 12 | 13 | pub fn storage_exist() -> bool { 14 | /*检查是否存在储存库 */ 15 | let rt = get_storage_path(); 16 | rt.is_ok() 17 | } 18 | 19 | pub fn check_repo_exist() { 20 | if !storage_exist() { 21 | println!("fatal: not a mit repository (or any of the parent directories): .mit"); 22 | panic!("不是合法的mit仓库"); 23 | } 24 | } 25 | // TODO 拆分为PathExt.rs,并定义为trait,实现PathBuf的扩展方法 26 | /// 获取.mit目录路径 27 | pub fn get_storage_path() -> Result { 28 | /*递归获取储存库 */ 29 | let mut current_dir = std::env::current_dir()?; 30 | loop { 31 | let mut git_path = current_dir.clone(); 32 | git_path.push(ROOT_DIR); 33 | if git_path.exists() { 34 | return Ok(git_path); 35 | } 36 | if !current_dir.pop() { 37 | return Err(io::Error::new( 38 | io::ErrorKind::NotFound, 39 | format!("{:?} is not a git repository", std::env::current_dir()?), 40 | )); 41 | } 42 | } 43 | } 44 | 45 | /// 获取项目工作区目录, 也就是.mit的父目录 46 | pub fn get_working_dir() -> Option { 47 | get_storage_path().unwrap().parent().map(|path| path.to_path_buf()) 48 | } 49 | 50 | /// 检查文件是否在dir内(包括子文件夹), 若不存在则false 51 | pub fn is_inside_dir(file: &Path, dir: &Path) -> bool { 52 | if file.exists() { 53 | let file = get_absolute_path(file); 54 | file.starts_with(dir) 55 | } else { 56 | false 57 | } 58 | } 59 | 60 | /// 检测dir是否是file的父目录 (不论文件是否存在) dir可以是一个文件 61 | pub fn is_parent_dir(file: &Path, dir: &Path) -> bool { 62 | let file = get_absolute_path(file); 63 | let dir = get_absolute_path(dir); 64 | file.starts_with(dir) 65 | } 66 | 67 | /// 从字符串角度判断path是否是parent的子路径(不检测存在性) alias: [is_parent_dir] 68 | pub fn is_sub_path(path: &Path, parent: &Path) -> bool { 69 | is_parent_dir(path, parent) 70 | } 71 | 72 | /// 判断文件是否在paths中(包括子目录),不检查存在性 73 | ///
注意,如果paths为空,则返回false 74 | pub fn include_in_paths(path: &Path, paths: U) -> bool 75 | where 76 | T: AsRef, 77 | U: IntoIterator, 78 | { 79 | for p in paths { 80 | if is_sub_path(path, p.as_ref()) { 81 | return true; 82 | } 83 | } 84 | false 85 | } 86 | 87 | /// 过滤列表中的元素,对.iter().filter().cloned().collect()的简化 88 | pub fn filter<'a, I, O, T, F>(items: I, pred: F) -> O 89 | where 90 | T: Clone + 'a, 91 | I: IntoIterator, 92 | O: FromIterator, 93 | F: Fn(&T) -> bool, 94 | { 95 | //items可以是一个引用 96 | items.into_iter().filter(|item| pred(item)).cloned().collect::() 97 | } 98 | 99 | /// 对列表中的元素应用func,对.iter().map().collect()的简化 100 | pub fn map<'a, I, O, T, F>(items: I, func: F) -> O 101 | where 102 | T: Clone + 'a, 103 | I: IntoIterator, 104 | O: FromIterator, 105 | F: Fn(&T) -> T, 106 | { 107 | //items可以是一个引用 108 | items.into_iter().map(func).collect::() 109 | } 110 | 111 | /// 过滤列表中的元素,使其在paths中(包括子目录),不检查存在性 112 | pub fn filter_to_fit_paths(items: &Vec, paths: &Vec) -> O 113 | where 114 | T: AsRef + Clone, 115 | O: FromIterator + IntoIterator, 116 | { 117 | filter::<_, O, _, _>(items, |item| include_in_paths(item.as_ref(), paths)) 118 | .into_iter() 119 | .collect::() 120 | } 121 | 122 | /// 检查文件是否在.mit内, 若不存在则false 123 | pub fn is_inside_repo(file: &Path) -> bool { 124 | is_inside_dir(file, &get_storage_path().unwrap()) 125 | } 126 | 127 | pub fn format_time(time: &std::time::SystemTime) -> String { 128 | let datetime: chrono::DateTime = (*time).into(); 129 | datetime.format("%Y-%m-%d %H:%M:%S.%3f").to_string() 130 | } 131 | 132 | /// 递归遍历给定目录及其子目录,列出所有文件,除了.mit 133 | pub fn list_files(path: &Path) -> io::Result> { 134 | let mut files = Vec::new(); 135 | if path.is_dir() { 136 | if path.file_name().unwrap_or_default() == ROOT_DIR { 137 | // 跳过 .mit 目录 138 | return Ok(files); 139 | } 140 | for entry in fs::read_dir(path)? { 141 | let entry = entry?; 142 | let path = entry.path(); 143 | if path.is_dir() { 144 | // 递归遍历子目录 145 | files.extend(list_files(&path)?); 146 | } else { 147 | // 将文件的路径添加到列表中 148 | files.push(path); 149 | } 150 | } 151 | } 152 | Ok(files) 153 | } 154 | 155 | /** 检查一个dir是否包含.mit(考虑.mit嵌套) */ 156 | pub fn include_root_dir(dir: &Path) -> bool { 157 | // 检查子文件夹是否有ROOT_DIR 158 | if !dir.is_dir() { 159 | return false; 160 | } 161 | let path = get_absolute_path(dir); 162 | for sub_path in fs::read_dir(path).unwrap() { 163 | let sub_path = sub_path.unwrap().path(); 164 | if sub_path.file_name().unwrap() == ROOT_DIR { 165 | return true; 166 | } 167 | } 168 | false 169 | } 170 | 171 | /// 级联删除空目录,直到遇到 [工作区根目录 | 当前目录] 172 | pub fn clear_empty_dir(dir: &Path) { 173 | let mut dir = if dir.is_dir() { 174 | dir.to_path_buf() 175 | } else { 176 | dir.parent().unwrap().to_path_buf() 177 | }; 178 | // 不能删除工作区根目录 & 当前目录 179 | while !include_root_dir(&dir) && !is_cur_dir(&dir) { 180 | if is_empty_dir(&dir) { 181 | fs::remove_dir(&dir).unwrap(); 182 | } else { 183 | break; //一旦发现非空目录,停止级联删除 184 | } 185 | dir.pop(); 186 | } 187 | } 188 | 189 | pub fn is_empty_dir(dir: &Path) -> bool { 190 | if !dir.is_dir() { 191 | return false; 192 | } 193 | fs::read_dir(dir).unwrap().next().is_none() 194 | } 195 | 196 | pub fn is_cur_dir(dir: &Path) -> bool { 197 | get_absolute_path(dir) == cur_dir() 198 | } 199 | 200 | pub fn cur_dir() -> PathBuf { 201 | std::env::current_dir().unwrap() //应该是绝对路径吧 202 | } 203 | 204 | /// 列出工作区所有文件(包括子文件夹) 205 | pub fn list_workdir_files() -> Vec { 206 | if let Ok(files) = list_files(&get_working_dir().unwrap()) { 207 | files 208 | } else { 209 | Vec::new() 210 | } 211 | } 212 | 213 | /// 获取相对于dir的 规范化 相对路径(不包含../ ./) 214 | pub fn get_relative_path_to_dir(path: &Path, dir: &Path) -> PathBuf { 215 | // 先统一为绝对路径 216 | let abs_path = if path.is_relative() { 217 | get_absolute_path(path) 218 | } else { 219 | path.to_path_buf() 220 | }; 221 | // 要考虑path在dir的上级目录的情况,要输出../../xxx 222 | let common_dir = get_common_dir(&abs_path, dir); 223 | let mut rel_path = PathBuf::new(); 224 | let mut _dir = dir.to_path_buf(); 225 | while _dir != common_dir { 226 | rel_path.push(".."); 227 | _dir.pop(); 228 | } 229 | rel_path.join(abs_path.strip_prefix(common_dir).unwrap()) 230 | } 231 | 232 | /// 获取两个路径的公共目录 233 | pub fn get_common_dir(p1: &Path, p2: &Path) -> PathBuf { 234 | let p1 = get_absolute_path(p1); 235 | let p2 = get_absolute_path(p2); 236 | let mut common_dir = PathBuf::new(); 237 | for (c1, c2) in p1.components().zip(p2.components()) { 238 | if c1 == c2 { 239 | common_dir.push(c1); 240 | } else { 241 | break; 242 | } 243 | } 244 | common_dir 245 | } 246 | 247 | /// 获取相较于工作区(Working Dir)的相对路径 248 | pub fn to_workdir_relative_path(path: &Path) -> PathBuf { 249 | get_relative_path_to_dir(path, &get_working_dir().unwrap()) 250 | } 251 | 252 | /// 获取相较于当前目录的 规范化 相对路径(不包含../ ./) 253 | pub fn get_relative_path(path: &Path) -> PathBuf { 254 | get_relative_path_to_dir(path, &cur_dir()) 255 | } 256 | 257 | /// 获取相较于工作区(Working Dir)的绝对路径 258 | pub fn to_workdir_absolute_path(path: &Path) -> PathBuf { 259 | get_absolute_path_to_dir(path, &get_working_dir().unwrap()) 260 | } 261 | 262 | fn is_executable(path: &str) -> bool { 263 | #[cfg(not(target_os = "windows"))] 264 | { 265 | use std::os::unix::fs::PermissionsExt; 266 | fs::metadata(path) 267 | .map(|metadata| metadata.permissions().mode() & 0o111 != 0) 268 | .unwrap_or(false) 269 | } 270 | 271 | #[cfg(windows)] 272 | { 273 | let path = Path::new(path); 274 | match path.extension().and_then(|s| s.to_str()) { 275 | Some(ext) => ext.eq_ignore_ascii_case("exe") || ext.eq_ignore_ascii_case("bat"), 276 | None => false, 277 | } 278 | } 279 | } 280 | 281 | pub fn get_file_mode(path: &Path) -> String { 282 | // if is_executable(path.to_str().unwrap()) { 283 | // "100755".to_string() 284 | // } else { 285 | // "100644".to_string() 286 | // } 287 | if path.is_dir() { 288 | "40000".to_string() // 目录 289 | } else if is_executable(path.to_str().unwrap()) { 290 | "100755".to_string() // 可执行文件 291 | } else { 292 | "100644".to_string() 293 | } 294 | } 295 | 296 | /// 获取绝对路径(相对于目录current_dir) 不论是否存在 297 | pub fn get_absolute_path(path: &Path) -> PathBuf { 298 | get_absolute_path_to_dir(path, &std::env::current_dir().unwrap()) 299 | } 300 | 301 | /// 获取绝对路径(相对于目录dir) 不论是否存在 302 | pub fn get_absolute_path_to_dir(path: &Path, dir: &Path) -> PathBuf { 303 | if path.is_absolute() { 304 | path.to_path_buf() 305 | } else { 306 | //相对路径 307 | // 所以决定手动解析相对路径中的../ ./ 308 | let mut abs_path = dir.to_path_buf(); 309 | // 这里会拆分所有组件,所以会自动统一路径分隔符 310 | for component in path.components() { 311 | match component { 312 | std::path::Component::ParentDir => { 313 | if !abs_path.pop() { 314 | panic!("relative path parse error"); 315 | } 316 | } 317 | std::path::Component::Normal(part) => abs_path.push(part), 318 | std::path::Component::CurDir => {} 319 | _ => {} 320 | } 321 | } 322 | abs_path 323 | } 324 | } 325 | /// 整理输入的路径数组(相对、绝对、文件、目录),返回一个绝对路径的文件数组(只包含exist) 326 | pub fn integrate_paths(paths: &Vec) -> HashSet { 327 | let mut abs_paths = HashSet::new(); 328 | for path in paths { 329 | let path = get_absolute_path(path); // 统一转换为绝对路径 330 | if path.is_dir() { 331 | // 包括目录下的所有文件(子文件夹) 332 | let files = list_files(&path).unwrap(); 333 | abs_paths.extend(files); 334 | } else { 335 | abs_paths.insert(path); 336 | } 337 | } 338 | abs_paths 339 | } 340 | 341 | #[derive(Debug, PartialEq)] 342 | pub enum ObjectType { 343 | Blob, 344 | Tree, 345 | Commit, 346 | Invalid, 347 | } 348 | pub fn check_object_type(hash: Hash) -> ObjectType { 349 | let path = get_storage_path().unwrap().join("objects").join(hash); 350 | if path.exists() { 351 | let data = fs::read_to_string(path).unwrap(); //TODO store::load? 352 | let result: Result = serde_json::from_str(&data); 353 | if result.is_ok() { 354 | return ObjectType::Commit; 355 | } 356 | let result: Result = serde_json::from_str(&data); 357 | if result.is_ok() { 358 | return ObjectType::Tree; 359 | } 360 | return ObjectType::Blob; 361 | } 362 | ObjectType::Invalid 363 | } 364 | 365 | /// 判断hash对应的文件是否是commit 366 | pub fn is_typeof_commit(hash: Hash) -> bool { 367 | check_object_type(hash) == ObjectType::Commit 368 | } 369 | 370 | /// 将内容对应的文件内容(主要是blob)还原到file 371 | pub fn write_workfile(content: String, file: &PathBuf) { 372 | let mut parent = file.clone(); 373 | parent.pop(); 374 | std::fs::create_dir_all(parent).unwrap(); 375 | std::fs::write(file, content).unwrap(); 376 | } 377 | 378 | /// 从工作区读取文件内容 379 | pub fn read_workfile(file: &Path) -> String { 380 | std::fs::read_to_string(file).unwrap() 381 | } 382 | 383 | #[cfg(test)] 384 | mod tests { 385 | use crate::{ 386 | models::{blob::Blob, index::Index}, 387 | utils::{ 388 | test, 389 | util::{self, *}, 390 | }, 391 | }; 392 | 393 | #[test] 394 | fn test_get_storage_path() { 395 | let path = get_storage_path(); 396 | match path { 397 | Ok(path) => println!("{:?}", path), 398 | Err(err) => match err.kind() { 399 | std::io::ErrorKind::NotFound => println!("Not a git repository"), 400 | _ => unreachable!("Unexpected error"), 401 | }, 402 | } 403 | } 404 | 405 | #[test] 406 | fn test_integrate_paths() { 407 | let paths = ["src/utils", "../test_del.txt", "src/utils/util.rs"] 408 | .iter() 409 | .map(PathBuf::from) 410 | .collect::>(); 411 | // paths.push(PathBuf::from(".")); 412 | let abs_paths = integrate_paths(&paths); 413 | for path in abs_paths { 414 | println!("{}", path.display()); 415 | } 416 | } 417 | 418 | #[test] 419 | fn test_get_absolute_path() { 420 | let path = Path::new("./mit_test_storage/.././src\\main.rs"); 421 | let abs_path = get_absolute_path(path); 422 | println!("{:?}", abs_path); 423 | 424 | let mut cur_dir = std::env::current_dir().unwrap(); 425 | cur_dir.push("mit_test_storage"); 426 | cur_dir.pop(); 427 | cur_dir.push("src\\main.rs"); 428 | assert_eq!(abs_path, cur_dir); // 只比较组件,不比较分隔符 429 | } 430 | 431 | #[test] 432 | fn test_get_relative_path() { 433 | test::setup_with_clean_mit(); 434 | let path = Path::new("../../src\\main.rs"); 435 | let rel_path = get_relative_path_to_dir(path, &cur_dir()); 436 | println!("{:?}", rel_path); 437 | 438 | assert_eq!(rel_path, path); 439 | } 440 | 441 | #[test] 442 | fn test_to_workdir_absolute_path() { 443 | test::setup_with_clean_mit(); 444 | let path = Path::new("./src/../main.rs"); 445 | let abs_path = to_workdir_absolute_path(path); 446 | println!("{:?}", abs_path); 447 | 448 | let mut cur_dir = get_working_dir().unwrap(); 449 | cur_dir.push("main.rs"); 450 | assert_eq!(abs_path, cur_dir); 451 | } 452 | 453 | #[test] 454 | fn test_format_time() { 455 | let time = std::time::SystemTime::now(); 456 | let formatted_time = format_time(&time); 457 | println!("{:?}", time); 458 | println!("{}", formatted_time); 459 | } 460 | 461 | #[test] 462 | fn test_list_files() { 463 | test::setup_with_clean_mit(); 464 | test::ensure_file(Path::new("test/test.txt"), None); 465 | test::ensure_file(Path::new("a.txt"), None); 466 | test::ensure_file(Path::new("b.txt"), None); 467 | let files = list_files(Path::new("./")); 468 | match files { 469 | Ok(files) => { 470 | for file in files { 471 | println!("{}", file.display()); 472 | } 473 | } 474 | Err(err) => println!("{}", err), 475 | } 476 | 477 | assert_eq!(list_files(Path::new(".")).unwrap(), list_files(Path::new("./")).unwrap()); 478 | } 479 | 480 | #[test] 481 | fn test_check_object_type() { 482 | test::setup_with_clean_mit(); 483 | assert_eq!(check_object_type("123".into()), ObjectType::Invalid); 484 | test::ensure_file(Path::new("test.txt"), Some("test")); 485 | let content = util::read_workfile(get_working_dir().unwrap().join("test.txt").as_path()); 486 | let hash = Blob::new(content).get_hash(); 487 | assert_eq!(check_object_type(hash), ObjectType::Blob); 488 | let mut commit = Commit::new(Index::get_instance(), vec![], "test".to_string()); 489 | assert_eq!(check_object_type(commit.get_tree_hash()), ObjectType::Tree); 490 | commit.save(); 491 | assert_eq!(check_object_type(commit.get_hash()), ObjectType::Commit); 492 | } 493 | 494 | #[test] 495 | fn test_check_root_dir() { 496 | test::setup_with_clean_mit(); 497 | list_workdir_files().iter().for_each(|f| { 498 | fs::remove_file(f).unwrap(); 499 | }); 500 | test::list_subdir(Path::new("./")).unwrap().iter().for_each(|f| { 501 | if include_root_dir(f) { 502 | fs::remove_dir_all(f).unwrap(); 503 | } 504 | }); 505 | assert!(include_root_dir(Path::new("./"))); 506 | fs::create_dir("./src").unwrap_or_default(); 507 | assert_eq!(include_root_dir(Path::new("./src")), false); 508 | fs::create_dir("./src/.mit").unwrap_or_default(); 509 | assert!(include_root_dir(Path::new("./src"))); 510 | } 511 | } 512 | --------------------------------------------------------------------------------