├── 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 |
--------------------------------------------------------------------------------