Skip to content

Commit da40d58

Browse files
authored
Merge branch 'main' into main
2 parents bd75d24 + e4e0ab2 commit da40d58

48 files changed

Lines changed: 2419 additions & 557 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

aria/contents/docs/libra/command/rm/index.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ that.)
2626

2727
- `-r`, `--recursive`<br/>
2828
Allow recursive removal when a leading directory name is given.
29+
30+
- `-f`, `--force`<br/>
31+
Force removal even if the specified paths are not tracked. This skips validation checks and deletes files or directories regardless of their index status.

libra/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ tracing-subscriber = { workspace = true }
5252
wax = { workspace = true }
5353
url = { workspace = true }
5454
ignore = { workspace = true }
55+
tempfile = { workspace = true }
56+
serial_test = { workspace = true }
5557

5658
[target.'cfg(unix)'.dependencies] # only on Unix
5759
pager = { workspace = true }

libra/src/command/remove.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ use mercury::internal::index::Index;
1313
#[derive(Parser, Debug)]
1414
pub struct RemoveArgs {
1515
/// file or dir to remove
16-
pathspec: Vec<String>,
16+
pub pathspec: Vec<String>,
1717
/// whether to remove from index
1818
#[clap(long)]
19-
cached: bool,
19+
pub cached: bool,
2020
/// indicate recursive remove dir
2121
#[clap(short, long)]
22-
recursive: bool,
22+
pub recursive: bool,
23+
/// force removal, skip validation
24+
#[clap(short, long)]
25+
pub force: bool,
2326
}
2427

