From c89e152ec1e12c14a225467f21a77cc814373f86 Mon Sep 17 00:00:00 2001 From: Jiaqi Zhou <1405758738@qq.com> Date: Sat, 16 Aug 2025 17:22:41 +0800 Subject: [PATCH 1/3] feat(init): add --template option, Issue: #1340 Signed-off-by: Jiaqi Zhou <1405758738@qq.com> --- .../docs/libra/command/init/index.mdx | 2 + libra/src/command/clone.rs | 1 + libra/src/command/init.rs | 86 ++++++++---- libra/src/utils/test.rs | 1 + libra/tests/command/checkout_test.rs | 1 + libra/tests/command/init_test.rs | 124 ++++++++++++++++++ 6 files changed, 187 insertions(+), 28 deletions(-) diff --git a/aria/contents/docs/libra/command/init/index.mdx b/aria/contents/docs/libra/command/init/index.mdx index 1e46b888f..a675d11e1 100644 --- a/aria/contents/docs/libra/command/init/index.mdx +++ b/aria/contents/docs/libra/command/init/index.mdx @@ -22,6 +22,8 @@ which sets up the repository without a working directory, containing only Libra **Note**:Unlike Git's `--bare`, the Libra init `--bare` initializes a bare repository that contains Libra's own SQLite database. +- `--template ` + Specify a directory from which templates will be used to initialize the repository. - `-b`, `--initial-branch ` Override the name of the initial branch - `-h`, `--help` Print help diff --git a/libra/src/command/clone.rs b/libra/src/command/clone.rs index d81147b7b..46d872ed0 100644 --- a/libra/src/command/clone.rs +++ b/libra/src/command/clone.rs @@ -88,6 +88,7 @@ pub async fn execute(args: CloneArgs) { initial_branch: args.branch.clone(), repo_directory: local_path.to_str().unwrap().to_string(), quiet: false, + template: None, }; command::init::execute(init_args).await; diff --git a/libra/src/command/init.rs b/libra/src/command/init.rs index 502a106eb..6bdef8a0d 100644 --- a/libra/src/command/init.rs +++ b/libra/src/command/init.rs @@ -23,6 +23,10 @@ pub struct InitArgs { #[clap(long, required = false)] pub bare: bool, // Default is false + /// directory from which templates will be used + #[clap(long = "template", name = "template-directory", required = false)] + pub template: Option, + /// Set the initial branch name #[clap(short = 'b', long, required = false)] pub initial_branch: Option, @@ -82,6 +86,22 @@ fn is_writable(cur_dir: &Path) -> io::Result<()> { Ok(()) } +/// Recursively copy the contents of the template directory to the destination directory +fn copy_template(src: &Path, dest: &Path) -> io::Result<()> { + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dest.join(entry.file_name()); + if path.is_dir() { + fs::create_dir_all(&dest_path)?; + copy_template(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path)?; + } + } + Ok(()) +} + /// Initialize a new Libra repository /// This function creates the necessary directories and files for a new Libra repository. /// It also sets up the database and the initial configuration. @@ -132,37 +152,47 @@ pub async fn init(args: InitArgs) -> io::Result<()> { for dir in dirs { fs::create_dir_all(root_dir.join(dir))?; } - // Create info/exclude - // `include_str!` includes the file content while compiling - fs::write( - root_dir.join("info/exclude"), - include_str!("../../template/exclude"), - )?; - // Create .libra/description - fs::write( - 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)?; + if let Some(template_path) = &args.template { + let template_dir = Path::new(template_path); + if !template_dir.is_dir() { + return Err(io::Error::new( + ErrorKind::NotFound, + format!("template directory '{}' not found", template_path), + )); + } + copy_template(template_dir, &root_dir)?; + } else { + // Create info/exclude + // `include_str!` includes the file content while compiling + fs::write( + root_dir.join("info/exclude"), + include_str!("../../template/exclude"), + )?; + // Create .libra/description + fs::write( + 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 .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; let database = root_dir.join(DATABASE); diff --git a/libra/src/utils/test.rs b/libra/src/utils/test.rs index d951d99a7..3146e3b83 100644 --- a/libra/src/utils/test.rs +++ b/libra/src/utils/test.rs @@ -106,6 +106,7 @@ pub async fn setup_with_new_libra_in(temp_path: impl AsRef) { initial_branch: None, repo_directory: temp_path.as_ref().to_str().unwrap().to_string(), quiet: false, + template: None, }; command::init::init(args).await.unwrap(); } diff --git a/libra/tests/command/checkout_test.rs b/libra/tests/command/checkout_test.rs index 99227f851..b6c9b42e1 100644 --- a/libra/tests/command/checkout_test.rs +++ b/libra/tests/command/checkout_test.rs @@ -64,6 +64,7 @@ async fn test_checkout_module_functions() { initial_branch: Some("main".to_string()), repo_directory: temp_path.path().to_str().unwrap().to_string(), quiet: false, + template: None, }; init::init(init_args) diff --git a/libra/tests/command/init_test.rs b/libra/tests/command/init_test.rs index 93c704414..644119187 100644 --- a/libra/tests/command/init_test.rs +++ b/libra/tests/command/init_test.rs @@ -32,6 +32,7 @@ async fn test_init() { initial_branch: None, repo_directory: target_dir.to_str().unwrap().to_string(), quiet: false, + template: None, }; // Run the init function init(args).await.unwrap(); @@ -44,6 +45,119 @@ async fn test_init() { verify_init(libra_dir.as_path()); } +#[tokio::test] +#[serial] +/// Test the init function with a template directory +async fn test_init_template() { + use std::fs; + use tempfile::tempdir; + + // Create a temporary target directory for the new repo + let target_dir = tempdir().unwrap().keep(); + + // Create a temporary template directory + let template_dir = tempdir().unwrap(); + + // Set up template structure similar to Git template + fs::create_dir_all(template_dir.path().join("objects/pack")).unwrap(); + fs::create_dir_all(template_dir.path().join("objects/info")).unwrap(); + fs::create_dir_all(template_dir.path().join("info")).unwrap(); + + // Add description file in the template + fs::write( + template_dir.path().join("description"), + "Template repository", + ) + .unwrap(); + + // Add info/exclude file in the template + fs::write(template_dir.path().join("info/exclude"), "").unwrap(); + + // Prepare init arguments with template path + let args = InitArgs { + bare: false, + initial_branch: None, + repo_directory: target_dir.to_str().unwrap().to_string(), + quiet: false, + template: Some(template_dir.path().to_str().unwrap().to_string()), + }; + + // Run the init function + init(args).await.unwrap(); + + // Verify that the `.libra` directory exists + let libra_dir = target_dir.join(".libra"); + assert!(libra_dir.exists(), ".libra directory does not exist"); + + // Verify the repository initialization structure + verify_init(libra_dir.as_path()); + + // --- Additional checks for template contents --- + + // Verify that description file is copied from template + let description_path = libra_dir.join("description"); + assert!( + description_path.exists(), + "Template description file not copied" + ); + + // Verify that info/exclude file is copied from template + let exclude_path = libra_dir.join("info/exclude"); + assert!( + exclude_path.exists(), + "Template info/exclude file not copied" + ); + + // Verify that objects subdirectories are copied from template + assert!( + libra_dir.join("objects/pack").exists(), + "Template objects/pack directory not copied" + ); + assert!( + libra_dir.join("objects/info").exists(), + "Template objects/info directory not copied" + ); +} + + +#[tokio::test] +#[serial] +/// Test the init function with an invalid template path +async fn test_init_with_invalid_template_path() { + use tempfile::tempdir; + + // Create a temporary target directory for the new repo + let target_dir = tempdir().unwrap().keep(); + + // Provide a non-existent template path + let invalid_template_path = "/path/to/nonexistent/template"; + + let args = InitArgs { + bare: false, + initial_branch: None, + repo_directory: target_dir.to_str().unwrap().to_string(), + quiet: false, + template: Some(invalid_template_path.to_string()), + }; + + // Run the init function and expect it to return an error + let result = init(args).await; + + // Verify that the function returns an error due to invalid template path + assert!( + result.is_err(), + "Init should fail when template path does not exist" + ); + + // Optionally, verify the error kind/message if your init function provides it + if let Err(err) = result { + // Uncomment and adjust depending on your error type + // assert_eq!(err.kind(), Some(ExpectedErrorKind::NotFound)); + println!("Received expected error: {:?}", err); + } +} + + #[tokio::test] #[serial] /// Test the init function with the --bare flag @@ -57,6 +171,7 @@ async fn test_init_bare() { initial_branch: None, repo_directory: target_dir.to_str().unwrap().to_string(), quiet: false, + template: None, }; // Run the init function init(args).await.unwrap(); @@ -64,6 +179,7 @@ async fn test_init_bare() { // Verify the contents of the other directory verify_init(target_dir.as_path()); } + #[tokio::test] #[serial] /// Test the init function with the --bare flag and an existing repository @@ -76,6 +192,7 @@ async fn test_init_bare_with_existing_repo() { initial_branch: None, repo_directory: target_dir.to_str().unwrap().to_string(), quiet: false, + template: None, }; init(init_args).await.unwrap(); // Execute init for bare repository @@ -86,6 +203,7 @@ async fn test_init_bare_with_existing_repo() { initial_branch: None, repo_directory: target_dir.to_str().unwrap().to_string(), quiet: false, + template: None, }; init(args).await }; @@ -110,6 +228,7 @@ async fn test_init_with_initial_branch() { initial_branch: Some("main".to_string()), repo_directory: temp_path.path().to_str().unwrap().to_string(), quiet: false, + template: None, }; // Run the init function init(args).await.unwrap(); @@ -157,6 +276,7 @@ async fn test_invalid_branch_name(branch_name: &str) { initial_branch: Some(branch_name.to_string()), repo_directory: target_dir.to_str().unwrap().to_string(), quiet: false, + template: None, }; // Run the init function let result = init(args).await; @@ -180,6 +300,7 @@ async fn test_init_with_directory() { initial_branch: None, repo_directory: test_dir.to_str().unwrap().to_owned(), quiet: false, + template: None, }; // Run the init function init(args).await.unwrap(); @@ -209,6 +330,7 @@ async fn test_init_with_invalid_directory() { initial_branch: None, repo_directory: test_dir.to_str().unwrap().to_owned(), quiet: false, + template: None, }; // Run the init function let result = init(args).await; @@ -249,6 +371,7 @@ async fn test_init_with_unauthorized_directory() { initial_branch: None, repo_directory: test_dir.to_str().unwrap().to_owned(), quiet: false, + template: None, }; // Run the init function let result = init(args).await; @@ -272,6 +395,7 @@ async fn test_init_quiet() { initial_branch: None, repo_directory: target_dir.to_str().unwrap().to_string(), quiet: true, + template: None, }; // Run the init function init(args).await.unwrap(); From fd399f50d7636098294d891ff8e7622249244728 Mon Sep 17 00:00:00 2001 From: Jiaqi Zhou <1405758738@qq.com> Date: Sat, 16 Aug 2025 22:35:21 +0800 Subject: [PATCH 2/3] fix(init): prevent default files from overwriting custom template Signed-off-by: Jiaqi Zhou <1405758738@qq.com> --- libra/src/command/init.rs | 47 ++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/libra/src/command/init.rs b/libra/src/command/init.rs index 6bdef8a0d..50d94276c 100644 --- a/libra/src/command/init.rs +++ b/libra/src/command/init.rs @@ -87,16 +87,20 @@ fn is_writable(cur_dir: &Path) -> io::Result<()> { } /// Recursively copy the contents of the template directory to the destination directory -fn copy_template(src: &Path, dest: &Path) -> io::Result<()> { +fn copy_template(src: &Path, dst: &Path) -> io::Result<()> { for entry in fs::read_dir(src)? { let entry = entry?; - let path = entry.path(); - let dest_path = dest.join(entry.file_name()); - if path.is_dir() { + let file_type = entry.file_type()?; + let dest_path = dst.join(entry.file_name()); + + if file_type.is_dir() { fs::create_dir_all(&dest_path)?; - copy_template(&path, &dest_path)?; + copy_template(&entry.path(), &dest_path)?; } else { - fs::copy(&path, &dest_path)?; + if !dest_path.exists() { + // Only copy if the file does not already exist + fs::copy(entry.path(), &dest_path)?; + } } } Ok(()) @@ -147,22 +151,26 @@ pub async fn init(args: InitArgs) -> io::Result<()> { } } - // Create .libra & sub-dirs - let dirs = ["objects/pack", "objects/info", "info", "hooks"]; - for dir in dirs { - fs::create_dir_all(root_dir.join(dir))?; - } + // ensure root dir exists + fs::create_dir_all(&root_dir)?; + // If a template path is provided, copy the template files to the root directory if let Some(template_path) = &args.template { let template_dir = Path::new(template_path); - if !template_dir.is_dir() { + if template_dir.exists() { + copy_template(template_dir, &root_dir)?; + } else { return Err(io::Error::new( - ErrorKind::NotFound, - format!("template directory '{}' not found", template_path), + io::ErrorKind::NotFound, + format!("template directory '{}' does not exist", template_path), )); } - copy_template(template_dir, &root_dir)?; } else { + // Create info & hooks + let dirs = ["info", "hooks"]; + for dir in dirs { + fs::create_dir_all(root_dir.join(dir))?; + } // Create info/exclude // `include_str!` includes the file content while compiling fs::write( @@ -179,6 +187,7 @@ pub async fn init(args: InitArgs) -> io::Result<()> { root_dir.join("hooks").join("pre-commit.sh"), include_str!("../../template/pre-commit.sh"), )?; + // Set Permission #[cfg(not(target_os = "windows"))] { @@ -186,6 +195,7 @@ pub async fn init(args: InitArgs) -> io::Result<()> { 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"), @@ -193,6 +203,12 @@ pub async fn init(args: InitArgs) -> io::Result<()> { )?; } + // Complete .libra and sub-directories + let dirs = ["objects/pack", "objects/info"]; + for dir in dirs { + fs::create_dir_all(root_dir.join(dir))?; + } + // Create database: .libra/libra.db let conn; let database = root_dir.join(DATABASE); @@ -236,6 +252,7 @@ pub async fn init(args: InitArgs) -> io::Result<()> { Ok(()) } + /// Initialize the configuration for the Libra repository /// This function creates the necessary configuration entries in the database. async fn init_config(conn: &DbConn) -> Result<(), DbErr> { From 1f6d7eb00aaed7dfe556257dd33c8607d5af8ef0 Mon Sep 17 00:00:00 2001 From: Jiaqi Zhou <1405758738@qq.com> Date: Sat, 16 Aug 2025 23:15:21 +0800 Subject: [PATCH 3/3] style(init): fix formatting and collapse else-if to satisfy Clippy Signed-off-by: Jiaqi Zhou <1405758738@qq.com> --- libra/src/command/init.rs | 15 +++++++++------ libra/tests/command/init_test.rs | 2 -- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libra/src/command/init.rs b/libra/src/command/init.rs index 50d94276c..61e73769d 100644 --- a/libra/src/command/init.rs +++ b/libra/src/command/init.rs @@ -86,7 +86,12 @@ fn is_writable(cur_dir: &Path) -> io::Result<()> { Ok(()) } -/// Recursively copy the contents of the template directory to the destination directory +/// Recursively copy the contents of the template directory to the destination directory. +/// +/// # Behavior +/// - Directories are created as needed. +/// - Existing files in `dst` are NOT overwritten. +/// - Subdirectories are copied recursively. fn copy_template(src: &Path, dst: &Path) -> io::Result<()> { for entry in fs::read_dir(src)? { let entry = entry?; @@ -96,11 +101,9 @@ fn copy_template(src: &Path, dst: &Path) -> io::Result<()> { if file_type.is_dir() { fs::create_dir_all(&dest_path)?; copy_template(&entry.path(), &dest_path)?; - } else { - if !dest_path.exists() { - // Only copy if the file does not already exist - fs::copy(entry.path(), &dest_path)?; - } + } else if !dest_path.exists() { + // Only copy if the file does not already exist + fs::copy(entry.path(), &dest_path)?; } } Ok(()) diff --git a/libra/tests/command/init_test.rs b/libra/tests/command/init_test.rs index 644119187..11b3e1c00 100644 --- a/libra/tests/command/init_test.rs +++ b/libra/tests/command/init_test.rs @@ -119,7 +119,6 @@ async fn test_init_template() { ); } - #[tokio::test] #[serial] /// Test the init function with an invalid template path @@ -157,7 +156,6 @@ async fn test_init_with_invalid_template_path() { } } - #[tokio::test] #[serial] /// Test the init function with the --bare flag