diff --git a/src/cli.rs b/src/cli.rs index 6ccf3982a..c34d308be 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1064,8 +1064,9 @@ fn rewrite_reset_pathspec_separator_args(args: Vec) -> Vec { command::reset::RESET_PATHSPEC_SEPARATOR_FLAG )); if !has_target_before_separator && args.get(separator_index + 1).is_some() { - out.push("HEAD".to_string()); + out.push(command::reset::DEFAULT_RESET_TARGET.to_string()); } + out.push("--".to_string()); out.extend(args.iter().skip(separator_index + 1).cloned()); out } diff --git a/src/command/cloud.rs b/src/command/cloud.rs index 9004ca8cb..f93f8a126 100644 --- a/src/command/cloud.rs +++ b/src/command/cloud.rs @@ -2504,6 +2504,22 @@ mod tests { } } + async fn enter_isolated_libra_repo() -> ( + tempfile::TempDir, + tempfile::TempDir, + ScopedEnvVar, + ScopedEnvVar, + ChangeDirGuard, + ) { + let repo = tempdir().unwrap(); + let home = tempdir().unwrap(); + let home_env = ScopedEnvVar::set("HOME", home.path()); + let test_home_env = ScopedEnvVar::set("LIBRA_TEST_HOME", home.path()); + setup_with_new_libra_in(repo.path()).await; + let cwd = ChangeDirGuard::new(repo.path()); + (repo, home, home_env, test_home_env, cwd) + } + struct ClearedEnvVarGuard { key: String, previous: Option, @@ -2882,7 +2898,9 @@ mod tests { } #[tokio::test] + #[serial] async fn cloud_restore_indexed_objects_downloads_skips_and_verifies_hash() { + let _repo = enter_isolated_libra_repo().await; let remote = RemoteStorage::new(Arc::new(InMemory::new())); let local_dir = tempdir().unwrap(); let local = LocalStorage::new(local_dir.path().to_path_buf()); @@ -2917,7 +2935,9 @@ mod tests { } #[tokio::test] + #[serial] async fn cloud_restore_indexed_objects_reports_hash_mismatch() { + let _repo = enter_isolated_libra_repo().await; let remote = RemoteStorage::new(Arc::new(InMemory::new())); let local_dir = tempdir().unwrap(); let local = LocalStorage::new(local_dir.path().to_path_buf()); diff --git a/src/command/reset.rs b/src/command/reset.rs index 9765aebf0..92d197915 100644 --- a/src/command/reset.rs +++ b/src/command/reset.rs @@ -50,7 +50,7 @@ EXAMPLES: libra reset --json --hard HEAD~1 Structured JSON output for agents"; pub(crate) const RESET_PATHSPEC_SEPARATOR_FLAG: &str = "__libra-reset-pathspec-separator"; -const DEFAULT_RESET_TARGET: &str = "HEAD"; +pub(crate) const DEFAULT_RESET_TARGET: &str = "HEAD"; #[derive(Parser, Debug)] #[command(after_help = RESET_EXAMPLES)] @@ -511,20 +511,22 @@ async fn normalize_reset_request(args: &ResetArgs) -> Result Err(ResetError::AmbiguousRevisionPath(target_arg.to_string())), - (true, false) => Ok(ResetRequest { target, pathspecs }), - (false, true) => { - pathspecs.insert(0, target_arg.to_string()); - Ok(ResetRequest { - target: DEFAULT_RESET_TARGET.to_string(), - pathspecs, - }) + if resolves_as_revision { + if pathspec_exists_in_worktree(target_arg) { + return Err(ResetError::AmbiguousRevisionPath(target_arg.to_string())); } - (false, false) => Ok(ResetRequest { target, pathspecs }), + return Ok(ResetRequest { target, pathspecs }); + } + + if pathspec_matches_known_path(target_arg).await? { + pathspecs.insert(0, target_arg.to_string()); + return Ok(ResetRequest { + target: DEFAULT_RESET_TARGET.to_string(), + pathspecs, + }); } + + Ok(ResetRequest { target, pathspecs }) } async fn target_resolves_as_revision(target: &str) -> Result { @@ -567,6 +569,11 @@ async fn pathspec_matches_known_path(pathspec: &str) -> Result find_tree_item(&tree, path_str).map(|item| item.is_some()) } +fn pathspec_exists_in_worktree(pathspec: &str) -> bool { + let absolute = util::workdir_to_absolute(PathBuf::from(pathspec)); + absolute.exists() && util::is_sub_path(&absolute, util::working_dir()) +} + /// Reset specific files in the index to their state in the target commit. /// This function only affects the index, not the working directory. async fn reset_pathspecs( diff --git a/tests/command/file_obliterate_test.rs b/tests/command/file_obliterate_test.rs index 1b61b20ea..1987ea017 100644 --- a/tests/command/file_obliterate_test.rs +++ b/tests/command/file_obliterate_test.rs @@ -6,7 +6,13 @@ //! //! Layer: L1 (deterministic; tempdir + isolated HOME, no network). -use std::fs; +use std::{ + fs, + path::{Path, PathBuf}, + time::Duration, +}; + +use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, Statement}; use super::{ assert_cli_success, create_committed_repo_via_cli, parse_cli_error_stderr, run_libra_command, @@ -36,10 +42,42 @@ fn repo_with_secret() -> (tempfile::TempDir, String) { (repo, oid) } -fn loose_path(repo: &std::path::Path, oid: &str) -> std::path::PathBuf { +fn loose_path(repo: &Path, oid: &str) -> PathBuf { repo.join(".libra/objects").join(&oid[..2]).join(&oid[2..]) } +async fn connect_raw_repo_db(repo: &Path) -> DatabaseConnection { + let db_path = repo.join(".libra").join("libra.db"); + let mut opts = ConnectOptions::new(format!("sqlite://{}", db_path.display())); + opts.sqlx_logging(false) + .connect_timeout(Duration::from_secs(5)); + Database::connect(opts) + .await + .expect("connect raw repository database") +} + +fn mark_obliteration_as_interrupted(repo: &Path) { + let runtime = tokio::runtime::Runtime::new().expect("create runtime for crash-recovery setup"); + runtime.block_on(async { + let conn = connect_raw_repo_db(repo).await; + let result = conn + .execute(Statement::from_string( + conn.get_database_backend(), + "UPDATE object_obliteration SET state='obliterating', payload_deleted_at=NULL", + )) + .await + .expect("reset obliteration state to 'obliterating'"); + assert_eq!( + result.rows_affected(), + 1, + "reset exactly one obliteration row to 'obliterating'" + ); + conn.close() + .await + .expect("close crash-recovery setup database"); + }); +} + #[test] fn obliterate_dry_run_previews_and_deletes_nothing() { let (repo, oid) = repo_with_secret(); @@ -148,17 +186,9 @@ fn obliterate_recover_finishes_interrupted() { "payload restored on disk for the test" ); - // Force the mid-state via sqlite3 — REQUIRED (fail, don't skip, if absent). - let db = p.join(".libra/libra.db"); - let status = std::process::Command::new("sqlite3") - .arg(&db) - .arg("UPDATE object_obliteration SET state='obliterating', payload_deleted_at=NULL;") - .status() - .expect("sqlite3 is required for the crash-recovery test"); - assert!( - status.success(), - "reset the obliteration state to 'obliterating'" - ); + // Force the mid-state in the repository database without relying on host + // sqlite3 binaries, which are not guaranteed on self-hosted runners. + mark_obliteration_as_interrupted(p); // Recovery must re-delete the payload and finalize. let recover = run_libra_command(&["file", "obliterate", "--recover"], p); diff --git a/tests/command/reset_test.rs b/tests/command/reset_test.rs index 96893997a..13dba2287 100644 --- a/tests/command/reset_test.rs +++ b/tests/command/reset_test.rs @@ -372,6 +372,41 @@ fn reset_double_dash_pathspec_unstages_file_named_like_revision() { assert_eq!(String::from_utf8_lossy(&status.stdout), " M HEAD\n"); } +#[test] +fn reset_double_dash_preserves_dash_prefixed_pathspecs() { + // Given: tracked files have names that would otherwise parse as flags. + let repo = create_committed_repo_via_cli(); + for name in ["-h", "--help"] { + fs::write(repo.path().join(name), format!("{name}\n")).unwrap(); + let add = run_libra_command(&["add", "--", name], repo.path()); + assert_cli_success(&add, "add dash-prefixed file"); + } + let commit = run_libra_command( + &["commit", "-m", "add dash-prefixed files", "--no-verify"], + repo.path(), + ); + assert_cli_success(&commit, "commit dash-prefixed files"); + + for name in ["-h", "--help"] { + fs::write(repo.path().join(name), format!("{name}\nupdated\n")).unwrap(); + let add = run_libra_command(&["add", "--", name], repo.path()); + assert_cli_success(&add, "stage dash-prefixed file update"); + + // When: `--` marks the dash-prefixed token as a pathspec. + let output = run_libra_command(&["reset", "--", name], repo.path()); + + // Then: reset treats the token as a filename instead of a reset/help flag. + assert_cli_success(&output, "reset dash-prefixed pathspec"); + let status = run_libra_command(&["status", "--short"], repo.path()); + assert_cli_success(&status, "status --short"); + let stdout = String::from_utf8_lossy(&status.stdout); + assert!( + stdout.contains(&format!(" M {name}\n")), + "expected {name} to be unstaged, got: {stdout}", + ); + } +} + #[test] fn reset_bare_revision_path_ambiguity_errors_like_git() { // Given: a token names both a branch and a tracked path. @@ -410,6 +445,23 @@ fn reset_bare_revision_path_ambiguity_errors_like_git() { assert_eq!(String::from_utf8_lossy(&status.stdout), "M feature\n"); } +#[test] +fn reset_soft_revision_does_not_probe_index_for_pathspec_disambiguation() { + // Given: a normal repository whose index is unreadable. + let repo = create_committed_repo_via_cli(); + fs::write( + repo.path().join(".libra").join("index"), + b"corrupted-index-data", + ) + .unwrap(); + + // When: reset receives an unambiguous revision target. + let output = run_libra_command(&["reset", "--soft", "HEAD"], repo.path()); + + // Then: soft reset resolves the revision without probing pathspec state. + assert_cli_success(&output, "reset --soft HEAD with corrupt index"); +} + #[test] fn test_reset_json_hard_with_pathspec_returns_usage_error() { let repo = create_committed_repo_via_cli(); diff --git a/tests/network_remotes_test.rs b/tests/network_remotes_test.rs index b738442b7..60cea353a 100644 --- a/tests/network_remotes_test.rs +++ b/tests/network_remotes_test.rs @@ -13,10 +13,12 @@ use libra::{ }; use url::Url; +const PUBLIC_FIXTURE_REPO: &str = "https://github.com/libra-tools/libra.git"; +const PUBLIC_FIXTURE_REPO_WEB_URL: &str = "https://github.com/libra-tools/libra/"; + #[tokio::test] async fn https_discovery_upload_pack_lists_refs() { - let client = - HttpsClient::from_url(&Url::parse("https://github.com/libra-tools/mega.git/").unwrap()); + let client = HttpsClient::from_url(&Url::parse(PUBLIC_FIXTURE_REPO).unwrap()); let discovery = client .discovery_reference(UploadPack) .await @@ -34,8 +36,7 @@ async fn https_discovery_upload_pack_lists_refs() { #[tokio::test] async fn https_upload_pack_returns_pack_data() { - let client = - HttpsClient::from_url(&Url::parse("https://github.com/libra-tools/mega/").unwrap()); + let client = HttpsClient::from_url(&Url::parse(PUBLIC_FIXTURE_REPO_WEB_URL).unwrap()); let discovery = client .discovery_reference(UploadPack) .await @@ -77,8 +78,7 @@ async fn github_lfs_batch_download_returns_response() { }], hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let lfs_client = - LFSClient::from_url(&Url::parse("https://github.com/libra-tools/mega.git").unwrap()); + let lfs_client = LFSClient::from_url(&Url::parse(PUBLIC_FIXTURE_REPO).unwrap()); let response = lfs_client .client .post(lfs_client.batch_url.clone())