2528
pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
@@ -28,11 +31,13 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
2831
}
2932
let idx_file = path::index();
3033
let mut index = Index::load(&idx_file)?;
31-
// check if pathspec is all in index
32-
if !validate_pathspec(&args.pathspec, &index) {
34+
35+
// check if pathspec is all in index (skip if force is enabled)
36+
if !args.force && !validate_pathspec(&args.pathspec, &index) {
3337
return Ok(());
3438
}
35-
let dirs = get_dirs(&args.pathspec, &index);
39+
40+
let dirs = get_dirs(&args.pathspec, &index, args.force);
3641
if !dirs.is_empty() && !args.recursive {
3742
println!(
3843
"fatal: not removing '{}' recursively without -r",
@@ -44,6 +49,7 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
4449
for path_str in args.pathspec.iter() {
4550
let path = PathBuf::from(path_str);
4651
let path_wd = path.to_workdir().to_string_or_panic();
52+
4753
if dirs.contains(path_str) {
4854
// dir
4955
let removed = index.remove_dir_files(&path_wd);
@@ -56,8 +62,20 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> {
5662
}
5763
} else {
5864
// file
59-
index.remove(&path_wd, 0);
60-
println!("rm '{}'", path_wd.bright_green());
65+
if args.force {
66+
// In force mode, remove from index if tracked, otherwise just delete from filesystem
67+
if index.tracked(&path_wd, 0) {
68+
index.remove(&path_wd, 0);
69+
println!("rm '{}'", path_wd.bright_green());
70+
} else {
71+
println!("rm '{}'", path_wd.bright_yellow());
72+
}
73+
} else {
74+
// Normal mode - only remove if tracked
75+
index.remove(&path_wd, 0);
76+
println!("rm '{}'", path_wd.bright_green());
77+
}
78+
6179
if !args.cached {
6280
fs::remove_file(&path)?;
6381
}
@@ -90,14 +108,22 @@ fn validate_pathspec(pathspec: &[String], index: &Index) -> bool {
90108
}
91109

92110
/// run after `validate_pathspec`
93-
fn get_dirs(pathspec: &[String], index: &Index) -> Vec<String> {
111+
fn get_dirs(pathspec: &[String], index: &Index, force: bool) -> Vec<String> {
94112
let mut dirs = Vec::new();
95113
for path_str in pathspec.iter() {
96114
let path = PathBuf::from(path_str);
97115
let path_wd = path.to_workdir().to_string_or_panic();
98-
// valid but not tracked, means a dir
99-
if !index.tracked(&path_wd, 0) {
100-
dirs.push(path_str.clone());
116+
117+
if force {
118+
// In force mode, check if the path exists and is a directory
119+
if path.exists() && path.is_dir() {
120+
dirs.push(path_str.clone());
121+
}
122+
} else {
123+
// valid but not tracked, means a dir
124+
if !index.tracked(&path_wd, 0) {
125+
dirs.push(path_str.clone());
126+
}
101127
}
102128
}
103129
dirs

libra/tests/command/fetch_test.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,110 @@
1+
use std::process::Command;
2+
use std::time::Duration;
3+
use tempfile::TempDir;
4+
use tokio::process::Command as TokioCommand;
5+
use tokio::time::timeout;
16

7+
/// Helper function: Initialize a temporary Libra repository
8+
fn init_temp_repo() -> TempDir {
9+
let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
10+
let temp_path = temp_dir.path();
11+
12+
eprintln!("Temporary directory created at: {temp_path:?}");
13+
assert!(temp_path.is_dir(), "Temporary path is not a valid directory");
14+
15+
let output = Command::new(env!("CARGO_BIN_EXE_libra"))
16+
.current_dir(temp_path)
17+
.arg("init")
18+
.output()
19+
.expect("Failed to execute libra binary");
20+
21+
if !output.status.success() {
22+
panic!(
23+
"Failed to initialize libra repository: {}",
24+
String::from_utf8_lossy(&output.stderr)
25+
);
26+
}
27+
28+
eprintln!("Initialized libra repo at: {temp_path:?}");
29+
temp_dir
30+
}
31+
32+
#[tokio::test]
33+
/// Test fetching from an invalid remote repository with timeout
34+
async fn test_fetch_invalid_remote() {
35+
let temp_repo = init_temp_repo();
36+
let temp_path = temp_repo.path();
37+
38+
eprintln!("Starting test: fetch from invalid remote");
39+
40+
// Configure an invalid remote repository
41+
eprintln!("Adding invalid remote: https://invalid-url.example/repo.git");
42+
let remote_output = TokioCommand::new(env!("CARGO_BIN_EXE_libra"))
43+
.current_dir(temp_path)
44+
.args(["remote", "add", "origin", "https://invalid-url.example/repo.git"])
45+
.output()
46+
.await
47+
.expect("Failed to add remote");
48+
49+
assert!(
50+
remote_output.status.success(),
51+
"Failed to add remote: {}",
52+
String::from_utf8_lossy(&remote_output.stderr)
53+
);
54+
55+
// Set upstream branch
56+
eprintln!("Setting upstream to origin/main");
57+
let branch_output = TokioCommand::new(env!("CARGO_BIN_EXE_libra"))
58+
.current_dir(temp_path)
59+
.args(["branch", "--set-upstream-to", "origin/main"])
60+
.output()
61+
.await
62+
.expect("Failed to set upstream branch");
63+
64+
assert!(
65+
branch_output.status.success(),
66+
"Failed to set upstream: {}",
67+
String::from_utf8_lossy(&branch_output.stderr)
68+
);
69+
70+
// Attempt to fetch with 15-second timeout to avoid hanging CI
71+
eprintln!("Attempting 'libra fetch' with 15s timeout...");
72+
let fetch_result = timeout(Duration::from_secs(15), async {
73+
TokioCommand::new(env!("CARGO_BIN_EXE_libra"))
74+
.current_dir(temp_path)
75+
.arg("fetch")
76+
.output()
77+
.await
78+
})
79+
.await;
80+
81+
match fetch_result {
82+
// Timeout occurred — this is expected for unreachable remotes
83+
Err(_) => {
84+
eprintln!("Fetch timed out after 15 seconds — expected for invalid remote");
85+
}
86+
// Command completed within timeout
87+
Ok(Ok(output)) => {
88+
89+
eprintln!("Fetch completed (status: {:?})", output.status);
90+
assert!(
91+
!output.status.success(),
92+
"Fetch should fail when remote is unreachable"
93+
);
94+
let stderr = String::from_utf8_lossy(&output.stderr);
95+
assert!(
96+
!stderr.trim().is_empty(),
97+
"Expected error message in stderr, but was empty"
98+
);
99+
100+
eprintln!("Fetch failed as expected: {stderr}");
101+
}
102+
// Failed to start the command
103+
Ok(Err(e)) => {
104+
105+
panic!("Failed to run 'libra fetch' command: {e}");
106+
}
107+
}
108+
109+
eprintln!("test_fetch_invalid_remote passed");
110+
}

libra/tests/command/lfs_test.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,107 @@
1+
use std::process::Command;
2+
use tempfile::TempDir;
13

4+
/// Helper function: Initialize a temporary Libra repository
5+
fn init_temp_repo() -> TempDir {
6+
let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
7+
let temp_path = temp_dir.path();
8+
9+
// Variables can be used directly in the `format!` string
10+
// FIX: Removed {:?} and added variable directly with formatting
11+
println!("Temporary directory created at: {temp_path:?}");
12+
assert!(temp_path.is_dir(), "Temporary path is not a valid directory");
13+
14+
// Using env!("CARGO_BIN_EXE_libra") to get the path to the libra executable
15+
let output = Command::new(env!("CARGO_BIN_EXE_libra"))
16+
.current_dir(temp_path)
17+
.arg("init")
18+
.output()
19+
.expect("Failed to execute libra binary");
20+
21+
if !output.status.success() {
22+
panic!(
23+
"Failed to initialize libra repository: {}",
24+
String::from_utf8_lossy(&output.stderr)
25+
);
26+
}
27+
28+
temp_dir
29+
}
30+
31+
#[tokio::test]
32+
/// Test track/untrack path rule management
33+
async fn test_lfs_track_untrack() {
34+
let temp_repo = init_temp_repo();
35+
let temp_path = temp_repo.path();
36+
37+
// Add a path rule
38+
// FIX: Removed & from args
39+
let track_output = Command::new(env!("CARGO_BIN_EXE_libra"))
40+
.current_dir(temp_path)
41+
.args(["lfs", "track", "*.txt"]) // Changed &[...] to [...]
42+
.output()
43+
.expect("Failed to track path");
44+
assert!(
45+
track_output.status.success(),
46+
"Failed to track path: {}",
47+
String::from_utf8_lossy(&track_output.stderr)
48+
);
49+
50+
// Remove a path rule
51+
// FIX: Removed & from args
52+
let untrack_output = Command::new(env!("CARGO_BIN_EXE_libra"))
53+
.current_dir(temp_path)
54+
.args(["lfs", "untrack", "*.txt"]) // Changed &[...] to [...]
55+
.output()
56+
.expect("Failed to untrack path");
57+
assert!(
58+
untrack_output.status.success(),
59+
"Failed to untrack path: {}",
60+
String::from_utf8_lossy(&untrack_output.stderr)
61+
);
62+
}
63+
64+
#[tokio::test]
65+
/// Test file status viewing
66+
async fn test_lfs_ls_files() {
67+
let temp_repo = init_temp_repo();
68+
let temp_path = temp_repo.path();
69+
70+
// Create a test file and add it to LFS
71+
let file_path = temp_path.join("tracked_file.txt");
72+
std::fs::write(&file_path, "Tracked content").expect("Failed to create tracked file");
73+
74+
// FIX: Removed & from args
75+
Command::new(env!("CARGO_BIN_EXE_libra"))
76+
.current_dir(temp_path)
77+
.args(["lfs", "track", "*.txt"]) // Changed &[...] to [...]
78+
.output()
79+
.expect("Failed to track file");
80+
81+
// FIX: Removed & from args
82+
Command::new(env!("CARGO_BIN_EXE_libra"))
83+
.current_dir(temp_path)
84+
.args(["add", "tracked_file.txt"]) // Changed &[...] to [...]
85+
.output()
86+
.expect("Failed to add file to LFS");
87+
88+
// View file status
89+
// FIX: Removed & from args
90+
let ls_files_output = Command::new(env!("CARGO_BIN_EXE_libra"))
91+
.current_dir(temp_path)
92+
.args(["lfs", "ls-files"]) // Changed &[...] to [...]
93+
.output()
94+
.expect("Failed to list LFS files");
95+
assert!(
96+
ls_files_output.status.success(),
97+
"Failed to list LFS files: {}",
98+
String::from_utf8_lossy(&ls_files_output.stderr)
99+
);
100+
101+
let stdout = String::from_utf8_lossy(&ls_files_output.stdout);
102+
// FIX: Variables can be used directly in the `format!` string
103+
assert!(
104+
stdout.contains("tracked_file.txt"),
105+
"LFS file list does not contain expected file: {stdout}", // Changed {} to direct variable embed
106+
);
107+
}

0 commit comments

Comments
 (0)