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