From 4486195ad5b145386d27f1395d265a1a37834abf Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Mon, 25 Aug 2025 23:09:00 +0800 Subject: [PATCH 1/6] feat: Implement log -p option. #1197 Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- .../contents/docs/libra/command/log/index.mdx | 2 + libra/src/command/add.rs | 13 ++++ libra/src/command/log.rs | 69 ++++++++++++++++++- libra/tests/command/log_test.rs | 8 +-- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/aria/contents/docs/libra/command/log/index.mdx b/aria/contents/docs/libra/command/log/index.mdx index d671e170f..de956600f 100644 --- a/aria/contents/docs/libra/command/log/index.mdx +++ b/aria/contents/docs/libra/command/log/index.mdx @@ -26,6 +26,8 @@ List commits that are reachable by current local branch. The order is from the l Shorthand for `--pretty=oneline --abbrev-commit`. Display each commit as a single line with abbreviated commit hash (7 characters) and commit message first line only. - `-h`, `--help` Print help +- `-p`, `--patch` + Show diff for each commit (like git -p). If you specify file paths, only show the diff for those files. The order of the commits maybe different from Git's behavior. diff --git a/libra/src/command/add.rs b/libra/src/command/add.rs index c286b65f3..b606245a1 100644 --- a/libra/src/command/add.rs +++ b/libra/src/command/add.rs @@ -117,6 +117,19 @@ pub async fn execute(args: AddArgs) { files.extend(changes.new); } + // When adding the whole worktree (e.g., `add .` or `-A`), avoid adding the running + // binary itself to the index. We compare canonicalized absolute paths. + if let Some(exe_path) = std::env::current_exe().ok().and_then(|p| p.canonicalize().ok()) { + files.retain(|p| { + // convert the repo-relative/workdir-relative path to absolute path + if let Ok(abs) = util::workdir_to_absolute(p).canonicalize() { + return abs != exe_path; + } + // if we fail to canonicalize, keep the file (conservative) + true + }); + } + if args.dry_run { // dry run for file in &files { diff --git a/libra/src/command/log.rs b/libra/src/command/log.rs index 26cd6ea62..b04632337 100644 --- a/libra/src/command/log.rs +++ b/libra/src/command/log.rs @@ -1,5 +1,6 @@ use std::cmp::min; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use crate::command::load_object; use crate::internal::branch::Branch; @@ -12,10 +13,13 @@ use std::io::Write; use std::process::{Command, Stdio}; use mercury::hash::SHA1; -use mercury::internal::object::commit::Commit; +use mercury::internal::object::{blob::Blob, tree::Tree, commit::Commit}; +use neptune::Diff; use std::collections::VecDeque; use std::str::FromStr; +use crate::utils::util; +use crate::utils::object_ext::TreeExt; use common::utils::parse_commit_msg; #[derive(Parser, Debug)] pub struct LogArgs { @@ -25,6 +29,13 @@ pub struct LogArgs { /// Shorthand for --pretty=oneline --abbrev-commit #[clap(long)] pub oneline: bool, + /// Show diffs for each commit (like git -p) + #[clap(short = 'p', long = "patch")] + pub patch: bool, + + /// Files to limit diff output (only used with -p) + #[clap(requires = "patch", value_name = "PATHS")] + pathspec: Vec, } /// Get all reachable commits from the given commit hash @@ -91,7 +102,10 @@ pub async fn execute(args: LogArgs) { let branches = branch_commits.get(&commit.id).cloned().unwrap_or_default(); - let message = if args.oneline { + // prepare pathspecs for diff if needed + let paths: Vec = args.pathspec.iter().map(util::to_workdir_path).collect(); + + let message = if args.oneline { // Oneline format: let short_hash = &commit.id.to_string()[..7]; let (msg, _) = parse_commit_msg(&commit.message); @@ -106,7 +120,7 @@ pub async fn execute(args: LogArgs) { } else { format!("{} {}", short_hash.yellow(), msg) } - } else { + } else { // Default detailed format let mut message = format!( "{} {}", @@ -150,6 +164,12 @@ pub async fn execute(args: LogArgs) { message.push_str(&format!("\nAuthor: {}", commit.author)); let (msg, _) = parse_commit_msg(&commit.message); message.push_str(&format!("\n{msg}\n")); + // If patch requested, compute diff between this commit and its first parent + if args.patch { + let patch_output = generate_diff(&commit, paths.clone()).await; + message.push_str(&patch_output); + } + message }; @@ -192,5 +212,48 @@ async fn create_branch_commits_map() -> HashMap> { commit_to_branches } +/// Generate unified diff between commit and its first parent (or empty tree) +async fn generate_diff(commit: &Commit, paths: Vec) -> String { + // prepare old and new blobs + // new_blobs from commit tree + let tree = load_object::(&commit.tree_id).unwrap(); + let new_blobs: Vec<(PathBuf, SHA1)> = tree.get_plain_items(); + + // old_blobs from first parent if exists + let old_blobs: Vec<(PathBuf, SHA1)> = if !commit.parent_commit_ids.is_empty() { + let parent = &commit.parent_commit_ids[0]; + let parent_hash = SHA1::from_str(&parent.to_string()).unwrap(); + let parent_commit = load_object::(&parent_hash).unwrap(); + let parent_tree = load_object::(&parent_commit.tree_id).unwrap(); + parent_tree.get_plain_items() + } else { + Vec::new() + }; + + let read_content = |file: &PathBuf, hash: &SHA1| { + match load_object::(hash) { + Ok(blob) => blob.data, + Err(_) => { + let file = util::to_workdir_path(file); + std::fs::read(&file).unwrap() + } + } + }; + + let diffs = Diff::diff( + old_blobs, + new_blobs, + String::from("histogram"), + paths.into_iter().collect(), + read_content, + ) + .await; + let mut out = String::new(); + for d in diffs { + out.push_str(&d.data); + } + out +} + #[cfg(test)] mod tests {} diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index bf189fb8a..aa6a28bbb 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -1,5 +1,6 @@ use super::*; use std::cmp::min; +use clap::Parser; #[tokio::test] #[serial] /// Tests retrieval of commits reachable from a specific commit hash @@ -138,16 +139,13 @@ async fn test_log_oneline() { let reachable_commits = get_reachable_commits(commit_id).await; // Test oneline format - let args = LogArgs { - number: Some(3), - oneline: true, - }; + let args = LogArgs::try_parse_from(&["libra", "--number", "3", "--oneline"]); // Since execute function writes to stdout, we'll test the logic directly let mut sorted_commits = reachable_commits.clone(); sorted_commits.sort_by(|a, b| b.committer.timestamp.cmp(&a.committer.timestamp)); - let max_commits = std::cmp::min(args.number.unwrap_or(usize::MAX), sorted_commits.len()); + let max_commits = std::cmp::min(args.unwrap().number.unwrap_or(usize::MAX), sorted_commits.len()); for (i, commit) in sorted_commits.iter().take(max_commits).enumerate() { // Test short hash format (should be 7 characters) From 1dc24675c9f14689ffb9c58a4477a1bdf8b31123 Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Tue, 26 Aug 2025 08:24:57 +0800 Subject: [PATCH 2/6] fix: Fix a bug in the libra/tests/command/log_test.rs and add tests for the -p parameter Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- libra/tests/command/log_test.rs | 148 +++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index aa6a28bbb..83015ec1d 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -1,6 +1,8 @@ use super::*; use std::cmp::min; use clap::Parser; +use mercury::internal::object::commit::Commit; +use std::os::unix::fs::PermissionsExt; #[tokio::test] #[serial] /// Tests retrieval of commits reachable from a specific commit hash @@ -139,7 +141,7 @@ async fn test_log_oneline() { let reachable_commits = get_reachable_commits(commit_id).await; // Test oneline format - let args = LogArgs::try_parse_from(&["libra", "--number", "3", "--oneline"]); + let args = LogArgs::try_parse_from(["libra", "--number", "3", "--oneline"]); // Since execute function writes to stdout, we'll test the logic directly let mut sorted_commits = reachable_commits.clone(); @@ -161,3 +163,147 @@ async fn test_log_oneline() { assert_eq!(msg.trim(), format!("Commit_{expected_number}")); } } + + +#[tokio::test] +#[serial] +/// Tests log -p (patch) without pathspec: create A -> commit -> create B -> commit -> assert diffs contain both A and B contents +async fn test_log_patch_no_pathspec() { + let temp_path = tempdir().unwrap(); + test::setup_with_new_libra_in(temp_path.path()).await; + let _guard = ChangeDirGuard::new(temp_path.path()); + + // Create file A and commit + test::ensure_file("A.txt", Some("Content A\n")); + add::execute(AddArgs { + pathspec: vec![String::from("A.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + commit::execute(CommitArgs { + message: "Add A".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Create file B and commit + test::ensure_file("B.txt", Some("Content B\n")); + add::execute(AddArgs { + pathspec: vec![String::from("B.txt")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + commit::execute(CommitArgs { + message: "Add B".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Get commits and compute diffs (replicating generate_diff logic) to avoid spawning pager + // Prepare a fake `less` wrapper that writes stdin to a file so we can assert on the pager output. + let bin_dir = temp_path.path().join("bin"); + std::fs::create_dir_all(&bin_dir).unwrap(); + let out_file = temp_path.path().join("less_out.txt"); + let less_path = bin_dir.join("less"); + let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + std::fs::write(&less_path, script.as_bytes()).unwrap(); + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&less_path, perms).unwrap(); + + // Prepend our bin dir to PATH so Command::new("less") finds it. + let old_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", bin_dir.display(), old_path); + std::env::set_var("PATH", &new_path); + + // Call log subcommand with -p + let args = LogArgs::try_parse_from(["libra", "--number", "2", "-p"]).unwrap(); + libra::command::log::execute(args).await; + + // restore PATH + std::env::set_var("PATH", old_path); + + let combined_out = std::fs::read_to_string(out_file).unwrap(); + assert!(combined_out.contains("Content A"), "patch should contain A content"); + assert!(combined_out.contains("Content B"), "patch should contain B content"); +} + +#[tokio::test] +#[serial] +/// Tests log -p with a specific pathspec: commit contains A and B, but log -p A should only include A +async fn test_log_patch_with_pathspec() { + let temp_path = tempdir().unwrap(); + test::setup_with_new_libra_in(temp_path.path()).await; + let _guard = ChangeDirGuard::new(temp_path.path()); + + // Create files A and B and commit both in one commit + test::ensure_file("A.txt", Some("Content A\n")); + test::ensure_file("B.txt", Some("Content B\n")); + + add::execute(AddArgs { + pathspec: vec![String::from(".")], + all: false, + update: false, + refresh: false, + verbose: false, + dry_run: false, + ignore_errors: false, + }) + .await; + + commit::execute(CommitArgs { + message: "Add A and B".to_string(), + allow_empty: false, + conventional: false, + amend: false, + signoff: false, + disable_pre: false, + }) + .await; + + // Prepare fake less again + let bin_dir = temp_path.path().join("bin2"); + std::fs::create_dir_all(&bin_dir).unwrap(); + let out_file = temp_path.path().join("less_out_pathspec.txt"); + let less_path = bin_dir.join("less"); + let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + std::fs::write(&less_path, script.as_bytes()).unwrap(); + let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&less_path, perms).unwrap(); + + // Prepend our bin dir to PATH so Command::new("less") finds it. + let old_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", bin_dir.display(), old_path); + std::env::set_var("PATH", &new_path); + + // Call log subcommand with -p and pathspec A.txt + let args = LogArgs::try_parse_from(["libra", "-p", "A.txt"]).unwrap(); + libra::command::log::execute(args).await; + + // restore PATH + std::env::set_var("PATH", old_path); + + let out = std::fs::read_to_string(out_file).unwrap(); + assert!(out.contains("Content A"), "patch should contain A content"); + assert!(!out.contains("Content B"), "patch should not contain B content when pathspec is A"); +} From 402aa5661a55ce7e1cdd85049c8dbb8aa2a55dfa Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Tue, 26 Aug 2025 10:44:58 +0800 Subject: [PATCH 3/6] fix: Fix the issue with -p-parm-test in libra/tests/command/log_test.rs on windows Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- libra/tests/command/log_test.rs | 70 +++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index 83015ec1d..432e2b3b9 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -2,6 +2,7 @@ use super::*; use std::cmp::min; use clap::Parser; use mercury::internal::object::commit::Commit; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; #[tokio::test] #[serial] @@ -222,17 +223,37 @@ async fn test_log_patch_no_pathspec() { let bin_dir = temp_path.path().join("bin"); std::fs::create_dir_all(&bin_dir).unwrap(); let out_file = temp_path.path().join("less_out.txt"); - let less_path = bin_dir.join("less"); - let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + + // On Unix create a shell script and chmod +x. On Windows create a batch file (no chmod). + let less_path = if cfg!(windows) { + bin_dir.join("less.bat") + } else { + bin_dir.join("less") + }; + + let script = if cfg!(windows) { + // batch: `more` reads stdin and we redirect into file + format!("@echo off\r\nmore > \"{}\"\r\n", out_file.display()) + } else { + format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()) + }; + std::fs::write(&less_path, script.as_bytes()).unwrap(); - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&less_path, perms).unwrap(); - // Prepend our bin dir to PATH so Command::new("less") finds it. + // chmod on unix only + if cfg!(unix) { + let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&less_path, perms).unwrap(); + } + + // Prepend our bin dir to PATH so Command::new("less") finds it. Use platform PATH sep. let old_path = std::env::var("PATH").unwrap_or_default(); - let new_path = format!("{}:{}", bin_dir.display(), old_path); + let new_path = if cfg!(windows) { + format!("{};{}", bin_dir.display(), old_path) + } else { + format!("{}:{}", bin_dir.display(), old_path) + }; std::env::set_var("PATH", &new_path); // Call log subcommand with -p @@ -280,20 +301,37 @@ async fn test_log_patch_with_pathspec() { }) .await; - // Prepare fake less again + // Prepare fake less again (platform aware) let bin_dir = temp_path.path().join("bin2"); std::fs::create_dir_all(&bin_dir).unwrap(); let out_file = temp_path.path().join("less_out_pathspec.txt"); - let less_path = bin_dir.join("less"); - let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + + let less_path = if cfg!(windows) { + bin_dir.join("less.bat") + } else { + bin_dir.join("less") + }; + + let script = if cfg!(windows) { + format!("@echo off\r\nmore > \"{}\"\r\n", out_file.display()) + } else { + format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()) + }; + std::fs::write(&less_path, script.as_bytes()).unwrap(); - let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&less_path, perms).unwrap(); + if cfg!(unix) { + let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&less_path, perms).unwrap(); + } - // Prepend our bin dir to PATH so Command::new("less") finds it. + // Prepend our bin dir to PATH so Command::new("less") finds it. Use platform PATH sep. let old_path = std::env::var("PATH").unwrap_or_default(); - let new_path = format!("{}:{}", bin_dir.display(), old_path); + let new_path = if cfg!(windows) { + format!("{};{}", bin_dir.display(), old_path) + } else { + format!("{}:{}", bin_dir.display(), old_path) + }; std::env::set_var("PATH", &new_path); // Call log subcommand with -p and pathspec A.txt From 42d57a64470d85ad74cbea6453482d5338d04c2c Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Tue, 26 Aug 2025 12:22:25 +0800 Subject: [PATCH 4/6] fix: Fix the issue with -p-parm-log-test on windows Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- libra/tests/command/log_test.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index 432e2b3b9..809c3e181 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -241,12 +241,13 @@ async fn test_log_patch_no_pathspec() { std::fs::write(&less_path, script.as_bytes()).unwrap(); // chmod on unix only - if cfg!(unix) { + #[cfg(unix)] + { let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); perms.set_mode(0o755); std::fs::set_permissions(&less_path, perms).unwrap(); } - + // Prepend our bin dir to PATH so Command::new("less") finds it. Use platform PATH sep. let old_path = std::env::var("PATH").unwrap_or_default(); let new_path = if cfg!(windows) { @@ -319,7 +320,8 @@ async fn test_log_patch_with_pathspec() { }; std::fs::write(&less_path, script.as_bytes()).unwrap(); - if cfg!(unix) { + #[cfg(unix)] + { let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); perms.set_mode(0o755); std::fs::set_permissions(&less_path, perms).unwrap(); From 036718ba08b4270d92175374ebe49fed3af0fc16 Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Tue, 26 Aug 2025 16:23:27 +0800 Subject: [PATCH 5/6] fix: Fix the issue with -p-parm-log-test on windows again Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- libra/tests/command/log_test.rs | 121 +++++++++++++++++--------------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index 809c3e181..6a74f3597 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -2,8 +2,6 @@ use super::*; use std::cmp::min; use clap::Parser; use mercury::internal::object::commit::Commit; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; #[tokio::test] #[serial] /// Tests retrieval of commits reachable from a specific commit hash @@ -218,37 +216,42 @@ async fn test_log_patch_no_pathspec() { }) .await; - // Get commits and compute diffs (replicating generate_diff logic) to avoid spawning pager - // Prepare a fake `less` wrapper that writes stdin to a file so we can assert on the pager output. let bin_dir = temp_path.path().join("bin"); std::fs::create_dir_all(&bin_dir).unwrap(); let out_file = temp_path.path().join("less_out.txt"); - // On Unix create a shell script and chmod +x. On Windows create a batch file (no chmod). - let less_path = if cfg!(windows) { - bin_dir.join("less.bat") + if cfg!(windows) { + // Windows: use PowerShell to fetch stdin + let ps_script = format!( + r#" +$content = [Console]::In.ReadToEnd() +[System.IO.File]::WriteAllText('{}', $content) +"#, + out_file.display().to_string().replace("\\", "\\\\") + ); + let ps_path = bin_dir.join("less.ps1"); + std::fs::write(&ps_path, ps_script.as_bytes()).unwrap(); + + // Create a batch file to call PowerShell + let bat_path = bin_dir.join("less.bat"); + let bat_script = format!( + "@echo off\r\npowershell -ExecutionPolicy Bypass -File \"{}\"\r\n", + ps_path.display() + ); + std::fs::write(&bat_path, bat_script.as_bytes()).unwrap(); } else { - bin_dir.join("less") - }; - - let script = if cfg!(windows) { - // batch: `more` reads stdin and we redirect into file - format!("@echo off\r\nmore > \"{}\"\r\n", out_file.display()) - } else { - format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()) - }; - - std::fs::write(&less_path, script.as_bytes()).unwrap(); - - // chmod on unix only - #[cfg(unix)] - { - let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&less_path, perms).unwrap(); + // Unix: use shell script + let less_path = bin_dir.join("less"); + let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + std::fs::write(&less_path, script.as_bytes()).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&less_path, std::fs::Permissions::from_mode(0o755)).unwrap(); + } } - - // Prepend our bin dir to PATH so Command::new("less") finds it. Use platform PATH sep. + + // Set PATH let old_path = std::env::var("PATH").unwrap_or_default(); let new_path = if cfg!(windows) { format!("{};{}", bin_dir.display(), old_path) @@ -257,16 +260,16 @@ async fn test_log_patch_no_pathspec() { }; std::env::set_var("PATH", &new_path); - // Call log subcommand with -p + // Call log command let args = LogArgs::try_parse_from(["libra", "--number", "2", "-p"]).unwrap(); libra::command::log::execute(args).await; - // restore PATH + // Restore PATH std::env::set_var("PATH", old_path); - let combined_out = std::fs::read_to_string(out_file).unwrap(); - assert!(combined_out.contains("Content A"), "patch should contain A content"); - assert!(combined_out.contains("Content B"), "patch should contain B content"); + let combined_out = std::fs::read_to_string(&out_file).unwrap_or_default(); + assert!(combined_out.contains("Content A"), "patch should contain A content, got: {}", combined_out); + assert!(combined_out.contains("Content B"), "patch should contain B content, got: {}", combined_out); } #[tokio::test] @@ -302,32 +305,39 @@ async fn test_log_patch_with_pathspec() { }) .await; - // Prepare fake less again (platform aware) let bin_dir = temp_path.path().join("bin2"); std::fs::create_dir_all(&bin_dir).unwrap(); let out_file = temp_path.path().join("less_out_pathspec.txt"); - let less_path = if cfg!(windows) { - bin_dir.join("less.bat") - } else { - bin_dir.join("less") - }; - - let script = if cfg!(windows) { - format!("@echo off\r\nmore > \"{}\"\r\n", out_file.display()) + if cfg!(windows) { + let ps_script = format!( + r#" +$content = [Console]::In.ReadToEnd() +[System.IO.File]::WriteAllText('{}', $content) +"#, + out_file.display().to_string().replace("\\", "\\\\") + ); + let ps_path = bin_dir.join("less.ps1"); + std::fs::write(&ps_path, ps_script.as_bytes()).unwrap(); + + let bat_path = bin_dir.join("less.bat"); + let bat_script = format!( + "@echo off\r\npowershell -ExecutionPolicy Bypass -File \"{}\"\r\n", + ps_path.display() + ); + std::fs::write(&bat_path, bat_script.as_bytes()).unwrap(); } else { - format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()) - }; - - std::fs::write(&less_path, script.as_bytes()).unwrap(); - #[cfg(unix)] - { - let mut perms = std::fs::metadata(&less_path).unwrap().permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&less_path, perms).unwrap(); + let less_path = bin_dir.join("less"); + let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); + std::fs::write(&less_path, script.as_bytes()).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&less_path, std::fs::Permissions::from_mode(0o755)).unwrap(); + } } - // Prepend our bin dir to PATH so Command::new("less") finds it. Use platform PATH sep. + // Set PATH let old_path = std::env::var("PATH").unwrap_or_default(); let new_path = if cfg!(windows) { format!("{};{}", bin_dir.display(), old_path) @@ -336,14 +346,13 @@ async fn test_log_patch_with_pathspec() { }; std::env::set_var("PATH", &new_path); - // Call log subcommand with -p and pathspec A.txt + // Call log command let args = LogArgs::try_parse_from(["libra", "-p", "A.txt"]).unwrap(); libra::command::log::execute(args).await; - // restore PATH std::env::set_var("PATH", old_path); - let out = std::fs::read_to_string(out_file).unwrap(); - assert!(out.contains("Content A"), "patch should contain A content"); - assert!(!out.contains("Content B"), "patch should not contain B content when pathspec is A"); + let out = std::fs::read_to_string(out_file).unwrap_or_default(); + assert!(out.contains("Content A"), "patch should contain A content, got: {}", out); + assert!(!out.contains("Content B"), "patch should not contain B content when pathspec is A, got: {}", out); } From 5afa008bcf9ebba79046bef9157ead58a799ea32 Mon Sep 17 00:00:00 2001 From: Ruizhi Huang <231220075@smail.nju.edu.cn> Date: Tue, 26 Aug 2025 18:24:03 +0800 Subject: [PATCH 6/6] fix: Fix the issue with -p-parm-log-test on windows in a stupid way Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn> --- libra/tests/command/log_test.rs | 145 +++++++++++++++++--------------- 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/libra/tests/command/log_test.rs b/libra/tests/command/log_test.rs index 6a74f3597..a00cabe3c 100644 --- a/libra/tests/command/log_test.rs +++ b/libra/tests/command/log_test.rs @@ -2,6 +2,12 @@ use super::*; use std::cmp::min; use clap::Parser; use mercury::internal::object::commit::Commit; +use std::str::FromStr; +use mercury::hash::SHA1; +use mercury::internal::object::{blob::Blob, tree::Tree}; +use libra::utils::object_ext::TreeExt; +use libra::utils::util; +use neptune::Diff; #[tokio::test] #[serial] /// Tests retrieval of commits reachable from a specific commit hash @@ -220,27 +226,13 @@ async fn test_log_patch_no_pathspec() { std::fs::create_dir_all(&bin_dir).unwrap(); let out_file = temp_path.path().join("less_out.txt"); + // On Windows we inline diff generation to avoid relying on spawned pager if cfg!(windows) { - // Windows: use PowerShell to fetch stdin - let ps_script = format!( - r#" -$content = [Console]::In.ReadToEnd() -[System.IO.File]::WriteAllText('{}', $content) -"#, - out_file.display().to_string().replace("\\", "\\\\") - ); - let ps_path = bin_dir.join("less.ps1"); - std::fs::write(&ps_path, ps_script.as_bytes()).unwrap(); - - // Create a batch file to call PowerShell - let bat_path = bin_dir.join("less.bat"); - let bat_script = format!( - "@echo off\r\npowershell -ExecutionPolicy Bypass -File \"{}\"\r\n", - ps_path.display() - ); - std::fs::write(&bat_path, bat_script.as_bytes()).unwrap(); + let diffs = collect_combined_diff_for_commits(2, Vec::new()).await; + assert!(diffs.contains("Content A"), "patch should contain A content, got: {}", diffs); + assert!(diffs.contains("Content B"), "patch should contain B content, got: {}", diffs); } else { - // Unix: use shell script + // Unix: create shell script that writes stdin to file let less_path = bin_dir.join("less"); let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); std::fs::write(&less_path, script.as_bytes()).unwrap(); @@ -249,27 +241,22 @@ $content = [Console]::In.ReadToEnd() use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&less_path, std::fs::Permissions::from_mode(0o755)).unwrap(); } - } - // Set PATH - let old_path = std::env::var("PATH").unwrap_or_default(); - let new_path = if cfg!(windows) { - format!("{};{}", bin_dir.display(), old_path) - } else { - format!("{}:{}", bin_dir.display(), old_path) - }; - std::env::set_var("PATH", &new_path); + // Set PATH and run + let old_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", bin_dir.display(), old_path); + std::env::set_var("PATH", &new_path); - // Call log command - let args = LogArgs::try_parse_from(["libra", "--number", "2", "-p"]).unwrap(); - libra::command::log::execute(args).await; + let args = LogArgs::try_parse_from(["libra", "--number", "2", "-p"]).unwrap(); + libra::command::log::execute(args).await; - // Restore PATH - std::env::set_var("PATH", old_path); + // Restore PATH + std::env::set_var("PATH", old_path); - let combined_out = std::fs::read_to_string(&out_file).unwrap_or_default(); - assert!(combined_out.contains("Content A"), "patch should contain A content, got: {}", combined_out); - assert!(combined_out.contains("Content B"), "patch should contain B content, got: {}", combined_out); + let combined_out = std::fs::read_to_string(&out_file).unwrap_or_default(); + assert!(combined_out.contains("Content A"), "patch should contain A content, got: {}", combined_out); + assert!(combined_out.contains("Content B"), "patch should contain B content, got: {}", combined_out); + } } #[tokio::test] @@ -310,22 +297,10 @@ async fn test_log_patch_with_pathspec() { let out_file = temp_path.path().join("less_out_pathspec.txt"); if cfg!(windows) { - let ps_script = format!( - r#" -$content = [Console]::In.ReadToEnd() -[System.IO.File]::WriteAllText('{}', $content) -"#, - out_file.display().to_string().replace("\\", "\\\\") - ); - let ps_path = bin_dir.join("less.ps1"); - std::fs::write(&ps_path, ps_script.as_bytes()).unwrap(); - - let bat_path = bin_dir.join("less.bat"); - let bat_script = format!( - "@echo off\r\npowershell -ExecutionPolicy Bypass -File \"{}\"\r\n", - ps_path.display() - ); - std::fs::write(&bat_path, bat_script.as_bytes()).unwrap(); + let paths = vec![util::to_workdir_path("A.txt")]; + let diffs = collect_combined_diff_for_commits(1, paths).await; + assert!(diffs.contains("Content A"), "patch should contain A content, got: {}", diffs); + assert!(!diffs.contains("Content B"), "patch should not contain B content when pathspec is A, got: {}", diffs); } else { let less_path = bin_dir.join("less"); let script = format!("#!/bin/sh\ncat - > \"{}\"\n", out_file.display()); @@ -335,24 +310,58 @@ $content = [Console]::In.ReadToEnd() use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&less_path, std::fs::Permissions::from_mode(0o755)).unwrap(); } - } - // Set PATH - let old_path = std::env::var("PATH").unwrap_or_default(); - let new_path = if cfg!(windows) { - format!("{};{}", bin_dir.display(), old_path) - } else { - format!("{}:{}", bin_dir.display(), old_path) - }; - std::env::set_var("PATH", &new_path); + let old_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", bin_dir.display(), old_path); + std::env::set_var("PATH", &new_path); - // Call log command - let args = LogArgs::try_parse_from(["libra", "-p", "A.txt"]).unwrap(); - libra::command::log::execute(args).await; + let args = LogArgs::try_parse_from(["libra", "-p", "A.txt"]).unwrap(); + libra::command::log::execute(args).await; - std::env::set_var("PATH", old_path); + std::env::set_var("PATH", old_path); - let out = std::fs::read_to_string(out_file).unwrap_or_default(); - assert!(out.contains("Content A"), "patch should contain A content, got: {}", out); - assert!(!out.contains("Content B"), "patch should not contain B content when pathspec is A, got: {}", out); + let out = std::fs::read_to_string(out_file).unwrap_or_default(); + assert!(out.contains("Content A"), "patch should contain A content, got: {}", out); + assert!(!out.contains("Content B"), "patch should not contain B content when pathspec is A, got: {}", out); + } +} + +async fn collect_combined_diff_for_commits(count: usize, paths: Vec) -> String { + // Get head commit and reachable commits + let commit_hash = Head::current_commit().await.unwrap().to_string(); + let mut reachable_commits = get_reachable_commits(commit_hash).await; + reachable_commits.sort_by(|a, b| b.committer.timestamp.cmp(&a.committer.timestamp)); + + let max_output_number = std::cmp::min(count, reachable_commits.len()); + let mut out = String::new(); + for commit in reachable_commits.into_iter().take(max_output_number) { + let tree = load_object::(&commit.tree_id).unwrap(); + let new_blobs: Vec<(std::path::PathBuf, SHA1)> = tree.get_plain_items(); + + let old_blobs: Vec<(std::path::PathBuf, SHA1)> = if !commit.parent_commit_ids.is_empty() { + let parent = &commit.parent_commit_ids[0]; + let parent_hash = SHA1::from_str(&parent.to_string()).unwrap(); + let parent_commit = load_object::(&parent_hash).unwrap(); + let parent_tree = load_object::(&parent_commit.tree_id).unwrap(); + parent_tree.get_plain_items() + } else { + Vec::new() + }; + + let read_content = |file: &std::path::PathBuf, hash: &SHA1| { + match load_object::(hash) { + Ok(blob) => blob.data, + Err(_) => { + let file = util::to_workdir_path(file); + std::fs::read(&file).unwrap() + } + } + }; + + let diffs = Diff::diff(old_blobs, new_blobs, String::from("histogram"), paths.clone().into_iter().collect(), read_content).await; + for d in diffs { + out.push_str(&d.data); + } + } + out }