From c85d3138337a4264a716a7c54a24d2d855a03470 Mon Sep 17 00:00:00 2001 From: zine yu Date: Tue, 22 Jul 2025 16:23:49 +0800 Subject: [PATCH 1/5] feat: add pre-commit hook support with --disable-pre flag Signed-off-by: zine yu --- libra/src/command/commit.rs | 45 ++++++++++++++++++++++++++++ libra/src/command/init.rs | 21 ++++++++++++- libra/src/utils/path.rs | 4 +++ libra/template/pre-commit.ps1 | 12 ++++++++ libra/template/pre-commit.sh | 11 +++++++ libra/tests/command/branch_test.rs | 4 +++ libra/tests/command/checkout_test.rs | 1 + libra/tests/command/commit_test.rs | 5 ++++ libra/tests/command/switch_test.rs | 4 +++ 9 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 libra/template/pre-commit.ps1 create mode 100644 libra/template/pre-commit.sh diff --git a/libra/src/command/commit.rs b/libra/src/command/commit.rs index ac078d1b4..ce0614797 100644 --- a/libra/src/command/commit.rs +++ b/libra/src/command/commit.rs @@ -1,3 +1,4 @@ +use std::process::Stdio; use std::str::FromStr; use std::{collections::HashSet, path::PathBuf}; @@ -15,6 +16,7 @@ use mercury::internal::index::Index; use mercury::internal::object::commit::Commit; use mercury::internal::object::tree::{Tree, TreeItem, TreeItemMode}; use mercury::internal::object::ObjectTrait; +use std::process::Command; use super::save_object; @@ -38,6 +40,9 @@ pub struct CommitArgs { /// add signed-off-by line at the end of the commit message #[arg(short = 's', long)] pub signoff: bool, + + #[arg(long)] + pub disable_pre: bool, } pub async fn execute(args: CommitArgs) { @@ -49,6 +54,46 @@ pub async fn execute(args: CommitArgs) { panic!("fatal: no changes added to commit, use --allow-empty to override"); } + // run pre commit hook + if !args.disable_pre { + let hooks_dir = path::hooks(); + + #[cfg(not(target_os = "windows"))] + let hook_path = hooks_dir.join("pre-commit.sh"); + + #[cfg(target_os = "windows")] + let hook_path = hooks_dir.join("pre-commit.ps1"); + if hook_path.exists() { + let hook_display = hook_path.display(); + #[cfg(not(target_os = "windows"))] + let output = Command::new("sh") + .arg(&hook_path) + .current_dir(util::working_dir()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap_or_else(|e| panic!("Failed to execute hook {}: {}", hook_display, e)); + + #[cfg(target_os = "windows")] + let output = Command::new("powershell") + .arg("-File") + .arg(&hook_path) + .current_dir(util::working_dir()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap_or_else(|e| panic!("Failed to execute hook {}: {}", hook_display, e)); + + if !output.status.success() { + panic!( + "Hook {} failed with exit code {}", + hook_display, + output.status.code().unwrap_or(-1) + ); + } + } + } + //Prepare commit message let commit_message = if args.signoff { // get user diff --git a/libra/src/command/init.rs b/libra/src/command/init.rs index 3029b2c9a..502a106eb 100644 --- a/libra/src/command/init.rs +++ b/libra/src/command/init.rs @@ -128,7 +128,7 @@ pub async fn init(args: InitArgs) -> io::Result<()> { } // Create .libra & sub-dirs - let dirs = ["objects/pack", "objects/info", "info"]; + let dirs = ["objects/pack", "objects/info", "info", "hooks"]; for dir in dirs { fs::create_dir_all(root_dir.join(dir))?; } @@ -143,6 +143,25 @@ pub async fn init(args: InitArgs) -> io::Result<()> { root_dir.join("description"), include_str!("../../template/description"), )?; + // Create .libra/hooks/pre-commit.sh + fs::write( + root_dir.join("hooks").join("pre-commit.sh"), + include_str!("../../template/pre-commit.sh"), + )?; + + // Set Permission + #[cfg(not(target_os = "windows"))] + { + use std::os::unix::fs::PermissionsExt; + let perms = fs::Permissions::from_mode(0o755); + fs::set_permissions(root_dir.join("hooks").join("pre-commit.sh"), perms)?; + } + + // Create .libra/hooks/pre-commit.ps1 + fs::write( + root_dir.join("hooks").join("pre-commit.ps1"), + include_str!("../../template/pre-commit.ps1"), + )?; // Create database: .libra/libra.db let conn; diff --git a/libra/src/utils/path.rs b/libra/src/utils/path.rs index ef84f1423..df20c1030 100644 --- a/libra/src/utils/path.rs +++ b/libra/src/utils/path.rs @@ -13,6 +13,10 @@ pub fn database() -> PathBuf { util::storage_path().join(util::DATABASE) } +pub fn hooks() -> PathBuf { + util::storage_path().join("hooks") +} + pub fn attributes() -> PathBuf { util::working_dir().join(util::ATTRIBUTES) } diff --git a/libra/template/pre-commit.ps1 b/libra/template/pre-commit.ps1 new file mode 100644 index 000000000..32cd5a214 --- /dev/null +++ b/libra/template/pre-commit.ps1 @@ -0,0 +1,12 @@ + +#!/bin/sh + + +# Pre-commit hook example +# Exit with 0 to allow commit, non-zero to reject + +# Example +# Write-Host "Running pre-commit hook" + + +exit 0 diff --git a/libra/template/pre-commit.sh b/libra/template/pre-commit.sh new file mode 100644 index 000000000..37c39f342 --- /dev/null +++ b/libra/template/pre-commit.sh @@ -0,0 +1,11 @@ +#!/bin/sh + + +# Pre-commit hook example +# Exit with 0 to allow commit, non-zero to reject + +# Example +# echo "Running pre-commit hook" + + +exit 0 diff --git a/libra/tests/command/branch_test.rs b/libra/tests/command/branch_test.rs index bff55ac10..5895cf787 100644 --- a/libra/tests/command/branch_test.rs +++ b/libra/tests/command/branch_test.rs @@ -17,6 +17,7 @@ async fn test_branch() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(commit_args).await; let first_commit_id = Branch::find_branch("master", None).await.unwrap().commit; @@ -27,6 +28,7 @@ async fn test_branch() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(commit_args).await; let second_commit_id = Branch::find_branch("master", None).await.unwrap().commit; @@ -112,6 +114,7 @@ async fn test_create_branch_from_remote() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; let hash = Head::current_commit().await.unwrap(); @@ -150,6 +153,7 @@ async fn test_invalid_branch_name() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; diff --git a/libra/tests/command/checkout_test.rs b/libra/tests/command/checkout_test.rs index a1a5a918d..d7b9b0671 100644 --- a/libra/tests/command/checkout_test.rs +++ b/libra/tests/command/checkout_test.rs @@ -77,6 +77,7 @@ async fn test_checkout_module_functions() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(commit_args).await; diff --git a/libra/tests/command/commit_test.rs b/libra/tests/command/commit_test.rs index 9ad795042..fec06071b 100644 --- a/libra/tests/command/commit_test.rs +++ b/libra/tests/command/commit_test.rs @@ -19,6 +19,7 @@ async fn test_execute_commit_with_empty_index_fail() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; } @@ -41,6 +42,7 @@ async fn test_execute_commit() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; @@ -66,6 +68,7 @@ async fn test_execute_commit() { conventional: false, amend: true, signoff: false, + pre:true, }; commit::execute(args).await; @@ -107,6 +110,7 @@ async fn test_execute_commit() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; @@ -135,6 +139,7 @@ async fn test_execute_commit() { conventional: false, amend: true, signoff: false, + pre:true, }; commit::execute(args).await; diff --git a/libra/tests/command/switch_test.rs b/libra/tests/command/switch_test.rs index 55c0bb696..748a8ee82 100644 --- a/libra/tests/command/switch_test.rs +++ b/libra/tests/command/switch_test.rs @@ -42,6 +42,7 @@ async fn test_switch_function() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; } @@ -84,6 +85,7 @@ async fn test_switch_function() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; @@ -160,6 +162,7 @@ async fn test_detach_head_basic() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; } @@ -199,6 +202,7 @@ async fn test_detach_head_basic() { conventional: false, amend: false, signoff: false, + pre:true, }; commit::execute(args).await; } From eb7b4c4b2b7502c6460756b909af9ef4f02c466d Mon Sep 17 00:00:00 2001 From: zine yu Date: Tue, 22 Jul 2025 16:25:16 +0800 Subject: [PATCH 2/5] chore: ignore VSCode workspace files in .gitignore Signed-off-by: zine yu --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ef5900d1b..eeff576d1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ Cargo.lock .vscode .zed .trae +*.code-workspace # Mac .DS_Store From e776a3fe3aae3056f2705749e69a490054abb90b Mon Sep 17 00:00:00 2001 From: zine yu Date: Tue, 22 Jul 2025 16:28:29 +0800 Subject: [PATCH 3/5] docs: add `--disable-pre` flag to ignore pre-commit hook Signed-off-by: zine yu --- aria/contents/docs/libra/command/commit/index.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/aria/contents/docs/libra/command/commit/index.mdx b/aria/contents/docs/libra/command/commit/index.mdx index f48154049..9d1e63a95 100644 --- a/aria/contents/docs/libra/command/commit/index.mdx +++ b/aria/contents/docs/libra/command/commit/index.mdx @@ -26,6 +26,7 @@ If the `--allow-empty` option is not given, the commit will be aborted if the in Check if the commit message is conventional format, if not, the commit will be aborted. - `-h`, `--help` Print help - `-s`,`--signoff` Add sign-off-by trailer by the committer at the end of the commit message. +- `--disable-pre` Ignore the pre-commit hook. The commit message should align with Git’s behavior as closely as possible, excpet the `--conventional` option, which is not supported in Git. Refs https://git-scm.com/docs/git-commit for more information. From adb14cc832a25de7b944237db29396b6696401eb Mon Sep 17 00:00:00 2001 From: zine yu Date: Sat, 26 Jul 2025 17:22:19 +0800 Subject: [PATCH 4/5] refactor: update panic message to use string interpolation Signed-off-by: zine yu --- libra/src/command/commit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libra/src/command/commit.rs b/libra/src/command/commit.rs index ce0614797..cf0cc0b56 100644 --- a/libra/src/command/commit.rs +++ b/libra/src/command/commit.rs @@ -72,7 +72,7 @@ pub async fn execute(args: CommitArgs) { .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() - .unwrap_or_else(|e| panic!("Failed to execute hook {}: {}", hook_display, e)); + .unwrap_or_else(|e| panic!("Failed to execute hook {hook_display}: {e}")); #[cfg(target_os = "windows")] let output = Command::new("powershell") @@ -82,7 +82,7 @@ pub async fn execute(args: CommitArgs) { .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() - .unwrap_or_else(|e| panic!("Failed to execute hook {}: {}", hook_display, e)); + .unwrap_or_else(|e| panic!("Failed to execute hook {hook_display}: {e}")); if !output.status.success() { panic!( From 07897905d653a81f88f4431ecc5f04b5d5d20e8c Mon Sep 17 00:00:00 2001 From: zine yu Date: Sat, 26 Jul 2025 17:36:33 +0800 Subject: [PATCH 5/5] test: rename `pre` flag to `disable_pre` in test files Signed-off-by: zine yu --- libra/tests/command/branch_test.rs | 8 ++++---- libra/tests/command/checkout_test.rs | 2 +- libra/tests/command/commit_test.rs | 10 +++++----- libra/tests/command/reset_test.rs | 4 ++++ libra/tests/command/switch_test.rs | 8 ++++---- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/libra/tests/command/branch_test.rs b/libra/tests/command/branch_test.rs index 5895cf787..d86e58902 100644 --- a/libra/tests/command/branch_test.rs +++ b/libra/tests/command/branch_test.rs @@ -17,7 +17,7 @@ async fn test_branch() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(commit_args).await; let first_commit_id = Branch::find_branch("master", None).await.unwrap().commit; @@ -28,7 +28,7 @@ async fn test_branch() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(commit_args).await; let second_commit_id = Branch::find_branch("master", None).await.unwrap().commit; @@ -114,7 +114,7 @@ async fn test_create_branch_from_remote() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; let hash = Head::current_commit().await.unwrap(); @@ -153,7 +153,7 @@ async fn test_invalid_branch_name() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; diff --git a/libra/tests/command/checkout_test.rs b/libra/tests/command/checkout_test.rs index d7b9b0671..b3d9be109 100644 --- a/libra/tests/command/checkout_test.rs +++ b/libra/tests/command/checkout_test.rs @@ -77,7 +77,7 @@ async fn test_checkout_module_functions() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(commit_args).await; diff --git a/libra/tests/command/commit_test.rs b/libra/tests/command/commit_test.rs index fec06071b..54a682483 100644 --- a/libra/tests/command/commit_test.rs +++ b/libra/tests/command/commit_test.rs @@ -19,7 +19,7 @@ async fn test_execute_commit_with_empty_index_fail() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; } @@ -42,7 +42,7 @@ async fn test_execute_commit() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; @@ -68,7 +68,7 @@ async fn test_execute_commit() { conventional: false, amend: true, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; @@ -110,7 +110,7 @@ async fn test_execute_commit() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; @@ -139,7 +139,7 @@ async fn test_execute_commit() { conventional: false, amend: true, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; diff --git a/libra/tests/command/reset_test.rs b/libra/tests/command/reset_test.rs index 2f6038d92..978aba922 100644 --- a/libra/tests/command/reset_test.rs +++ b/libra/tests/command/reset_test.rs @@ -24,6 +24,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1, conventional: false, amend: false, signoff: false, + disable_pre:true, }) .await; let commit1 = Head::current_commit().await.unwrap(); @@ -54,6 +55,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1, conventional: false, amend: false, signoff: false, + disable_pre:true, }) .await; let commit2 = Head::current_commit().await.unwrap(); @@ -84,6 +86,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1, conventional: false, amend: false, signoff: false, + disable_pre:true, }) .await; let commit3 = Head::current_commit().await.unwrap(); @@ -114,6 +117,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1, conventional: false, amend: false, signoff: false, + disable_pre:true, }) .await; let commit4 = Head::current_commit().await.unwrap(); diff --git a/libra/tests/command/switch_test.rs b/libra/tests/command/switch_test.rs index 748a8ee82..a43101a83 100644 --- a/libra/tests/command/switch_test.rs +++ b/libra/tests/command/switch_test.rs @@ -42,7 +42,7 @@ async fn test_switch_function() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; } @@ -85,7 +85,7 @@ async fn test_switch_function() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; @@ -162,7 +162,7 @@ async fn test_detach_head_basic() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; } @@ -202,7 +202,7 @@ async fn test_detach_head_basic() { conventional: false, amend: false, signoff: false, - pre:true, + disable_pre:true, }; commit::execute(args).await; }