git branch实现&测试,show类型需要结合checkout测试

This commit is contained in:
HouXiaoxuan
2023-12-22 01:49:36 +08:00
parent df39d1e71b
commit c61cc619ef
5 changed files with 245 additions and 4 deletions

View File

@@ -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<usize>,
},
/// branch
Branch {
/// 新分支名
#[clap(group = "sub")]
new_branch: Option<String>,
/// 基于某个commit创建分支
#[clap(requires = "new_branch")]
commit_hash: Option<String>,
/// 列出所有分支
#[clap(short, long, action, group = "sub", default_value = "true")]
list: bool,
/// 删除制定分支,不能删除当前所在分支
#[clap(short = 'D', long, group = "sub")]
delete: Option<String>,
/// 显示当前分支
#[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);
}
}
}

201
src/commands/branch.rs Normal file
View File

@@ -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<Hash> {
// 分支名
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<String>,
commit_hash: Option<Hash>,
list: bool,
delete: Option<String>,
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::<u32>().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::<u32>().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::<u32>().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");
}
}

View File

@@ -3,4 +3,5 @@ pub mod commit;
pub mod init;
pub mod remove;
pub mod status;
pub mod log;
pub mod log;
pub mod branch;

View File

@@ -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<String> {
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();

View File

@@ -34,6 +34,9 @@ impl Store {
/** 根据前缀搜索,有歧义时返回 None*/
pub fn search(&self, hash: &String) -> Option<Hash> {
if hash.is_empty() {
return None;
}
let objects = util::list_files(self.store_path.join("objects").as_path()).unwrap();
// 转string
let objects = objects