diff --git a/libra/src/command/commit.rs b/libra/src/command/commit.rs index d278aed2a..833588525 100644 --- a/libra/src/command/commit.rs +++ b/libra/src/command/commit.rs @@ -98,7 +98,7 @@ pub async fn execute(args: CommitArgs) { } /// recursively create tree from index's tracked entries -async fn create_tree(index: &Index, storage: &ClientStorage, current_root: PathBuf) -> Tree { +pub async fn create_tree(index: &Index, storage: &ClientStorage, current_root: PathBuf) -> Tree { // blob created when add file to index let get_blob_entry = |path: &PathBuf| { let name = util::path_to_string(path); diff --git a/libra/src/command/mod.rs b/libra/src/command/mod.rs index 32cc73942..0cba9d97f 100644 --- a/libra/src/command/mod.rs +++ b/libra/src/command/mod.rs @@ -114,7 +114,7 @@ pub async fn get_target_commit(branch_or_commit: &str) -> Result 1 { return Err(format!("Ambiguous commit hash '{}'", branch_or_commit).into()); } diff --git a/libra/src/command/restore.rs b/libra/src/command/restore.rs index 1ce4e1ce6..6ac3287b0 100644 --- a/libra/src/command/restore.rs +++ b/libra/src/command/restore.rs @@ -68,7 +68,7 @@ pub async fn execute(args: RestoreArgs) { Some(Branch::find_branch(src, None).await.unwrap().commit) } else { // [Commit Hash, e.g. a1b2c3d4] || [Wrong Branch Name] - let objs = storage.search(src); + let objs = storage.search(src).await; // TODO hash can be `commit` or `tree` if objs.len() != 1 || !storage.is_object_type(&objs[0], ObjectType::Commit) { None // Wrong Commit Hash @@ -99,7 +99,7 @@ pub async fn execute(args: RestoreArgs) { tree.get_plain_items() } else { let src = source.unwrap(); - if storage.search(&src).len() != 1 { + if storage.search(&src).await.len() != 1 { eprintln!("fatal: could not resolve {}", src); } else { eprintln!("fatal: reference is not a commit: {}", src); diff --git a/libra/src/command/switch.rs b/libra/src/command/switch.rs index 90050fc67..b16dfa0e0 100644 --- a/libra/src/command/switch.rs +++ b/libra/src/command/switch.rs @@ -47,9 +47,9 @@ pub async fn execute(args: SwitchArgs) { } None => match args.detach { true => { - let commit_base = get_commit_base(&args.branch.unwrap()); + let commit_base = get_commit_base(&args.branch.unwrap()).await; if commit_base.is_err() { - eprintln!("{}", commit_base.unwrap()); + eprintln!("{:?}", commit_base.unwrap_err()); return; } switch_to_commit(commit_base.unwrap()).await; diff --git a/libra/src/internal/head.rs b/libra/src/internal/head.rs index a436dea8c..422eb66f3 100644 --- a/libra/src/internal/head.rs +++ b/libra/src/internal/head.rs @@ -83,7 +83,6 @@ impl Head { Some(remote) => Self::query_remote_head(remote).await, None => Some(Self::query_local_head().await), }; - match head { Some(head) => { // update diff --git a/libra/src/utils/client_storage.rs b/libra/src/utils/client_storage.rs index 5b15b2f94..4d4d0531c 100644 --- a/libra/src/utils/client_storage.rs +++ b/libra/src/utils/client_storage.rs @@ -1,10 +1,7 @@ -use std::collections::HashSet; -use std::io::prelude::*; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; -use std::{fs, io}; - +use crate::command; +use crate::command::load_object; +use crate::internal::branch::Branch; +use crate::internal::head::Head; use byteorder::{BigEndian, ReadBytesExt}; use flate2::read::ZlibDecoder; use flate2::write::ZlibEncoder; @@ -12,13 +9,19 @@ use flate2::Compression; use lru_mem::LruCache; use mercury::errors::GitError; use mercury::hash::SHA1; +use mercury::internal::object::commit::Commit; use mercury::internal::object::types::ObjectType; use mercury::internal::pack::cache_object::CacheObject; use mercury::internal::pack::Pack; use mercury::utils::read_sha1; use once_cell::sync::Lazy; - -use crate::command; +use regex::Regex; +use std::collections::HashSet; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::{fs, io}; static PACK_OBJ_CACHE: Lazy>> = Lazy::new(|| { // `lazy_static!` may affect IDE's code completion Mutex::new(LruCache::new(1024 * 1024 * 200)) @@ -74,7 +77,59 @@ impl ClientStorage { } /// Search objects that start with `obj_id`, loose & pack - pub fn search(&self, obj_id: &str) -> Vec { + /// add support for relative path + pub async fn search(&self, obj_id: &str) -> Vec { + if obj_id == "HEAD" { + return vec![Head::current_commit().await.unwrap()]; + } + if obj_id.contains('~') || obj_id.contains('^') { + // Find the position of the last non-~^ symbol and split into base reference and path. + let mut split_pos = 0; + let mut found_special = false; + + for (i, c) in obj_id.char_indices() { + if c == '~' || c == '^' { + found_special = true; + split_pos = i; + break; + } + } + + if found_special { + let base_ref = &obj_id[..split_pos]; + let path_part = &obj_id[split_pos..]; + + let base_commit = match base_ref { + "HEAD" => Head::current_commit().await.unwrap(), + _ => { + if let Some(branch) = Branch::find_branch(base_ref, None).await { + branch.commit + } else { + let matches: Vec = self + .list_objects_pack() + .into_iter() + .chain(self.list_objects_loose().into_iter()) + .filter(|x| self.is_object_type(x, ObjectType::Commit)) + .filter(|x| x.to_string().starts_with(base_ref)) + .collect(); + + if matches.len() == 1 { + matches[0] + } else { + return Vec::new(); + } + } + } + }; + let target_commit = match self.navigate_commit_path(base_commit, path_part) { + Ok(commit) => commit, + Err(_) => return Vec::new(), + }; + + return vec![target_commit]; + } + } + let mut objs = self.list_objects_pack(); objs.extend(self.list_objects_loose()); @@ -82,6 +137,94 @@ impl ClientStorage { .filter(|x| x.to_string().starts_with(obj_id)) .collect() } + /// Navigates through commit history following a Git-style reference path + /// For example: given "^2~3", navigate from base_commit to its second parent, + /// then follow first parent three generations up + /// + /// Parameters: + /// - base_commit: Starting commit SHA1 + /// - path: Reference path string (e.g. "^2~3", "~~~", "^~^2") + /// + fn navigate_commit_path(&self, base_commit: SHA1, path: &str) -> Result { + let mut current = base_commit; + + let re = Regex::new(r"(\^|~)(\d*)").unwrap(); + + if !re.is_match(path) { + return Err(GitError::InvalidArgument(format!( + "Invalid reference path: {}", + path + ))); + } + for cap in re.captures_iter(path) { + let symbol = cap.get(1).unwrap().as_str(); + let num_str = cap.get(2).map_or("1", |m| m.as_str()); + let num: usize = num_str.parse().unwrap_or(1); + + match symbol { + "^" => { + current = self.get_parent_commit(¤t, num)?; + } + "~" => { + for _ in 0..num { + current = self.get_parent_commit(¤t, 1)?; + } + } + _ => unreachable!(), + } + } + + Ok(current) + } + /// get the reference of HEAD, e.g. HEAD~1, HEAD^2 + #[allow(dead_code)] + async fn parse_head_reference(&self, reference: &str) -> Result { + let mut current = Head::current_commit().await.unwrap(); + + if reference == "HEAD" { + return Ok(current); + } + + let re = Regex::new(r"(\^|~)(\d*)").unwrap(); + let path = &reference[4..]; + if !re.is_match(path) { + return Err(GitError::InvalidArgument(reference.to_string())); + } + + for cap in re.captures_iter(path) { + let symbol = cap.get(1).unwrap().as_str(); + let num_str = cap.get(2).map_or("1", |m| m.as_str()); + let num: usize = num_str.parse().unwrap_or(1); + + match symbol { + "^" => { + current = self.get_parent_commit(¤t, num)?; + } + "~" => { + for _ in 0..num { + current = self.get_parent_commit(¤t, 1)?; + } + } + _ => unreachable!(), + } + } + Ok(current) + } + + /// get the nth parent commit of a commit + fn get_parent_commit(&self, commit_id: &SHA1, n: usize) -> Result { + let commit: Commit = load_object(commit_id)?; + + // the index starts from 0 + if n == 0 || n > commit.parent_commit_ids.len() { + return Err(GitError::ObjectNotFound(format!( + "Parent {} does not exist", + n + ))); + } + + Ok(commit.parent_commit_ids[n - 1]) + } /// list all objects' hash in `objects` fn list_objects_loose(&self) -> Vec { @@ -401,10 +544,10 @@ mod tests { assert_eq!(String::from_utf8(data).unwrap(), content); } - #[test] + #[tokio::test] /// Tests object search functionality by partial hash prefix. /// Verifies that objects can be correctly found when searching with a partial SHA1 hash. - fn test_search() { + async fn test_search() { let blob = Blob::from_content("Hello, world!"); let mut source = PathBuf::from(test::find_cargo_dir().parent().unwrap()); @@ -415,7 +558,7 @@ mod tests { .put(&blob.id, &blob.data, blob.get_type()) .is_ok()); - let objs = client_storage.search("5dd01c177"); + let objs = client_storage.search("5dd01c177").await; assert_eq!(objs.len(), 1); } diff --git a/libra/src/utils/util.rs b/libra/src/utils/util.rs index 9c3b12a91..79516b0fc 100644 --- a/libra/src/utils/util.rs +++ b/libra/src/utils/util.rs @@ -293,10 +293,10 @@ pub fn path_to_string(path: &Path) -> String { } /// extend hash, panic if not valid or ambiguous -pub fn get_commit_base(commit_base: &str) -> Result { +pub async fn get_commit_base(commit_base: &str) -> Result { let storage = objects_storage(); - let commits = storage.search(commit_base); + let commits = storage.search(commit_base).await; if commits.is_empty() { return Err(format!("fatal: invalid reference: {}", commit_base)); } else if commits.len() > 1 { diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index 1cc16725b..ef7bc3057 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -53,11 +53,11 @@ async fn test_execute_log() { /// create a test commit tree structure as graph and create branch (master) head to commit 6 /// return a commit hash of commit 6 -/// 3 6 -/// / \ / -/// 1 -- 2 5 -// \ / \ -/// 4 7 +/// 3 -- 6 +/// / / +/// 1 -- 2 -- 5 +// \ / \ +/// 4 7 async fn create_test_commit_tree() -> String { let mut commit_1 = Commit::from_tree_id( SHA1::new(&[1; 20]), diff --git a/libra/tests/command/switch_test.rs b/libra/tests/command/switch_test.rs index 0d76608f7..92469de4d 100644 --- a/libra/tests/command/switch_test.rs +++ b/libra/tests/command/switch_test.rs @@ -1,3 +1,7 @@ +use libra::utils::client_storage::ClientStorage; +use libra::utils::path; +use mercury::internal::index::Index; + use super::*; use std::fs; async fn test_check_status() { @@ -132,3 +136,254 @@ async fn test_parts_of_switch_module_function() { // Test the switch module funsctions test_check_status().await; } + +#[tokio::test] +#[serial] +/// Tests basic HEAD detachment capabilities with simple reference paths. +/// Validates relative references (HEAD^, HEAD~), numeric references (HEAD^1, HEAD~1), +/// and complex reference combinations (HEAD^^^, HEAD~~~, HEAD^~^~). +async fn test_detach_head_basic() { + println!("\n\x1b[1mTest detach use the head's ref basically.\x1b[0m"); + let temp_path = tempdir().unwrap(); + test::setup_with_new_libra_in(temp_path.path()).await; + let _guard = ChangeDirGuard::new(temp_path.path()); + println!("temp_path {:?}", temp_path); + + for i in 0..6 { + let args = CommitArgs { + message: format!("commit_{}", i), + allow_empty: true, + conventional: false, + amend: false, + }; + commit::execute(args).await; + } + //detach to head + { + switch_to_branch("master".to_string()).await; + + let commit_message = switch_to_detach("HEAD".to_string()).await; + assert_eq!(&commit_message, "commit_5"); + } + + //detach to the before commit + { + let commit_message = switch_to_detach("HEAD^".to_string()).await; + assert_eq!(&commit_message, "commit_4"); + } + + { + let commit_message = switch_to_detach("HEAD~".to_string()).await; + assert_eq!(&commit_message, "commit_3"); + } + { + let commit_message = switch_to_detach("HEAD^1".to_string()).await; + assert_eq!(&commit_message, "commit_2"); + } + + { + let commit_message = switch_to_detach("HEAD~1".to_string()).await; + assert_eq!(&commit_message, "commit_1"); + } + switch_to_branch("master".to_string()).await; + + for i in 6..12 { + let args = CommitArgs { + message: format!("commit_{}", i), + allow_empty: true, + conventional: false, + amend: false, + }; + commit::execute(args).await; + } + + //detach use head's ref + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("HEAD~11".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("HEAD~~~~~~~~~~~".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("HEAD^^^^^^^^^^^".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("HEAD^~^~^~^~^~^".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + //detach use branch's ref + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("master~11".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("master~~~~~~~~~~~".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("master^^^^^^^^^^^".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach("master^~^~^~^~^~^".to_string()).await; + assert_eq!(&commit_message, "commit_0"); + } + let master_commit_id = Branch::find_branch("master", None).await.unwrap().commit; + //detach use commit's ref + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach(format!("{}~11", master_commit_id)).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach(format!("{}~11", master_commit_id)).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach(format!("{}~~~~~~~~~~~", master_commit_id)).await; + assert_eq!(&commit_message, "commit_0"); + } + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach(format!("{}^^^^^^^^^^^", master_commit_id)).await; + assert_eq!(&commit_message, "commit_0"); + } + + { + switch_to_branch("master".to_string()).await; + let commit_message = switch_to_detach(format!("{}^~^~^~^~^~^", master_commit_id)).await; + assert_eq!(&commit_message, "commit_0"); + } +} + +// a tree with many parents. +async fn create_commit_tree() { + let index = Index::load(path::index()).unwrap(); + let storage = ClientStorage::init(path::objects()); + + let tree = commit::create_tree(&index, &storage, "".into()).await; + + let mut commit_1 = Commit::from_tree_id(tree.id, vec![], &format_commit_msg("commit_0", None)); + commit_1.committer.timestamp = 1; + save_object(&commit_1, &commit_1.id).unwrap(); + + let mut parents_ids = vec![]; + for i in 1..12 { + let tree = commit::create_tree(&index, &storage, "".into()).await; + + let mut commit = Commit::from_tree_id( + tree.id, + vec![commit_1.id], + &format_commit_msg(&format!("commit_{}", i), None), + ); + commit.committer.timestamp = (i + 1) as usize; + save_object(&commit, &commit.id).unwrap(); + parents_ids.push(commit.id); + } + { + let tree = commit::create_tree(&index, &storage, "".into()).await; + + let mut commit_last = Commit::from_tree_id( + tree.id, + parents_ids, + &format_commit_msg("commit_last", None), + ); + commit_last.committer.timestamp = 100; + save_object(&commit_last, &commit_last.id).unwrap(); + Branch::update_branch("master", &commit_last.id.to_string(), None).await; + } +} + +#[tokio::test] +#[serial] +// Comprehensive tests for HEAD reference navigation using Git-style paths +// Validates support for ^ (parent selection), ~ (ancestry traversal), and their combinations +async fn test_detach_head_extra() { + println!("\n\x1b[1mTest detach use the head's ref extra.\x1b[0m"); + let temp_path = tempdir().unwrap(); + test::setup_with_new_libra_in(temp_path.path()).await; + let _guard = ChangeDirGuard::new(temp_path.path()); + println!("temp_path {:?}", temp_path); + + create_commit_tree().await; + //detach to head + { + let commit_message = switch_to_detach("HEAD".to_string()).await; + assert_eq!(commit_message, "commit_last".to_string()); + } + + for i in 1..12 { + let commit_message = switch_to_detach(format!("HEAD^{}", i)).await; + assert_eq!(commit_message, format!("commit_{}", i)); + + //back to the last commit + switch_to_branch("master".to_string()).await; + } + //detach use the branch's ref + for i in 1..12 { + let commit_message = switch_to_detach(format!("master^{}", i)).await; + assert_eq!(commit_message, format!("commit_{}", i)); + + //back to the last commit + switch_to_branch("master".to_string()).await; + } + //detach use head's ref + { + let commit_message = switch_to_detach("HEAD^11~".to_string()).await; + assert_eq!(commit_message, "commit_0".to_string()); + switch_to_branch("master".to_string()).await; + } + //detach use branch's ref + { + let commit_message = switch_to_detach("master^11~".to_string()).await; + assert_eq!(commit_message, "commit_0".to_string()); + switch_to_branch("master".to_string()).await; + } + let master_commit_id = Branch::find_branch("master", None).await.unwrap().commit; + //detach use commit's ref + { + let commit_message = switch_to_detach(format!("{}^11~", master_commit_id)).await; + assert_eq!(commit_message, "commit_0".to_string()); + switch_to_branch("master".to_string()).await; + } +} + +async fn switch_to_detach(branch_test: String) -> String { + let args = SwitchArgs { + branch: Some(branch_test), + create: None, + detach: true, + }; + switch::execute(args).await; + let head = Head::current().await; + let commit_id = match head { + Head::Detached(commit) => commit, + _ => panic!("head not detached,unreachable"), + }; + let commit = load_object::(&commit_id).unwrap(); + commit.message.trim().to_string() +} + +async fn switch_to_branch(branch_test: String) { + let args = SwitchArgs { + branch: Some(branch_test), + create: None, + detach: false, + }; + switch::execute(args).await; +} diff --git a/libra/tests/mega_test.rs b/libra/tests/mega_test.rs index 79e252759..1b3d4ea2e 100644 --- a/libra/tests/mega_test.rs +++ b/libra/tests/mega_test.rs @@ -4,7 +4,6 @@ use libra::internal::protocol::lfs_client::LFSClient; use libra::internal::protocol::ProtocolClient; use libra::utils::lfs; use reqwest::Url; -/// integration tests for the mega module use std::env; use std::net::TcpStream; use std::path::PathBuf;