Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1064,8 +1064,9 @@ fn rewrite_reset_pathspec_separator_args(args: Vec<String>) -> Vec<String> {
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
}
Expand Down
20 changes: 20 additions & 0 deletions src/command/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OsString>,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
33 changes: 20 additions & 13 deletions src/command/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -511,20 +511,22 @@ async fn normalize_reset_request(args: &ResetArgs) -> Result<ResetRequest, Reset
return Ok(ResetRequest { target, pathspecs });
};
let resolves_as_revision = target_resolves_as_revision(target_arg).await?;
let matches_path = pathspec_matches_known_path(target_arg).await?;

match (resolves_as_revision, matches_path) {
(true, true) => 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<bool, ResetError> {
Expand Down Expand Up @@ -567,6 +569,11 @@ async fn pathspec_matches_known_path(pathspec: &str) -> Result<bool, ResetError>
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(
Expand Down
56 changes: 43 additions & 13 deletions tests/command/file_obliterate_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions tests/command/reset_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 6 additions & 6 deletions tests/network_remotes_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down