Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libra/src/command/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion libra/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pub async fn get_target_commit(branch_or_commit: &str) -> Result<SHA1, Box<dyn s

if possible_branches.is_empty() {
let storage = util::objects_storage();
let possible_commits = storage.search(branch_or_commit);
let possible_commits = storage.search(branch_or_commit).await;
if possible_commits.len() > 1 {
return Err(format!("Ambiguous commit hash '{}'", branch_or_commit).into());
}
Expand Down
4 changes: 2 additions & 2 deletions libra/src/command/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions libra/src/command/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion libra/src/internal/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
169 changes: 156 additions & 13 deletions libra/src/utils/client_storage.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
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;
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<Mutex<LruCache<String, CacheObject>>> = Lazy::new(|| {
// `lazy_static!` may affect IDE's code completion
Mutex::new(LruCache::new(1024 * 1024 * 200))
Expand Down Expand Up @@ -74,14 +77,154 @@ impl ClientStorage {
}

/// Search objects that start with `obj_id`, loose & pack
pub fn search(&self, obj_id: &str) -> Vec<SHA1> {
/// add support for relative path
pub async fn search(&self, obj_id: &str) -> Vec<SHA1> {
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<SHA1> = 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());

objs.into_iter()
.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<SHA1, GitError> {
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(&current, num)?;
}
"~" => {
for _ in 0..num {
current = self.get_parent_commit(&current, 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<SHA1, GitError> {
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(&current, num)?;
}
"~" => {
for _ in 0..num {
current = self.get_parent_commit(&current, 1)?;
}
}
_ => unreachable!(),
}
}
Ok(current)
}

/// get the nth parent commit of a commit
fn get_parent_commit(&self, commit_id: &SHA1, n: usize) -> Result<SHA1, GitError> {
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<SHA1> {
Expand Down Expand Up @@ -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());
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions libra/src/utils/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SHA1, String> {
pub async fn get_commit_base(commit_base: &str) -> Result<SHA1, String> {
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 {
Expand Down
10 changes: 5 additions & 5 deletions libra/tests/command/log_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
Loading