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
8 changes: 7 additions & 1 deletion src/authorship/rebase_authorship.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,13 @@ pub fn reconstruct_working_log_after_reset(
let pathspecs: Vec<String> = if let Some(user_paths) = user_pathspecs {
all_changed_files
.into_iter()
.filter(|f| user_paths.iter().any(|p| f == p || f.starts_with(p)))
.filter(|f| {
user_paths.iter().any(|p| {
f == p
|| (p.ends_with('/') && f.starts_with(p))
|| f.starts_with(&format!("{}/", p))
})
})
.collect()
} else {
all_changed_files
Expand Down
6 changes: 5 additions & 1 deletion src/commands/hooks/checkout_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,9 @@ fn remove_attributions_for_pathspecs(repository: &Repository, head: &str, pathsp
}

fn matches_any_pathspec(file: &str, pathspecs: &[String]) -> bool {
pathspecs.iter().any(|p| file == p || file.starts_with(p))
pathspecs.iter().any(|p| {
file == p
|| (p.ends_with('/') && file.starts_with(p))
|| file.starts_with(&format!("{}/", p))
})
}
8 changes: 5 additions & 3 deletions src/commands/hooks/reset_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,11 @@ fn handle_reset_pathspec_preserve_working_dir(
let mut non_pathspec_checkpoints = Vec::new();
for mut checkpoint in existing_checkpoints {
checkpoint.entries.retain(|entry| {
!pathspecs
.iter()
.any(|pathspec| entry.file == *pathspec || entry.file.starts_with(pathspec))
!pathspecs.iter().any(|pathspec| {
entry.file == *pathspec
|| (pathspec.ends_with('/') && entry.file.starts_with(pathspec))
|| entry.file.starts_with(&format!("{}/", pathspec))
})
});
if !checkpoint.entries.is_empty() {
non_pathspec_checkpoints.push(checkpoint);
Expand Down
61 changes: 61 additions & 0 deletions tests/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mod repos;
use repos::test_file::ExpectedLineExt;
use repos::test_repo::TestRepo;
use std::fs;

/// Test git reset --hard: should discard all changes and reset to target commit
#[test]
Expand Down Expand Up @@ -542,3 +543,63 @@ fn test_reset_mixed_pathspec_multiple_commits() {
"// More lib".ai(),
]);
}

/// Test git reset with directory pathspec: should reset only files in the specified directory
#[test]
fn test_reset_with_directory_pathspec() {
let repo = TestRepo::new();

// Create directory structure
fs::create_dir_all(repo.path().join("src")).unwrap();
fs::create_dir_all(repo.path().join("lib")).unwrap();

let mut src_file = repo.filename("src/app.rs");
let mut lib_file = repo.filename("lib/utils.rs");
let mut root_file = repo.filename("root.txt");

// Base commit with files in different directories
src_file.set_contents(lines!["fn main() {}", ""]);
lib_file.set_contents(lines!["pub fn helper() {}", ""]);
root_file.set_contents(lines!["root content", ""]);
let base_commit = repo.stage_all_and_commit("Base commit").unwrap();

// Second commit: AI modifies files in all directories
src_file.insert_at(1, lines![" // AI src change".ai()]);
lib_file.insert_at(1, lines![" // AI lib change".ai()]);
root_file.insert_at(1, lines!["// AI root change".ai()]);
repo.stage_all_and_commit("AI changes everywhere").unwrap();

// Make uncommitted AI changes to lib and root (not src)
lib_file.insert_at(2, lines![" // More AI lib".ai()]);
root_file.insert_at(2, lines!["// More AI root".ai()]);

// Reset only the src directory to base commit using directory pathspec
repo.git(&["reset", &base_commit.commit_sha, "--", "src"])
.expect("reset with directory pathspec should succeed");

// Stage all and commit to verify attributions
let new_commit = repo
.stage_all_and_commit("After directory pathspec reset")
.unwrap();

assert!(
!new_commit.authorship_log.attestations.is_empty(),
"AI authorship should be preserved for lib and root files"
);

// lib/utils.rs should still have AI changes (not in reset pathspec)
lib_file = repo.filename("lib/utils.rs");
lib_file.assert_lines_and_blame(lines![
"pub fn helper() {}".human(),
" // AI lib change".ai(),
" // More AI lib".ai(),
]);

// root.txt should still have AI changes (not in reset pathspec)
root_file = repo.filename("root.txt");
root_file.assert_lines_and_blame(lines![
"root content".human(),
"// AI root change".ai(),
"// More AI root".ai(),
]);
}
Loading