From c61cc619ef69bf5606c2dc5336b93327f3a130a7 Mon Sep 17 00:00:00 2001 From: HouXiaoxuan Date: Fri, 22 Dec 2023 01:49:36 +0800 Subject: [PATCH] =?UTF-8?q?git=20branch=E5=AE=9E=E7=8E=B0&=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=8Cshow=E7=B1=BB=E5=9E=8B=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E7=BB=93=E5=90=88checkout=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.rs | 29 +++++- src/commands/branch.rs | 201 +++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 3 +- src/head.rs | 13 ++- src/store.rs | 3 + 5 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 src/commands/branch.rs diff --git a/src/cli.rs b/src/cli.rs index c064157..7317099 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ -use clap::{Parser, Subcommand}; +use clap::{ArgGroup, Parser, Subcommand}; use mit::commands::add::add; +use mit::commands::branch::branch; use mit::commands::commit::commit; use mit::commands::init::init; use mit::commands::log::log; @@ -55,6 +56,7 @@ enum Command { /// 查看当前状态 Status, /// log 现实提交历史 + #[clap(group = ArgGroup::new("sub").required(false))] Log { #[clap(short = 'A', long)] all: bool, @@ -62,6 +64,28 @@ enum Command { #[clap(short, long)] number: Option, }, + /// branch + Branch { + /// 新分支名 + #[clap(group = "sub")] + new_branch: Option, + + /// 基于某个commit创建分支 + #[clap(requires = "new_branch")] + commit_hash: Option, + + /// 列出所有分支 + #[clap(short, long, action, group = "sub", default_value = "true")] + list: bool, + + /// 删除制定分支,不能删除当前所在分支 + #[clap(short = 'D', long, group = "sub")] + delete: Option, + + /// 显示当前分支 + #[clap(long, action, group = "sub")] + show_current: bool, + }, } pub fn handle_command() { let cli = Cli::parse(); @@ -84,5 +108,8 @@ pub fn handle_command() { Command::Log { all, number } => { log(all, number); } + Command::Branch { list, delete, new_branch, commit_hash, show_current } => { + branch(new_branch, commit_hash, list, delete, show_current); + } } } diff --git a/src/commands/branch.rs b/src/commands/branch.rs new file mode 100644 index 0000000..985abd0 --- /dev/null +++ b/src/commands/branch.rs @@ -0,0 +1,201 @@ +use colored::Colorize; + +use crate::{ + head, + models::{commit::Commit, object::Hash}, + store, +}; + +// branch error +enum BranchErr { + BranchExist, + InvalidObject, + + BranchNoExist, + BranchCheckedOut, +} +// 从分支名、commit hash中搜索commit +fn search_hash(commit_hash: Hash) -> Option { + // 分支名 + if head::list_local_branches().contains(&commit_hash) { + let commit_hash = head::get_branch_head(&commit_hash); + return Some(commit_hash); + } + // commit hash + let store = store::Store::new(); + let commit = store.search(&commit_hash); + commit +} + +fn create_branch(branch_name: String, _base_commit: Hash) -> Result<(), BranchErr> { + // 找到正确的base_commit_hash + let base_commit = search_hash(_base_commit.clone()); + if base_commit.is_none() { + println!("fatal: 非法的 commit: '{}'", _base_commit); + return Err(BranchErr::InvalidObject); + } + + let base_commit = Commit::load(&base_commit.unwrap()); // TODO 这里会直接panic,可以优化一下错误处理流程 + + let exist_branchs = head::list_local_branches(); + if exist_branchs.contains(&branch_name) { + println!("fatal: 分支 '{}' 已存在", branch_name); + return Err(BranchErr::BranchExist); + } + + head::update_branch(&branch_name, &base_commit.get_hash()); + Ok(()) +} + +fn delete_branch(branch_name: String) -> Result<(), BranchErr> { + let branches = head::list_local_branches(); + if !branches.contains(&branch_name) { + println!("error: 分支 '{}' 不存在", branch_name); + return Err(BranchErr::BranchNoExist); + } + + // 仅在当前分支为删除分支时,不允许删除(在历史commit上允许删除) + let current_branch = match head::current_head() { + head::Head::Branch(branch_name) => branch_name, + _ => "".to_string(), + }; + if current_branch == branch_name { + println!("error: 不能删除当前所在分支 {:?}", branch_name); + return Err(BranchErr::BranchCheckedOut); + } + + head::delete_branch(&branch_name); // 删除refs/heads/branch_name,不删除任何commit + Ok(()) +} + +fn show_current_branch() { + println!("show_current_branch"); + let head = head::current_head(); + match head { + head::Head::Branch(branch_name) => println!("{}", branch_name), + _ => (), // do nothing + } +} + +fn list_branches() { + println!("list_branches"); + let branches = head::list_local_branches(); + match head::current_head() { + head::Head::Branch(branch_name) => { + println!("* {}", branch_name.green()); + for branch in branches { + if branch != branch_name { + println!(" {}", branch); + } + } + } + head::Head::Detached(commit_hash) => { + println!("* (HEAD detached at {}) {}", commit_hash.green(), commit_hash[0..7].green()); + for branch in branches { + println!(" {}", branch); + } + } + } +} + +pub fn branch( + new_branch: Option, + commit_hash: Option, + list: bool, + delete: Option, + show_current: bool, +) { + if new_branch.is_some() { + let basic_commit = if commit_hash.is_some() { + commit_hash.unwrap() + } else { + head::current_head_commit() // 默认使用当前commit + }; + let _ = create_branch(new_branch.unwrap(), basic_commit); + } else if delete.is_some() { + let _ = delete_branch(delete.unwrap()); + } else if show_current { + show_current_branch(); + } else if list { + // 兜底list + list_branches(); + } else { + panic!("should not reach here") + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::commands; + use crate::utils::util; + #[test] + fn test_create_branch() { + util::setup_test_with_clean_mit(); + + // no commit: invalid object + let result = create_branch("test_branch".to_string(), head::current_head_commit()); + assert!(result.is_err()); + assert!(match result.unwrap_err() { + BranchErr::InvalidObject => true, + _ => false, + }); + assert!(head::list_local_branches().is_empty()); + + commands::commit::commit("test commit 1".to_string(), true); + let commit_hash_one = head::current_head_commit(); + commands::commit::commit("test commit 2".to_string(), true); + let commit_hash_two = head::current_head_commit(); + + // success, use part of commit hash + let new_branch_one = "test_branch".to_string() + &rand::random::().to_string(); + let result = create_branch(new_branch_one.clone(), commit_hash_one[0..7].to_string()); + assert!(result.is_ok()); + assert!(head::list_local_branches().contains(&new_branch_one), "new branch not in list"); + assert!(head::get_branch_head(&new_branch_one) == commit_hash_one, "new branch head error"); + + // branch exist + let result = create_branch(new_branch_one.clone(), commit_hash_two.clone()); + assert!(result.is_err()); + assert!(match result.unwrap_err() { + BranchErr::BranchExist => true, + _ => false, + }); + + // use branch name as commit hash, success + let new_branch_two = "test_branch".to_string() + &rand::random::().to_string(); + let result = create_branch(new_branch_two.clone(), new_branch_one.clone()); + assert!(result.is_ok()); + assert!(head::list_local_branches().contains(&new_branch_two), "new branch not in list"); + assert!(head::get_branch_head(&new_branch_two) == commit_hash_one, "new branch head error"); + } + + #[test] + fn test_delete_branch() { + util::setup_test_with_clean_mit(); + + // no commit: invalid object + let result = delete_branch("test_branch".to_string()); + assert!(result.is_err()); + assert!(match result.unwrap_err() { + BranchErr::BranchNoExist => true, + _ => false, + }); + assert!(head::list_local_branches().is_empty()); + + commands::commit::commit("test commit 1".to_string(), true); + let commit_hash = head::current_head_commit(); + + // success + let new_branch = "test_branch".to_string() + &rand::random::().to_string(); + let result = create_branch(new_branch.clone(), commit_hash.clone()); + assert!(result.is_ok()); + assert!(head::list_local_branches().contains(&new_branch), "new branch not in list"); + assert!(head::get_branch_head(&new_branch) == commit_hash, "new branch head error"); + + // branch exist + let result = delete_branch(new_branch.clone()); + assert!(result.is_ok()); + assert!(!head::list_local_branches().contains(&new_branch), "new branch not in list"); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index fc651b0..a3986cb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod commit; pub mod init; pub mod remove; pub mod status; -pub mod log; \ No newline at end of file +pub mod log; +pub mod branch; \ No newline at end of file diff --git a/src/head.rs b/src/head.rs index a0c6506..129c79f 100644 --- a/src/head.rs +++ b/src/head.rs @@ -41,6 +41,17 @@ pub fn get_branch_head(branch_name: &String) -> String { "".to_string() // 分支不存在或者没有commit } } +pub fn delete_branch(branch_name: &String) { + let mut branch = util::get_storage_path().unwrap(); + branch.push("refs"); + branch.push("heads"); + branch.push(branch_name); + if branch.exists() { + std::fs::remove_file(branch).expect("无法删除branch"); + } else { + panic!("branch file not exist"); + } +} /**返回当前head指向的commit hash,如果是分支,则返回分支的commit hash*/ pub fn current_head_commit() -> String { @@ -69,7 +80,6 @@ pub fn update_head_commit(commit_hash: &String) { } } - /** 列出本地的branch */ pub fn list_local_branches() -> Vec { let mut branches = Vec::new(); @@ -96,7 +106,6 @@ pub fn change_head_to_branch(branch_name: &String) { update_head_commit(&branch_head); } - /** 切换head到非branchcommit */ pub fn change_head_to_commit(commit_hash: &String) { let mut head = util::get_storage_path().unwrap(); diff --git a/src/store.rs b/src/store.rs index d0f5911..3dc6bd1 100644 --- a/src/store.rs +++ b/src/store.rs @@ -34,6 +34,9 @@ impl Store { /** 根据前缀搜索,有歧义时返回 None*/ pub fn search(&self, hash: &String) -> Option { + if hash.is_empty() { + return None; + } let objects = util::list_files(self.store_path.join("objects").as_path()).unwrap(); // 转string let objects = objects