From 9b52008c1aa8ef5fe1b23735f23d6109d5d7da84 Mon Sep 17 00:00:00 2001 From: "Aidan Cunniffe (windows machine)" Date: Sat, 8 Nov 2025 09:38:16 -0500 Subject: [PATCH 01/19] fixes --- scripts/dev-symlinks.sh | 0 src/authorship/post_commit.rs | 1 + src/authorship/rebase_authorship.rs | 7 ++++++ src/commands/checkpoint.rs | 25 +++++++++++++++++++--- src/commands/git_ai_handlers.rs | 3 +++ tests/amend.rs | 1 - tests/git-compat/run.py | 0 tests/github/scripts/cleanup-test-repos.sh | 0 tests/github/scripts/run-github-tests.sh | 0 tests/repos/test_file.rs | 4 +++- tests/repos/test_repo.rs | 2 +- 11 files changed, 37 insertions(+), 6 deletions(-) mode change 100755 => 100644 scripts/dev-symlinks.sh mode change 100755 => 100644 tests/git-compat/run.py mode change 100755 => 100644 tests/github/scripts/cleanup-test-repos.sh mode change 100755 => 100644 tests/github/scripts/run-github-tests.sh diff --git a/scripts/dev-symlinks.sh b/scripts/dev-symlinks.sh old mode 100755 new mode 100644 diff --git a/src/authorship/post_commit.rs b/src/authorship/post_commit.rs index a11b5036..bc121c4f 100644 --- a/src/authorship/post_commit.rs +++ b/src/authorship/post_commit.rs @@ -54,6 +54,7 @@ pub fn post_commit( .flat_map(|cp| cp.entries.iter().map(|e| e.file.clone())) .collect(); + // Split VirtualAttributions into committed (authorship log) and uncommitted (INITIAL) let (mut authorship_log, initial_attributions) = working_va .to_authorship_log_and_initial_working_log( diff --git a/src/authorship/rebase_authorship.rs b/src/authorship/rebase_authorship.rs index 68226008..e4ae3364 100644 --- a/src/authorship/rebase_authorship.rs +++ b/src/authorship/rebase_authorship.rs @@ -706,10 +706,14 @@ pub fn rewrite_authorship_after_commit_amend( let changed_files = repo.list_commit_files(amended_commit, None)?; let mut pathspecs: HashSet = changed_files.into_iter().collect(); + // println!("changed_files: {:?}", changed_files); + let working_log = repo.storage.working_log_for_base_commit(original_commit); let touched_files = working_log.all_touched_files()?; pathspecs.extend(touched_files); + println!("pathspecs: {:?}", pathspecs); + // Check if original commit has an authorship log with prompts let has_existing_log = get_reference_as_authorship_log_v3(repo, original_commit).is_ok(); let has_existing_prompts = if has_existing_log { @@ -756,6 +760,9 @@ pub fn rewrite_authorship_after_commit_amend( Some(&pathspecs_set), )?; + + println!("authorship_log: {:?}", authorship_log); + println!("initial_attributions: {:?}", initial_attributions); // Update base commit SHA authorship_log.metadata.base_commit_sha = amended_commit.to_string(); diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs index eb065ef5..4c9080d3 100644 --- a/src/commands/checkpoint.rs +++ b/src/commands/checkpoint.rs @@ -24,6 +24,8 @@ pub fn run( agent_run_result: Option, is_pre_commit: bool, ) -> Result<(usize, usize, usize), GitAiError> { + + // Robustly handle zero-commit repos let base_commit = match repo.head() { Ok(head) => match head.target() { @@ -73,13 +75,24 @@ pub fn run( paths.and_then(|p| { let repo_workdir = repo.workdir().ok()?; + + // CRITICAL FIX: Canonicalize repo_workdir for consistent path comparison on Windows + // On Windows, canonicalize() returns paths with \\?\ UNC prefix, so both paths + // must be canonicalized for starts_with() to work correctly + let canonical_repo_workdir = repo_workdir.canonicalize().ok()?; + let filtered: Vec = p .iter() .filter_map(|path| { // Check if path is absolute and outside repo if std::path::Path::new(path).is_absolute() { - // For absolute paths, check if they start with repo_workdir - if !std::path::Path::new(path).starts_with(&repo_workdir) { + // For absolute paths, canonicalize and compare with canonical_repo_workdir + if let Ok(canonical_path) = std::path::Path::new(path).canonicalize() { + if !canonical_path.starts_with(&canonical_repo_workdir) { + return None; + } + } else if !std::path::Path::new(path).starts_with(&repo_workdir) { + // Fallback if canonicalize fails return None; } } else { @@ -87,7 +100,8 @@ pub fn run( let joined = repo_workdir.join(path); // Try to canonicalize to resolve .. and . components if let Ok(canonical) = joined.canonicalize() { - if !canonical.starts_with(&repo_workdir) { + // Compare canonical paths (both have \\?\ prefix on Windows) + if !canonical.starts_with(&canonical_repo_workdir) { return None; } } else { @@ -106,6 +120,7 @@ pub fn run( acc }, ); + // For the fallback, compare against the original repo_workdir if !normalized_joined.starts_with(&repo_workdir) { return None; } @@ -124,6 +139,8 @@ pub fn run( }) }); + + println!("pathspec_filter: {:?}", pathspec_filter); let files = get_all_tracked_files( repo, &base_commit, @@ -132,6 +149,8 @@ pub fn run( is_pre_commit, )?; + println!("files: {:?}", files); + let mut checkpoints = if reset { // If reset flag is set, start with an empty working log working_log.reset_working_log()?; diff --git a/src/commands/git_ai_handlers.rs b/src/commands/git_ai_handlers.rs index a83b6ca4..b083ae21 100644 --- a/src/commands/git_ai_handlers.rs +++ b/src/commands/git_ai_handlers.rs @@ -326,6 +326,9 @@ fn handle_checkpoint(args: &[String]) { .map(|r| r.checkpoint_kind) .unwrap_or(CheckpointKind::Human); + println!("checkpoint_kind: {:?}", checkpoint_kind); + println!("files for mock_ai: {:?}", get_all_files_for_mock_ai(&final_working_dir)); + if CheckpointKind::Human == checkpoint_kind && agent_run_result.is_none() { println!( "get_all_files_for_mock_ai HUMAN Checkpoints: {:?}", diff --git a/tests/amend.rs b/tests/amend.rs index 429968ec..d7350add 100644 --- a/tests/amend.rs +++ b/tests/amend.rs @@ -24,7 +24,6 @@ fn test_amend_add_lines_at_top() { ); // Amend the commit WITHOUT staging the AI lines - // repo.git(&["add", "-A"]).unwrap(); repo.git(&["commit", "--amend", "-m", "Initial commit (amended)"]) .unwrap(); diff --git a/tests/git-compat/run.py b/tests/git-compat/run.py old mode 100755 new mode 100644 diff --git a/tests/github/scripts/cleanup-test-repos.sh b/tests/github/scripts/cleanup-test-repos.sh old mode 100755 new mode 100644 diff --git a/tests/github/scripts/run-github-tests.sh b/tests/github/scripts/run-github-tests.sh old mode 100755 new mode 100644 diff --git a/tests/repos/test_file.rs b/tests/repos/test_file.rs index 01dd4981..f0de88de 100644 --- a/tests/repos/test_file.rs +++ b/tests/repos/test_file.rs @@ -728,7 +728,9 @@ impl<'a> TestFile<'a> { let contents = self.contents(); fs::write(&self.file_path, contents).unwrap(); let _ = if author_type == &AuthorType::Ai { - self.repo.git_ai(&["checkpoint", "mock_ai"]) + let a = self.repo.git_ai(&["checkpoint", "mock_ai"]); + println!("checkpoint result: {:?}", a); + a } else { self.repo.git_ai(&["checkpoint"]) }; diff --git a/tests/repos/test_repo.rs b/tests/repos/test_repo.rs index 3395fb1a..e0cc0560 100644 --- a/tests/repos/test_repo.rs +++ b/tests/repos/test_repo.rs @@ -224,7 +224,7 @@ impl TestRepo { impl Drop for TestRepo { fn drop(&mut self) { - fs::remove_dir_all(self.path.clone()).expect("failed to remove test repo"); + // fs::remove_dir_all(self.path.clone()).expect("failed to remove test repo"); } } From fd4321d553e6377c3848a098b4c83dcddff08685 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Sat, 8 Nov 2025 09:51:53 -0500 Subject: [PATCH 02/19] add canonical working dir --- src/commands/checkpoint.rs | 58 ++++++++------------------------------ src/git/repository.rs | 40 +++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs index 4c9080d3..40ab17de 100644 --- a/src/commands/checkpoint.rs +++ b/src/commands/checkpoint.rs @@ -76,57 +76,23 @@ pub fn run( paths.and_then(|p| { let repo_workdir = repo.workdir().ok()?; - // CRITICAL FIX: Canonicalize repo_workdir for consistent path comparison on Windows - // On Windows, canonicalize() returns paths with \\?\ UNC prefix, so both paths - // must be canonicalized for starts_with() to work correctly - let canonical_repo_workdir = repo_workdir.canonicalize().ok()?; - let filtered: Vec = p .iter() .filter_map(|path| { - // Check if path is absolute and outside repo - if std::path::Path::new(path).is_absolute() { - // For absolute paths, canonicalize and compare with canonical_repo_workdir - if let Ok(canonical_path) = std::path::Path::new(path).canonicalize() { - if !canonical_path.starts_with(&canonical_repo_workdir) { - return None; - } - } else if !std::path::Path::new(path).starts_with(&repo_workdir) { - // Fallback if canonicalize fails - return None; - } + let path_buf = if std::path::Path::new(path).is_absolute() { + // Absolute path - check directly + std::path::PathBuf::from(path) } else { - // For relative paths, join with workdir and canonicalize to check - let joined = repo_workdir.join(path); - // Try to canonicalize to resolve .. and . components - if let Ok(canonical) = joined.canonicalize() { - // Compare canonical paths (both have \\?\ prefix on Windows) - if !canonical.starts_with(&canonical_repo_workdir) { - return None; - } - } else { - // If we can't canonicalize (file doesn't exist), check the joined path - // Convert both to canonical form if possible, otherwise use as-is - let normalized_joined = joined.components().fold( - std::path::PathBuf::new(), - |mut acc, component| { - match component { - std::path::Component::ParentDir => { - acc.pop(); - } - std::path::Component::CurDir => {} - _ => acc.push(component), - } - acc - }, - ); - // For the fallback, compare against the original repo_workdir - if !normalized_joined.starts_with(&repo_workdir) { - return None; - } - } + // Relative path - join with workdir + repo_workdir.join(path) + }; + + // Use centralized path comparison (handles Windows canonical paths correctly) + if repo.path_is_in_workdir(&path_buf) { + Some(path.clone()) + } else { + None } - Some(path.clone()) }) .collect(); diff --git a/src/git/repository.rs b/src/git/repository.rs index 69c37f80..7fcfe38e 100644 --- a/src/git/repository.rs +++ b/src/git/repository.rs @@ -790,6 +790,9 @@ pub struct Repository { pub pre_command_base_commit: Option, pub pre_command_refname: Option, workdir: PathBuf, + /// Canonical (absolute, resolved) version of workdir for reliable path comparisons + /// On Windows, this uses the \\?\ UNC prefix format + canonical_workdir: PathBuf, } impl Repository { @@ -902,6 +905,31 @@ impl Repository { Ok(self.workdir.clone()) } + /// Check if a path is within the repository's working directory + /// Uses canonical path comparison for reliability on Windows + pub fn path_is_in_workdir(&self, path: &Path) -> bool { + // Try canonical comparison first (most reliable, especially on Windows) + if let Ok(canonical_path) = path.canonicalize() { + return canonical_path.starts_with(&self.canonical_workdir); + } + + // Fallback for paths that don't exist yet: normalize by resolving .. and . + let normalized = path.components().fold( + std::path::PathBuf::new(), + |mut acc, component| { + match component { + std::path::Component::ParentDir => { + acc.pop(); + } + std::path::Component::CurDir => {} + _ => acc.push(component), + } + acc + }, + ); + normalized.starts_with(&self.workdir) + } + // List all remotes for a given repository pub fn remotes(&self) -> Result, GitAiError> { let mut args = self.global_args_for_exec(); @@ -1696,7 +1724,16 @@ pub fn find_repository(global_args: &Vec) -> Result) -> Result Date: Sat, 8 Nov 2025 10:36:15 -0500 Subject: [PATCH 03/19] windows fix --- src/commands/checkpoint.rs | 53 ++++++++++++++++++++++++++++---------- src/git/repository.rs | 6 +++++ src/utils.rs | 25 ++++++++++++++++++ tests/ai_tab.rs | 38 ++++++++++++++++++++------- tests/repos/test_repo.rs | 4 +++ 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs index 40ab17de..5614968a 100644 --- a/src/commands/checkpoint.rs +++ b/src/commands/checkpoint.rs @@ -7,7 +7,7 @@ use crate::error::GitAiError; use crate::git::repo_storage::{PersistedWorkingLog, RepoStorage}; use crate::git::repository::Repository; use crate::git::status::{EntryKind, StatusCode}; -use crate::utils::debug_log; +use crate::utils::{debug_log, normalize_to_posix}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use std::collections::{HashMap, HashSet}; @@ -89,7 +89,26 @@ pub fn run( // Use centralized path comparison (handles Windows canonical paths correctly) if repo.path_is_in_workdir(&path_buf) { - Some(path.clone()) + // Convert to relative path for git operations + if std::path::Path::new(path).is_absolute() { + if let Ok(relative) = path_buf.strip_prefix(&repo_workdir) { + // Normalize path separators to forward slashes for git + Some(normalize_to_posix(&relative.to_string_lossy())) + } else { + // Fallback: try with canonical paths + let canonical_workdir = repo_workdir.canonicalize().ok()?; + let canonical_path = path_buf.canonicalize().ok()?; + if let Ok(relative) = canonical_path.strip_prefix(&canonical_workdir) { + // Normalize path separators to forward slashes for git + Some(normalize_to_posix(&relative.to_string_lossy())) + } else { + None + } + } + } else { + // Normalize path separators to forward slashes for git + Some(normalize_to_posix(path)) + } } else { None } @@ -357,18 +376,22 @@ fn get_all_tracked_files( .unwrap_or_default(); for file in working_log.read_initial_attributions().files.keys() { - if is_text_file(working_log, &file) { - files.insert(file.clone()); + // Normalize path separators to forward slashes + let normalized_path = normalize_to_posix(file); + if is_text_file(working_log, &normalized_path) { + files.insert(normalized_path); } } if let Ok(working_log_data) = working_log.read_all_checkpoints() { for checkpoint in &working_log_data { for entry in &checkpoint.entries { - if !files.contains(&entry.file) { + // Normalize path separators to forward slashes + let normalized_path = normalize_to_posix(&entry.file); + if !files.contains(&normalized_path) { // Check if it's a text file before adding - if is_text_file(working_log, &entry.file) { - files.insert(entry.file.clone()); + if is_text_file(working_log, &normalized_path) { + files.insert(normalized_path); } } } @@ -392,11 +415,13 @@ fn get_all_tracked_files( // Ensure to always include all dirty files if let Some(ref dirty_files) = working_log.dirty_files { for file_path in dirty_files.keys() { + // Normalize path separators to forward slashes + let normalized_path = normalize_to_posix(file_path); // Only add if not already in the files list - if !results_for_tracked_files.contains(&file_path) { + if !results_for_tracked_files.contains(&normalized_path) { // Check if it's a text file before adding - if is_text_file(working_log, &file_path) { - results_for_tracked_files.push(file_path.clone()); + if is_text_file(working_log, &normalized_path) { + results_for_tracked_files.push(normalized_path); } } } @@ -1222,14 +1247,16 @@ mod tests { } fn is_text_file(working_log: &PersistedWorkingLog, path: &str) -> bool { + // Normalize path for dirty_files lookup + let normalized_path = normalize_to_posix(path); let skip_metadata_check = working_log .dirty_files .as_ref() - .map(|m| m.contains_key(path)) + .map(|m| m.contains_key(&normalized_path)) .unwrap_or(false); if !skip_metadata_check { - if let Ok(metadata) = std::fs::metadata(working_log.to_repo_absolute_path(path)) { + if let Ok(metadata) = std::fs::metadata(working_log.to_repo_absolute_path(&normalized_path)) { if !metadata.is_file() { return false; } @@ -1239,7 +1266,7 @@ fn is_text_file(working_log: &PersistedWorkingLog, path: &str) -> bool { } working_log - .read_current_file_content(path) + .read_current_file_content(&normalized_path) .map(|content| !content.chars().any(|c| c == '\0')) .unwrap_or(false) } diff --git a/src/git/repository.rs b/src/git/repository.rs index 7fcfe38e..5693905e 100644 --- a/src/git/repository.rs +++ b/src/git/repository.rs @@ -905,6 +905,12 @@ impl Repository { Ok(self.workdir.clone()) } + /// Get the canonical (absolute, resolved) path of the working directory + /// On Windows, this uses the \\?\ UNC prefix format for reliable path comparisons + pub fn canonical_workdir(&self) -> &Path { + &self.canonical_workdir + } + /// Check if a path is within the repository's working directory /// Uses canonical path comparison for reliability on Windows pub fn path_is_in_workdir(&self, path: &Path) -> bool { diff --git a/src/utils.rs b/src/utils.rs index 9bdb370c..06639e25 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -74,3 +74,28 @@ pub fn _print_diff(diff: &Diff, old_label: &str, new_label: &str) { println!(" No changes between {} and {}", old_label, new_label); } } + +/// Normalize a path to use POSIX-style forward slashes +/// +/// Git expects paths with forward slashes, even on Windows. This function +/// converts any backslashes to forward slashes for cross-platform compatibility. +/// +/// # Arguments +/// +/// * `path` - The path string to normalize (e.g., from `Path::to_string_lossy()`) +/// +/// # Returns +/// +/// A string with all backslashes replaced by forward slashes +/// +/// # Example +/// +/// ``` +/// let path = std::path::Path::new("src\\commands\\checkpoint.rs"); +/// let normalized = normalize_to_posix(&path.to_string_lossy()); +/// assert_eq!(normalized, "src/commands/checkpoint.rs"); +/// ``` +#[inline] +pub fn normalize_to_posix(path: &str) -> String { + path.replace('\\', "/") +} \ No newline at end of file diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index 5711c1a1..cd11f3be 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -16,8 +16,13 @@ use git_ai::{ fn run_ai_tab_checkpoint(repo: &TestRepo, hook_payload: serde_json::Value) { let hook_input = hook_payload.to_string(); let args: Vec<&str> = vec!["checkpoint", "ai_tab", "--hook-input", hook_input.as_str()]; - if let Err(err) = repo.git_ai(&args) { - panic!("ai_tab checkpoint failed: {}", err); + match repo.git_ai(&args) { + Ok(output) => { + println!("git_ai checkpoint output: {}", output); + } + Err(err) => { + panic!("ai_tab checkpoint failed: {}", err); + } } } @@ -223,7 +228,7 @@ fn test_ai_tab_requires_non_empty_tool_and_model() { fn test_ai_tab_e2e_marks_ai_lines() { let repo = TestRepo::new(); let relative_path = "notes_test.ts"; - let file_path = repo.path().join(relative_path); + let file_path = repo.canonical_path().join(relative_path); let base_content = "console.log(\"hello world\");\n".to_string(); fs::write(&file_path, &base_content).unwrap(); @@ -238,7 +243,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "will_edit_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): base_content.clone() @@ -256,7 +261,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "edited_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): ai_content.clone() @@ -278,13 +283,13 @@ fn test_ai_tab_e2e_marks_ai_lines() { fn test_ai_tab_e2e_handles_dirty_files_map() { let repo = TestRepo::new(); let lib_relative_path = std::path::Path::new("src").join("lib.rs"); - let lib_file_path = repo.path().join(lib_relative_path); + let lib_file_path = repo.canonical_path().join(lib_relative_path); // Create parent directory - handle Windows paths safely if let Some(parent) = lib_file_path.parent() { fs::create_dir_all(parent).unwrap(); } let readme_relative_path = "README.md"; - let readme_file_path = repo.path().join(readme_relative_path); + let readme_file_path = repo.canonical_path().join(readme_relative_path); let base_lib_content = "fn greet() {\n println!(\"hello\");\n}\n".to_string(); fs::write(&lib_file_path, &base_lib_content).unwrap(); @@ -299,6 +304,9 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { let lib_file_path_str = lib_file_path.to_string_lossy().to_string(); let readme_file_path_str = readme_file_path.to_string_lossy().to_string(); + println!("lib_file_path_str: {}", lib_file_path_str); + println!("readme_file_path_str: {}", readme_file_path_str); + // Before edit snapshot includes all dirty files (AI target plus unrelated human edits) run_ai_tab_checkpoint( &repo, @@ -306,7 +314,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "will_edit_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): base_lib_content.clone(), @@ -327,7 +335,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "edited_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): ai_content.clone(), @@ -336,6 +344,18 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); + // Debug: Check working logs before commit + let working_logs = repo.current_working_logs(); + if let Ok(checkpoints) = working_logs.read_all_checkpoints() { + println!("Checkpoints before commit: {}", checkpoints.len()); + for (i, cp) in checkpoints.iter().enumerate() { + println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); + for entry in &cp.entries { + println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); + } + } + } + repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); let mut file = repo.filename(&std::path::Path::new("src").join("lib.rs").to_string_lossy()); diff --git a/tests/repos/test_repo.rs b/tests/repos/test_repo.rs index e0cc0560..da6798de 100644 --- a/tests/repos/test_repo.rs +++ b/tests/repos/test_repo.rs @@ -39,6 +39,10 @@ impl TestRepo { &self.path } + pub fn canonical_path(&self) -> PathBuf { + self.path.canonicalize().expect("failed to canonicalize test repo path") + } + pub fn stats(&self) -> Result { let mut stats = self.git_ai(&["stats", "--json"]).unwrap(); stats = stats.split("}}}").next().unwrap().to_string() + "}}}"; From 2eda272b4b43d06343c92177356f8dea64d6f021 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Sat, 8 Nov 2025 10:39:52 -0500 Subject: [PATCH 04/19] fix doc error --- src/utils.rs | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 06639e25..ab8cd594 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -75,26 +75,7 @@ pub fn _print_diff(diff: &Diff, old_label: &str, new_label: &str) { } } -/// Normalize a path to use POSIX-style forward slashes -/// -/// Git expects paths with forward slashes, even on Windows. This function -/// converts any backslashes to forward slashes for cross-platform compatibility. -/// -/// # Arguments -/// -/// * `path` - The path string to normalize (e.g., from `Path::to_string_lossy()`) -/// -/// # Returns -/// -/// A string with all backslashes replaced by forward slashes -/// -/// # Example -/// -/// ``` -/// let path = std::path::Path::new("src\\commands\\checkpoint.rs"); -/// let normalized = normalize_to_posix(&path.to_string_lossy()); -/// assert_eq!(normalized, "src/commands/checkpoint.rs"); -/// ``` + #[inline] pub fn normalize_to_posix(path: &str) -> String { path.replace('\\', "/") From 7d2586067b9a28b017a77218ba5ee604c6796fb5 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Sat, 8 Nov 2025 11:03:32 -0500 Subject: [PATCH 05/19] fix. use canocical repo root --- src/git/repo_storage.rs | 80 +++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/src/git/repo_storage.rs b/src/git/repo_storage.rs index ce586b4d..a940875f 100644 --- a/src/git/repo_storage.rs +++ b/src/git/repo_storage.rs @@ -73,7 +73,17 @@ impl RepoStorage { pub fn working_log_for_base_commit(&self, sha: &str) -> PersistedWorkingLog { let working_log_dir = self.working_logs.join(sha); fs::create_dir_all(&working_log_dir).unwrap(); - PersistedWorkingLog::new(working_log_dir, sha, self.repo_workdir.clone(), None) + let canonical_workdir = self + .repo_workdir + .canonicalize() + .unwrap_or_else(|_| self.repo_workdir.clone()); + PersistedWorkingLog::new( + working_log_dir, + sha, + self.repo_workdir.clone(), + canonical_workdir, + None, + ) } #[allow(dead_code)] @@ -123,6 +133,9 @@ pub struct PersistedWorkingLog { #[allow(dead_code)] pub base_commit: String, pub repo_workdir: PathBuf, + /// Canonical (absolute, resolved) version of workdir for reliable path comparisons + /// On Windows, this uses the \\?\ UNC prefix format + pub canonical_workdir: PathBuf, pub dirty_files: Option>, } @@ -131,12 +144,14 @@ impl PersistedWorkingLog { dir: PathBuf, base_commit: &str, repo_root: PathBuf, + canonical_workdir: PathBuf, dirty_files: Option>, ) -> Self { Self { dir, base_commit: base_commit.to_string(), repo_workdir: repo_root, + canonical_workdir, dirty_files, } } @@ -144,9 +159,7 @@ impl PersistedWorkingLog { pub fn set_dirty_files(&mut self, dirty_files: Option>) { self.dirty_files = dirty_files.map(|map| { map.into_iter() - .map(|(file_path, content)| { - (self.to_repo_relative_path(&file_path), content) - }) + .map(|(file_path, content)| (self.to_repo_relative_path(&file_path), content)) .collect() }); } @@ -192,7 +205,10 @@ impl PersistedWorkingLog { if Path::new(file_path).is_absolute() { return file_path.to_string(); } - self.repo_workdir.join(file_path).to_string_lossy().to_string() + self.repo_workdir + .join(file_path) + .to_string_lossy() + .to_string() } pub fn to_repo_relative_path(&self, file_path: &str) -> String { @@ -201,23 +217,26 @@ impl PersistedWorkingLog { } let path = Path::new(file_path); - // Try without canonicalizing first - if path.starts_with(&self.repo_workdir) { - return path.strip_prefix(&self.repo_workdir).unwrap().to_string_lossy().to_string(); + // Try canonical comparison first (most reliable, especially on Windows) + if let Ok(canonical_path) = path.canonicalize() { + if canonical_path.starts_with(&self.canonical_workdir) { + return canonical_path + .strip_prefix(&self.canonical_workdir) + .unwrap() + .to_string_lossy() + .to_string(); + } } - // If we couldn't match yet, try canonicalizing both repo_workdir and the input path - let canonical_workdir = match self.repo_workdir.canonicalize() { - Ok(p) => p, - Err(_) => self.repo_workdir.clone(), - }; - let canonical_path = match path.canonicalize() { - Ok(p) => p, - Err(_) => path.to_path_buf(), - }; - if canonical_path.starts_with(&canonical_workdir) { - return canonical_path.strip_prefix(&canonical_workdir).unwrap().to_string_lossy().to_string(); + // Fallback: try with non-canonical paths + if path.starts_with(&self.repo_workdir) { + return path + .strip_prefix(&self.repo_workdir) + .unwrap() + .to_string_lossy() + .to_string(); } + return file_path.to_string(); } @@ -400,7 +419,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage - let _repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let _repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); // Verify .git/ai directory exists let ai_dir = tmp_repo.repo().path().join("ai"); @@ -433,7 +453,10 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage - let repo_storage = RepoStorage::for_repo_path(&tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = RepoStorage::for_repo_path( + &tmp_repo.repo().path(), + &tmp_repo.repo().workdir().unwrap(), + ); // Add some content to rewrite_log let rewrite_log_file = tmp_repo.repo().path().join("ai").join("rewrite_log"); @@ -467,7 +490,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage and PersistedWorkingLog - let repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); let working_log = repo_storage.working_log_for_base_commit("test-commit-sha"); // Test persisting a file version @@ -510,7 +534,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage and PersistedWorkingLog - let repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); let working_log = repo_storage.working_log_for_base_commit("test-commit-sha"); // Create a test checkpoint @@ -566,7 +591,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage and PersistedWorkingLog - let repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); let working_log = repo_storage.working_log_for_base_commit("test-commit-sha"); // Build three checkpoints: missing version, wrong version, and correct version @@ -618,7 +644,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage and PersistedWorkingLog - let repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); let working_log = repo_storage.working_log_for_base_commit("test-commit-sha"); // Add some blobs @@ -686,7 +713,8 @@ mod tests { let tmp_repo = TmpRepo::new().expect("Failed to create tmp repo"); // Create RepoStorage - let repo_storage = RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); + let repo_storage = + RepoStorage::for_repo_path(tmp_repo.repo().path(), &tmp_repo.repo().workdir().unwrap()); // Create working log for a specific commit let commit_sha = "abc123def456"; From 337074d7201019a3ade1dd25ab10c75dac044442 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Sat, 8 Nov 2025 11:10:49 -0500 Subject: [PATCH 06/19] fix ests --- tests/ai_tab.rs | 60 ++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index cd11f3be..8db65e93 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -16,13 +16,8 @@ use git_ai::{ fn run_ai_tab_checkpoint(repo: &TestRepo, hook_payload: serde_json::Value) { let hook_input = hook_payload.to_string(); let args: Vec<&str> = vec!["checkpoint", "ai_tab", "--hook-input", hook_input.as_str()]; - match repo.git_ai(&args) { - Ok(output) => { - println!("git_ai checkpoint output: {}", output); - } - Err(err) => { - panic!("ai_tab checkpoint failed: {}", err); - } + if let Err(err) = repo.git_ai(&args) { + panic!("ai_tab checkpoint failed: {}", err); } } @@ -135,10 +130,7 @@ fn test_ai_tab_after_edit_checkpoint_includes_dirty_files_and_paths() { let edited = result .edited_filepaths .expect("after_edit should include edited filepaths"); - assert_eq!( - edited, - vec!["/Users/test/project/src/main.rs".to_string()] - ); + assert_eq!(edited, vec!["/Users/test/project/src/main.rs".to_string()]); let dirty_files = result .dirty_files @@ -173,7 +165,10 @@ fn test_ai_tab_rejects_invalid_hook_event() { message ); } - other => panic!("expected PresetError for invalid hook_event_name, got {:?}", other), + other => panic!( + "expected PresetError for invalid hook_event_name, got {:?}", + other + ), } } @@ -228,7 +223,7 @@ fn test_ai_tab_requires_non_empty_tool_and_model() { fn test_ai_tab_e2e_marks_ai_lines() { let repo = TestRepo::new(); let relative_path = "notes_test.ts"; - let file_path = repo.canonical_path().join(relative_path); + let file_path = repo.path().join(relative_path); let base_content = "console.log(\"hello world\");\n".to_string(); fs::write(&file_path, &base_content).unwrap(); @@ -243,7 +238,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.canonical_path().to_string_lossy(), + "repo_working_dir": repo.path().to_string_lossy(), "will_edit_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): base_content.clone() @@ -252,7 +247,9 @@ fn test_ai_tab_e2e_marks_ai_lines() { ); // AI tab inserts new lines alongside the existing content - let ai_content = "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n".to_string(); + let ai_content = + "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n" + .to_string(); fs::write(&file_path, &ai_content).unwrap(); run_ai_tab_checkpoint( @@ -261,7 +258,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.canonical_path().to_string_lossy(), + "repo_working_dir": repo.path().to_string_lossy(), "edited_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): ai_content.clone() @@ -269,7 +266,8 @@ fn test_ai_tab_e2e_marks_ai_lines() { }), ); - repo.stage_all_and_commit("Accept AI tab completion").unwrap(); + repo.stage_all_and_commit("Accept AI tab completion") + .unwrap(); let mut file = repo.filename(relative_path); file.assert_lines_and_blame(lines![ @@ -283,13 +281,13 @@ fn test_ai_tab_e2e_marks_ai_lines() { fn test_ai_tab_e2e_handles_dirty_files_map() { let repo = TestRepo::new(); let lib_relative_path = std::path::Path::new("src").join("lib.rs"); - let lib_file_path = repo.canonical_path().join(lib_relative_path); + let lib_file_path = repo.path().join(lib_relative_path); // Create parent directory - handle Windows paths safely if let Some(parent) = lib_file_path.parent() { fs::create_dir_all(parent).unwrap(); } let readme_relative_path = "README.md"; - let readme_file_path = repo.canonical_path().join(readme_relative_path); + let readme_file_path = repo.path().join(readme_relative_path); let base_lib_content = "fn greet() {\n println!(\"hello\");\n}\n".to_string(); fs::write(&lib_file_path, &base_lib_content).unwrap(); @@ -304,9 +302,6 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { let lib_file_path_str = lib_file_path.to_string_lossy().to_string(); let readme_file_path_str = readme_file_path.to_string_lossy().to_string(); - println!("lib_file_path_str: {}", lib_file_path_str); - println!("readme_file_path_str: {}", readme_file_path_str); - // Before edit snapshot includes all dirty files (AI target plus unrelated human edits) run_ai_tab_checkpoint( &repo, @@ -314,7 +309,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.canonical_path().to_string_lossy(), + "repo_working_dir": repo.path().to_string_lossy(), "will_edit_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): base_lib_content.clone(), @@ -335,7 +330,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.canonical_path().to_string_lossy(), + "repo_working_dir": repo.path().to_string_lossy(), "edited_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): ai_content.clone(), @@ -344,19 +339,8 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); - // Debug: Check working logs before commit - let working_logs = repo.current_working_logs(); - if let Ok(checkpoints) = working_logs.read_all_checkpoints() { - println!("Checkpoints before commit: {}", checkpoints.len()); - for (i, cp) in checkpoints.iter().enumerate() { - println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); - for entry in &cp.entries { - println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); - } - } - } - - repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); + repo.stage_all_and_commit("Record AI tab completion while other files dirty") + .unwrap(); let mut file = repo.filename(&std::path::Path::new("src").join("lib.rs").to_string_lossy()); file.assert_lines_and_blame(lines![ @@ -367,4 +351,4 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { " println!(\"from ai\");".ai(), "}".ai(), ]); -} \ No newline at end of file +} From a5bd40ca93a6106c2ed53dabefa4f70b48d106f4 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Sat, 8 Nov 2025 12:37:11 -0500 Subject: [PATCH 07/19] more debugging --- tests/ai_tab.rs | 56 +++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index 8db65e93..91d07f41 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -16,8 +16,13 @@ use git_ai::{ fn run_ai_tab_checkpoint(repo: &TestRepo, hook_payload: serde_json::Value) { let hook_input = hook_payload.to_string(); let args: Vec<&str> = vec!["checkpoint", "ai_tab", "--hook-input", hook_input.as_str()]; - if let Err(err) = repo.git_ai(&args) { - panic!("ai_tab checkpoint failed: {}", err); + match repo.git_ai(&args) { + Ok(output) => { + println!("git_ai checkpoint output: {}", output); + } + Err(err) => { + panic!("ai_tab checkpoint failed: {}", err); + } } } @@ -130,7 +135,10 @@ fn test_ai_tab_after_edit_checkpoint_includes_dirty_files_and_paths() { let edited = result .edited_filepaths .expect("after_edit should include edited filepaths"); - assert_eq!(edited, vec!["/Users/test/project/src/main.rs".to_string()]); + assert_eq!( + edited, + vec!["/Users/test/project/src/main.rs".to_string()] + ); let dirty_files = result .dirty_files @@ -165,10 +173,7 @@ fn test_ai_tab_rejects_invalid_hook_event() { message ); } - other => panic!( - "expected PresetError for invalid hook_event_name, got {:?}", - other - ), + other => panic!("expected PresetError for invalid hook_event_name, got {:?}", other), } } @@ -223,7 +228,7 @@ fn test_ai_tab_requires_non_empty_tool_and_model() { fn test_ai_tab_e2e_marks_ai_lines() { let repo = TestRepo::new(); let relative_path = "notes_test.ts"; - let file_path = repo.path().join(relative_path); + let file_path = repo.canonical_path().join(relative_path); let base_content = "console.log(\"hello world\");\n".to_string(); fs::write(&file_path, &base_content).unwrap(); @@ -238,7 +243,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "will_edit_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): base_content.clone() @@ -247,9 +252,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { ); // AI tab inserts new lines alongside the existing content - let ai_content = - "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n" - .to_string(); + let ai_content = "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n".to_string(); fs::write(&file_path, &ai_content).unwrap(); run_ai_tab_checkpoint( @@ -258,7 +261,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "edited_filepaths": [file_path_str.clone()], "dirty_files": { file_path_str.clone(): ai_content.clone() @@ -266,8 +269,7 @@ fn test_ai_tab_e2e_marks_ai_lines() { }), ); - repo.stage_all_and_commit("Accept AI tab completion") - .unwrap(); + repo.stage_all_and_commit("Accept AI tab completion").unwrap(); let mut file = repo.filename(relative_path); file.assert_lines_and_blame(lines![ @@ -302,6 +304,9 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { let lib_file_path_str = lib_file_path.to_string_lossy().to_string(); let readme_file_path_str = readme_file_path.to_string_lossy().to_string(); + println!("lib_file_path_str: {}", lib_file_path_str); + println!("readme_file_path_str: {}", readme_file_path_str); + // Before edit snapshot includes all dirty files (AI target plus unrelated human edits) run_ai_tab_checkpoint( &repo, @@ -309,7 +314,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "before_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "will_edit_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): base_lib_content.clone(), @@ -330,7 +335,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { "hook_event_name": "after_edit", "tool": "github-copilot-tab", "model": "default", - "repo_working_dir": repo.path().to_string_lossy(), + "repo_working_dir": repo.canonical_path().to_string_lossy(), "edited_filepaths": [lib_file_path_str.clone()], "dirty_files": { lib_file_path_str.clone(): ai_content.clone(), @@ -339,8 +344,19 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); - repo.stage_all_and_commit("Record AI tab completion while other files dirty") - .unwrap(); + // Debug: Check working logs before commit + let working_logs = repo.current_working_logs(); + if let Ok(checkpoints) = working_logs.read_all_checkpoints() { + println!("Checkpoints before commit: {}", checkpoints.len()); + for (i, cp) in checkpoints.iter().enumerate() { + println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); + for entry in &cp.entries { + println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); + } + } + } + + repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); let mut file = repo.filename(&std::path::Path::new("src").join("lib.rs").to_string_lossy()); file.assert_lines_and_blame(lines![ @@ -351,4 +367,4 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { " println!(\"from ai\");".ai(), "}".ai(), ]); -} +} \ No newline at end of file From 08902c88904107151f7fb39567185ac5ae12249d Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Mon, 10 Nov 2025 17:10:15 -0500 Subject: [PATCH 08/19] close --- src/git/repo_storage.rs | 18 ++++++++++++++---- tests/ai_tab.rs | 29 +++++++++++++++++++---------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/git/repo_storage.rs b/src/git/repo_storage.rs index a940875f..921af746 100644 --- a/src/git/repo_storage.rs +++ b/src/git/repo_storage.rs @@ -3,7 +3,7 @@ use crate::authorship::authorship_log::PromptRecord; use crate::authorship::working_log::{CHECKPOINT_API_VERSION, Checkpoint, CheckpointKind}; use crate::error::GitAiError; use crate::git::rewrite_log::{RewriteLogEvent, append_event_to_file}; -use crate::utils::debug_log; +use crate::utils::{debug_log, normalize_to_posix}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; @@ -157,11 +157,21 @@ impl PersistedWorkingLog { } pub fn set_dirty_files(&mut self, dirty_files: Option>) { - self.dirty_files = dirty_files.map(|map| { + println!("pre_transform dirty_files: {:?}", dirty_files); + + let normalized_dirty_files = dirty_files.map(|map| { map.into_iter() - .map(|(file_path, content)| (self.to_repo_relative_path(&file_path), content)) - .collect() + .map(|(file_path, content)| { + let relative_path = self.to_repo_relative_path(&file_path); + let normalized_path = normalize_to_posix(&relative_path); + (normalized_path, content) + }) + .collect::>() }); + + self.dirty_files = normalized_dirty_files; + + println!("setdirty_files: {:?}", self.dirty_files); } pub fn reset_working_log(&self) -> Result<(), GitAiError> { diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index 91d07f41..4091a874 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -307,6 +307,9 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { println!("lib_file_path_str: {}", lib_file_path_str); println!("readme_file_path_str: {}", readme_file_path_str); + let working_logs = repo.current_working_logs(); + println!("dirty_files: {:?}", working_logs.dirty_files); + // Before edit snapshot includes all dirty files (AI target plus unrelated human edits) run_ai_tab_checkpoint( &repo, @@ -329,6 +332,9 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { .to_string(); fs::write(&lib_file_path, &ai_content).unwrap(); + let working_logs = repo.current_working_logs(); + println!("dirty_files: {:?}", working_logs.dirty_files); + run_ai_tab_checkpoint( &repo, json!({ @@ -344,17 +350,20 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); - // Debug: Check working logs before commit + // // Debug: Check working logs before commit + // let working_logs = repo.current_working_logs(); + // if let Ok(checkpoints) = working_logs.read_all_checkpoints() { + // println!("Checkpoints before commit: {}", checkpoints.len()); + // for (i, cp) in checkpoints.iter().enumerate() { + // println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); + // for entry in &cp.entries { + // println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); + // } + // } + // } + let working_logs = repo.current_working_logs(); - if let Ok(checkpoints) = working_logs.read_all_checkpoints() { - println!("Checkpoints before commit: {}", checkpoints.len()); - for (i, cp) in checkpoints.iter().enumerate() { - println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); - for entry in &cp.entries { - println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); - } - } - } + println!("dirty_files: {:?}", working_logs.dirty_files); repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); From a0a9804bdc64615ccf1ee4f69b0f38eb56e05860 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Mon, 10 Nov 2025 18:27:33 -0500 Subject: [PATCH 09/19] windows working. fix test helper --- tests/ai_tab.rs | 19 ++++++------------- tests/repos/test_file.rs | 38 +++++++++++++++++++++++++++++--------- tests/repos/test_repo.rs | 4 ++-- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index 4091a874..9aae3987 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -350,24 +350,17 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); - // // Debug: Check working logs before commit - // let working_logs = repo.current_working_logs(); - // if let Ok(checkpoints) = working_logs.read_all_checkpoints() { - // println!("Checkpoints before commit: {}", checkpoints.len()); - // for (i, cp) in checkpoints.iter().enumerate() { - // println!("Checkpoint {}: kind={:?}, entries={}", i, cp.kind, cp.entries.len()); - // for entry in &cp.entries { - // println!(" File: {}, attributions={}", entry.file, entry.attributions.len()); - // } - // } - // } let working_logs = repo.current_working_logs(); println!("dirty_files: {:?}", working_logs.dirty_files); - repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); + let commit_result = repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); + println!("COMMIT OUTPUT: {}", commit_result.stdout); - let mut file = repo.filename(&std::path::Path::new("src").join("lib.rs").to_string_lossy()); + + commit_result.print_authorship(); + + let mut file = repo.filename("src/lib.rs"); file.assert_lines_and_blame(lines![ "fn greet() {".human(), " println!(\"hello\");".human(), diff --git a/tests/repos/test_file.rs b/tests/repos/test_file.rs index f0de88de..9a641bdd 100644 --- a/tests/repos/test_file.rs +++ b/tests/repos/test_file.rs @@ -120,8 +120,12 @@ impl<'a> TestFile<'a> { }; } - // Run blame to get authorship - let filename = file_path.to_str().expect("valid path"); + // Run blame to get authorship - use relative path from repo root + let filename = file_path + .strip_prefix(&repo.path) + .expect("file path should be within repo") + .to_str() + .expect("valid path"); let blame_result = repo.git_ai(&["blame", filename]); let lines = if let Ok(blame_output) = blame_result { @@ -213,7 +217,11 @@ impl<'a> TestFile<'a> { } pub fn assert_blame_snapshot(&self) { - let filename = self.file_path.to_str().expect("valid path"); + let filename = self.file_path + .strip_prefix(&self.repo.path) + .expect("file path should be within repo") + .to_str() + .expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -226,8 +234,12 @@ impl<'a> TestFile<'a> { pub fn assert_lines_and_blame>(&mut self, lines: Vec) { let expected_lines: Vec = lines.into_iter().map(|l| l.into()).collect(); - // Get blame output - let filename = self.file_path.to_str().expect("valid path"); + // Get blame output - use relative path from repo root + let filename = self.file_path + .strip_prefix(&self.repo.path) + .expect("file path should be within repo") + .to_str() + .expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -300,8 +312,12 @@ impl<'a> TestFile<'a> { pub fn assert_committed_lines>(&mut self, lines: Vec) { let expected_lines: Vec = lines.into_iter().map(|l| l.into()).collect(); - // Get blame output - let filename = self.file_path.to_str().expect("valid path"); + // Get blame output - use relative path from repo root + let filename = self.file_path + .strip_prefix(&self.repo.path) + .expect("file path should be within repo") + .to_str() + .expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -430,8 +446,12 @@ impl<'a> TestFile<'a> { /// Assert that the file at the given path matches the expected contents and authorship pub fn assert_blame_contents_expected(&self) { - // Get blame output - let filename = self.file_path.to_str().expect("valid path"); + // Get blame output - use relative path from repo root + let filename = self.file_path + .strip_prefix(&self.repo.path) + .expect("file path should be within repo") + .to_str() + .expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) diff --git a/tests/repos/test_repo.rs b/tests/repos/test_repo.rs index da6798de..fc876767 100644 --- a/tests/repos/test_repo.rs +++ b/tests/repos/test_repo.rs @@ -14,7 +14,7 @@ use super::test_file::TestFile; #[derive(Clone, Debug)] pub struct TestRepo { - path: PathBuf, + pub path: PathBuf, } impl TestRepo { @@ -216,7 +216,7 @@ impl TestRepo { } pub fn stage_all_and_commit(&self, message: &str) -> Result { - self.git(&["add", "-A"]).expect("add --all should succeed"); + println!("stage_all_and_commit: {:?}", self.git(&["add", "-A"]).expect("add --all should succeed")); self.commit(message) } From f3cf9722dd6cb47d14da2ebb347cf148c9be5e9d Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 18:40:45 -0500 Subject: [PATCH 10/19] relitivize blame --- src/commands/blame.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/blame.rs b/src/commands/blame.rs index bb31486c..c62df72b 100644 --- a/src/commands/blame.rs +++ b/src/commands/blame.rs @@ -5,6 +5,8 @@ use crate::error::GitAiError; use crate::git::refs::get_reference_as_authorship_log_v3; use crate::git::repository::Repository; use crate::git::repository::exec_git; +#[cfg(windows)] +use crate::utils::normalize_to_posix; use chrono::{DateTime, FixedOffset, TimeZone, Utc}; use std::collections::HashMap; use std::fs; @@ -194,6 +196,12 @@ impl Repository { file_path.to_string() }; + #[cfg(windows)] + let relative_file_path = normalize_to_posix(&relative_file_path); + + #[cfg(not(windows))] + let relative_file_path = relative_file_path; + let abs_file_path = repo_root.join(&relative_file_path); // Validate that the file exists From 85129e6584eba6ac55f5ae326fbb3664a1bc9d45 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Mon, 10 Nov 2025 18:56:56 -0500 Subject: [PATCH 11/19] fix --- src/commands/blame.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/commands/blame.rs b/src/commands/blame.rs index c62df72b..a6db9a60 100644 --- a/src/commands/blame.rs +++ b/src/commands/blame.rs @@ -196,13 +196,23 @@ impl Repository { file_path.to_string() }; + // do this before normalizing the path + let abs_file_path = repo_root.join(&relative_file_path); + + #[cfg(windows)] - let relative_file_path = normalize_to_posix(&relative_file_path); + let relative_file_path = { + let normalized = normalize_to_posix(&relative_file_path); + // Strip leading ./ or .\ if present + normalized.strip_prefix("./").unwrap_or(&normalized).to_string() + }; #[cfg(not(windows))] - let relative_file_path = relative_file_path; + let relative_file_path = { + // Also strip leading ./ on non-Windows for consistency + relative_file_path.strip_prefix("./").unwrap_or(&relative_file_path).to_string() + }; - let abs_file_path = repo_root.join(&relative_file_path); // Validate that the file exists if !abs_file_path.exists() { From 6cfa65dddac75df31ac744ae73246e2568d5fff9 Mon Sep 17 00:00:00 2001 From: "acunniffe@gmail.com" Date: Mon, 10 Nov 2025 18:58:37 -0500 Subject: [PATCH 12/19] fix --- tests/repos/test_file.rs | 38 +++++++++----------------------------- tests/repos/test_repo.rs | 4 ++-- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/tests/repos/test_file.rs b/tests/repos/test_file.rs index 9a641bdd..f0de88de 100644 --- a/tests/repos/test_file.rs +++ b/tests/repos/test_file.rs @@ -120,12 +120,8 @@ impl<'a> TestFile<'a> { }; } - // Run blame to get authorship - use relative path from repo root - let filename = file_path - .strip_prefix(&repo.path) - .expect("file path should be within repo") - .to_str() - .expect("valid path"); + // Run blame to get authorship + let filename = file_path.to_str().expect("valid path"); let blame_result = repo.git_ai(&["blame", filename]); let lines = if let Ok(blame_output) = blame_result { @@ -217,11 +213,7 @@ impl<'a> TestFile<'a> { } pub fn assert_blame_snapshot(&self) { - let filename = self.file_path - .strip_prefix(&self.repo.path) - .expect("file path should be within repo") - .to_str() - .expect("valid path"); + let filename = self.file_path.to_str().expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -234,12 +226,8 @@ impl<'a> TestFile<'a> { pub fn assert_lines_and_blame>(&mut self, lines: Vec) { let expected_lines: Vec = lines.into_iter().map(|l| l.into()).collect(); - // Get blame output - use relative path from repo root - let filename = self.file_path - .strip_prefix(&self.repo.path) - .expect("file path should be within repo") - .to_str() - .expect("valid path"); + // Get blame output + let filename = self.file_path.to_str().expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -312,12 +300,8 @@ impl<'a> TestFile<'a> { pub fn assert_committed_lines>(&mut self, lines: Vec) { let expected_lines: Vec = lines.into_iter().map(|l| l.into()).collect(); - // Get blame output - use relative path from repo root - let filename = self.file_path - .strip_prefix(&self.repo.path) - .expect("file path should be within repo") - .to_str() - .expect("valid path"); + // Get blame output + let filename = self.file_path.to_str().expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) @@ -446,12 +430,8 @@ impl<'a> TestFile<'a> { /// Assert that the file at the given path matches the expected contents and authorship pub fn assert_blame_contents_expected(&self) { - // Get blame output - use relative path from repo root - let filename = self.file_path - .strip_prefix(&self.repo.path) - .expect("file path should be within repo") - .to_str() - .expect("valid path"); + // Get blame output + let filename = self.file_path.to_str().expect("valid path"); let blame_output = self .repo .git_ai(&["blame", filename]) diff --git a/tests/repos/test_repo.rs b/tests/repos/test_repo.rs index fc876767..da6798de 100644 --- a/tests/repos/test_repo.rs +++ b/tests/repos/test_repo.rs @@ -14,7 +14,7 @@ use super::test_file::TestFile; #[derive(Clone, Debug)] pub struct TestRepo { - pub path: PathBuf, + path: PathBuf, } impl TestRepo { @@ -216,7 +216,7 @@ impl TestRepo { } pub fn stage_all_and_commit(&self, message: &str) -> Result { - println!("stage_all_and_commit: {:?}", self.git(&["add", "-A"]).expect("add --all should succeed")); + self.git(&["add", "-A"]).expect("add --all should succeed"); self.commit(message) } From a6373790070fc586c1355c8d398b8b0a9dc589b2 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 19:11:29 -0500 Subject: [PATCH 13/19] add to bare repo constructor --- ...__stats__tests__markdown_stats_display.snap.new | 6 ++++++ src/commands/blame.rs | 10 ++++++++-- src/git/repo_storage.rs | 8 ++++---- src/git/repository.rs | 14 ++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new new file mode 100644 index 00000000..91cc02aa --- /dev/null +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new @@ -0,0 +1,6 @@ +--- +source: src/authorship/stats.rs +assertion_line: 768 +expression: mixed_output +--- +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 63%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 31%\n```\n\n
\nMore stats\n\n- 4.0 lines generated for every 1 accepted\n- 1200 minutes waiting for AI \n\n
" diff --git a/src/commands/blame.rs b/src/commands/blame.rs index 86d5b569..ce024af7 100644 --- a/src/commands/blame.rs +++ b/src/commands/blame.rs @@ -201,13 +201,19 @@ impl Repository { let relative_file_path = { let normalized = normalize_to_posix(&relative_file_path); // Strip leading ./ or .\ if present - normalized.strip_prefix("./").unwrap_or(&normalized).to_string() + normalized + .strip_prefix("./") + .unwrap_or(&normalized) + .to_string() }; #[cfg(not(windows))] let relative_file_path = { // Also strip leading ./ on non-Windows for consistency - relative_file_path.strip_prefix("./").unwrap_or(&relative_file_path).to_string() + relative_file_path + .strip_prefix("./") + .unwrap_or(&relative_file_path) + .to_string() }; // Read file content either from a specific commit or from working directory diff --git a/src/git/repo_storage.rs b/src/git/repo_storage.rs index 01521143..355ac7a4 100644 --- a/src/git/repo_storage.rs +++ b/src/git/repo_storage.rs @@ -240,18 +240,18 @@ impl PersistedWorkingLog { // On Windows, this uses the canonical_workdir that was pre-computed #[cfg(windows)] let canonical_workdir = &self.canonical_workdir; - + #[cfg(not(windows))] let canonical_workdir = match self.repo_workdir.canonicalize() { Ok(p) => p, Err(_) => self.repo_workdir.clone(), }; - + let canonical_path = match path.canonicalize() { Ok(p) => p, Err(_) => path.to_path_buf(), }; - + #[cfg(windows)] if canonical_path.starts_with(canonical_workdir) { return canonical_path @@ -260,7 +260,7 @@ impl PersistedWorkingLog { .to_string_lossy() .to_string(); } - + #[cfg(not(windows))] if canonical_path.starts_with(&canonical_workdir) { return canonical_path diff --git a/src/git/repository.rs b/src/git/repository.rs index 5ce76c90..30b1d8af 100644 --- a/src/git/repository.rs +++ b/src/git/repository.rs @@ -931,11 +931,11 @@ impl Repository { if let Ok(canonical_path) = path.canonicalize() { return canonical_path.starts_with(&self.canonical_workdir); } - + // Fallback for paths that don't exist yet: normalize by resolving .. and . - let normalized = path.components().fold( - std::path::PathBuf::new(), - |mut acc, component| { + let normalized = path + .components() + .fold(std::path::PathBuf::new(), |mut acc, component| { match component { std::path::Component::ParentDir => { acc.pop(); @@ -944,8 +944,7 @@ impl Repository { _ => acc.push(component), } acc - }, - ); + }); normalized.starts_with(&self.workdir) } @@ -1776,6 +1775,8 @@ pub fn from_bare_repository(git_dir: &Path) -> Result { .to_path_buf(); let global_args = vec!["-C".to_string(), git_dir.to_string_lossy().to_string()]; + let canonical_workdir = workdir.canonicalize().unwrap_or_else(|_| workdir.clone()); + Ok(Repository { global_args, storage: RepoStorage::for_repo_path(git_dir, &workdir), @@ -1783,6 +1784,7 @@ pub fn from_bare_repository(git_dir: &Path) -> Result { pre_command_base_commit: None, pre_command_refname: None, workdir, + canonical_workdir, }) } From 124d8c466dfb06a73ba9b00650f0d58b388152c5 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 19:13:38 -0500 Subject: [PATCH 14/19] add back snapshots --- ...i__authorship__stats__tests__markdown_stats_display.snap | 2 +- ...uthorship__stats__tests__markdown_stats_display.snap.new | 6 ------ tests/snapshots/stats__markdown_stats_all_ai.snap | 2 +- tests/snapshots/stats__markdown_stats_all_human.snap | 2 +- tests/snapshots/stats__markdown_stats_formatting.snap | 2 +- tests/snapshots/stats__markdown_stats_minimal_human.snap | 2 +- tests/snapshots/stats__markdown_stats_mixed.snap | 2 +- tests/snapshots/stats__markdown_stats_no_mixed.snap | 2 +- 8 files changed, 7 insertions(+), 13 deletions(-) delete mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap index 5889e75a..671cb092 100644 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap @@ -2,4 +2,4 @@ source: src/authorship/stats.rs expression: mixed_output --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 63%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 31%\n```\n\n
\nMore stats\n\n- 4.0 lines generated for every 1 accepted\n- 1200 mins 9 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 63%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 31%\n```\n\n
\nMore stats\n\n- 4.0 lines generated for every 1 accepted\n- 1200 minutes waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new deleted file mode 100644 index 91cc02aa..00000000 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display.snap.new +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: src/authorship/stats.rs -assertion_line: 768 -expression: mixed_output ---- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 63%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 31%\n```\n\n
\nMore stats\n\n- 4.0 lines generated for every 1 accepted\n- 1200 minutes waiting for AI \n\n
" diff --git a/tests/snapshots/stats__markdown_stats_all_ai.snap b/tests/snapshots/stats__markdown_stats_all_ai.snap index 6a6d6869..6a543f15 100644 --- a/tests/snapshots/stats__markdown_stats_all_ai.snap +++ b/tests/snapshots/stats__markdown_stats_all_ai.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\nšŸ¤– ai ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 30 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\nšŸ¤– ai ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 30 seconds waiting for AI \n\n
" diff --git a/tests/snapshots/stats__markdown_stats_all_human.snap b/tests/snapshots/stats__markdown_stats_all_human.snap index 43a68fe8..a291943a 100644 --- a/tests/snapshots/stats__markdown_stats_all_human.snap +++ b/tests/snapshots/stats__markdown_stats_all_human.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 seconds waiting for AI \n\n
" diff --git a/tests/snapshots/stats__markdown_stats_formatting.snap b/tests/snapshots/stats__markdown_stats_formatting.snap index 0fce86af..4a6a753f 100644 --- a/tests/snapshots/stats__markdown_stats_formatting.snap +++ b/tests/snapshots/stats__markdown_stats_formatting.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 38%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 15%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 46%\n```\n\n
\nMore stats\n\n- 1.7 lines generated for every 1 accepted\n- 25 secs time waiting for AI\n- Top model: cursor::claude-3.5-sonnet (6 accepted lines, 10 generated lines)\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 38%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 15%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 46%\n```\n\n
\nMore stats\n\n- 1.7 lines generated for every 1 accepted\n- 25 seconds waiting for AI \n- Top model: cursor::claude-3.5-sonnet (6 accepted lines, 10 generated lines)\n\n
" diff --git a/tests/snapshots/stats__markdown_stats_minimal_human.snap b/tests/snapshots/stats__markdown_stats_minimal_human.snap index 0b893871..2bd81976 100644 --- a/tests/snapshots/stats__markdown_stats_minimal_human.snap +++ b/tests/snapshots/stats__markdown_stats_minimal_human.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 98%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 10 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 98%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 10 seconds waiting for AI \n\n
" diff --git a/tests/snapshots/stats__markdown_stats_mixed.snap b/tests/snapshots/stats__markdown_stats_mixed.snap index 4599a631..e6e36be3 100644 --- a/tests/snapshots/stats__markdown_stats_mixed.snap +++ b/tests/snapshots/stats__markdown_stats_mixed.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 33%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 17%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\n```\n\n
\nMore stats\n\n- 1.7 lines generated for every 1 accepted\n- 45 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 33%\nšŸ¤ mixed ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 17%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 50%\n```\n\n
\nMore stats\n\n- 1.7 lines generated for every 1 accepted\n- 45 seconds waiting for AI \n\n
" diff --git a/tests/snapshots/stats__markdown_stats_no_mixed.snap b/tests/snapshots/stats__markdown_stats_no_mixed.snap index 0d44dec0..880da7b1 100644 --- a/tests/snapshots/stats__markdown_stats_no_mixed.snap +++ b/tests/snapshots/stats__markdown_stats_no_mixed.snap @@ -2,4 +2,4 @@ source: tests/stats.rs expression: markdown --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 40%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 60%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 15 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 40%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 60%\n```\n\n
\nMore stats\n\n- 1.0 lines generated for every 1 accepted\n- 15 seconds waiting for AI \n\n
" From 5f9a56d28ec400b154187ce8d034351c82c18d73 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 20:18:28 -0500 Subject: [PATCH 15/19] fix stats --- ..._authorship__stats__tests__markdown_stats_display-2.snap | 2 +- ...horship__stats__tests__markdown_stats_display-3.snap.new | 6 ++++++ ...horship__stats__tests__markdown_stats_display-4.snap.new | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new create mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-2.snap b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-2.snap index 4413ab4a..89ba6ac2 100644 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-2.snap +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-2.snap @@ -2,4 +2,4 @@ source: src/authorship/stats.rs expression: ai_only_output --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 95%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 45 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 95%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 45 seconds waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new new file mode 100644 index 00000000..3589f2e5 --- /dev/null +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new @@ -0,0 +1,6 @@ +--- +source: src/authorship/stats.rs +assertion_line: 802 +expression: human_only_output +--- +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 seconds waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new new file mode 100644 index 00000000..b40943d3 --- /dev/null +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new @@ -0,0 +1,6 @@ +--- +source: src/authorship/stats.rs +assertion_line: 819 +expression: minimal_human_output +--- +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 93%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 30 seconds waiting for AI \n\n
" From fc4fc982c61866fc7517526144f3d9ce1ce2c519 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 20:19:49 -0500 Subject: [PATCH 16/19] fix other tests --- ..._authorship__stats__tests__markdown_stats_display-3.snap | 2 +- ...horship__stats__tests__markdown_stats_display-3.snap.new | 6 ------ ..._authorship__stats__tests__markdown_stats_display-4.snap | 2 +- ...horship__stats__tests__markdown_stats_display-4.snap.new | 6 ------ 4 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new delete mode 100644 src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap index a42a3e75..4b379051 100644 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap @@ -2,4 +2,4 @@ source: src/authorship/stats.rs expression: human_only_output --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 seconds waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new deleted file mode 100644 index 3589f2e5..00000000 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-3.snap.new +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: src/authorship/stats.rs -assertion_line: 802 -expression: human_only_output ---- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 100%\nšŸ¤– ai ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 0%\n```\n\n
\nMore stats\n\n- 0.0 lines generated for every 1 accepted\n- 0 seconds waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap index eee72b8e..a000284e 100644 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap +++ b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap @@ -2,4 +2,4 @@ source: src/authorship/stats.rs expression: minimal_human_output --- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 93%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 30 secs time waiting for AI\n\n
" +"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 93%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 30 seconds waiting for AI \n\n
" diff --git a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new b/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new deleted file mode 100644 index b40943d3..00000000 --- a/src/authorship/snapshots/git_ai__authorship__stats__tests__markdown_stats_display-4.snap.new +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: src/authorship/stats.rs -assertion_line: 819 -expression: minimal_human_output ---- -"Stats powered by [Git AI](https://github.com/acunniffe/git-ai)\n\n```text\n🧠 you ā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ 2%\nšŸ¤– ai ā–‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ 93%\n```\n\n
\nMore stats\n\n- 1.1 lines generated for every 1 accepted\n- 30 seconds waiting for AI \n\n
" From 1aa52fb5e6df1b51eac7f160d0378229b38c483b Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 20:27:52 -0500 Subject: [PATCH 17/19] revert back to unix file executable --- scripts/dev-symlinks.sh | 0 tests/git-compat/run.py | 0 tests/github/scripts/cleanup-test-repos.sh | 0 tests/github/scripts/run-github-tests.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/dev-symlinks.sh mode change 100644 => 100755 tests/git-compat/run.py mode change 100644 => 100755 tests/github/scripts/cleanup-test-repos.sh mode change 100644 => 100755 tests/github/scripts/run-github-tests.sh diff --git a/scripts/dev-symlinks.sh b/scripts/dev-symlinks.sh old mode 100644 new mode 100755 diff --git a/tests/git-compat/run.py b/tests/git-compat/run.py old mode 100644 new mode 100755 diff --git a/tests/github/scripts/cleanup-test-repos.sh b/tests/github/scripts/cleanup-test-repos.sh old mode 100644 new mode 100755 diff --git a/tests/github/scripts/run-github-tests.sh b/tests/github/scripts/run-github-tests.sh old mode 100644 new mode 100755 From 954822d21adf5fb2ccc375699cdbb1e67cd570cb Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 20:36:05 -0500 Subject: [PATCH 18/19] remove loggers --- src/authorship/rebase_authorship.rs | 7 ------- src/commands/checkpoint.rs | 21 +++++++++---------- src/commands/git_ai_handlers.rs | 3 --- src/git/repo_storage.rs | 4 ---- tests/ai_tab.rs | 32 +++++++++++++---------------- tests/repos/test_file.rs | 8 +++----- tests/repos/test_repo.rs | 6 ++++-- 7 files changed, 31 insertions(+), 50 deletions(-) diff --git a/src/authorship/rebase_authorship.rs b/src/authorship/rebase_authorship.rs index ec63da4c..8465ef62 100644 --- a/src/authorship/rebase_authorship.rs +++ b/src/authorship/rebase_authorship.rs @@ -703,14 +703,10 @@ pub fn rewrite_authorship_after_commit_amend( let changed_files = repo.list_commit_files(amended_commit, None)?; let mut pathspecs: HashSet = changed_files.into_iter().collect(); - // println!("changed_files: {:?}", changed_files); - let working_log = repo.storage.working_log_for_base_commit(original_commit); let touched_files = working_log.all_touched_files()?; pathspecs.extend(touched_files); - println!("pathspecs: {:?}", pathspecs); - // Check if original commit has an authorship log with prompts let has_existing_log = get_reference_as_authorship_log_v3(repo, original_commit).is_ok(); let has_existing_prompts = if has_existing_log { @@ -757,9 +753,6 @@ pub fn rewrite_authorship_after_commit_amend( Some(&pathspecs_set), )?; - - println!("authorship_log: {:?}", authorship_log); - println!("initial_attributions: {:?}", initial_attributions); // Update base commit SHA authorship_log.metadata.base_commit_sha = amended_commit.to_string(); diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs index 5614968a..2ba7390d 100644 --- a/src/commands/checkpoint.rs +++ b/src/commands/checkpoint.rs @@ -24,8 +24,6 @@ pub fn run( agent_run_result: Option, is_pre_commit: bool, ) -> Result<(usize, usize, usize), GitAiError> { - - // Robustly handle zero-commit repos let base_commit = match repo.head() { Ok(head) => match head.target() { @@ -75,7 +73,7 @@ pub fn run( paths.and_then(|p| { let repo_workdir = repo.workdir().ok()?; - + let filtered: Vec = p .iter() .filter_map(|path| { @@ -86,7 +84,7 @@ pub fn run( // Relative path - join with workdir repo_workdir.join(path) }; - + // Use centralized path comparison (handles Windows canonical paths correctly) if repo.path_is_in_workdir(&path_buf) { // Convert to relative path for git operations @@ -98,7 +96,9 @@ pub fn run( // Fallback: try with canonical paths let canonical_workdir = repo_workdir.canonicalize().ok()?; let canonical_path = path_buf.canonicalize().ok()?; - if let Ok(relative) = canonical_path.strip_prefix(&canonical_workdir) { + if let Ok(relative) = + canonical_path.strip_prefix(&canonical_workdir) + { // Normalize path separators to forward slashes for git Some(normalize_to_posix(&relative.to_string_lossy())) } else { @@ -124,8 +124,6 @@ pub fn run( }) }); - - println!("pathspec_filter: {:?}", pathspec_filter); let files = get_all_tracked_files( repo, &base_commit, @@ -134,8 +132,6 @@ pub fn run( is_pre_commit, )?; - println!("files: {:?}", files); - let mut checkpoints = if reset { // If reset flag is set, start with an empty working log working_log.reset_working_log()?; @@ -463,7 +459,9 @@ fn get_checkpoint_entry_for_file( initial_attributions: Arc>>, ts: u128, ) -> Result, GitAiError> { - let current_content = working_log.read_current_file_content(&file_path).unwrap_or_default(); + let current_content = working_log + .read_current_file_content(&file_path) + .unwrap_or_default(); // Try to get previous state from checkpoints first let from_checkpoint = previous_checkpoints.iter().rev().find_map(|checkpoint| { @@ -1256,7 +1254,8 @@ fn is_text_file(working_log: &PersistedWorkingLog, path: &str) -> bool { .unwrap_or(false); if !skip_metadata_check { - if let Ok(metadata) = std::fs::metadata(working_log.to_repo_absolute_path(&normalized_path)) { + if let Ok(metadata) = std::fs::metadata(working_log.to_repo_absolute_path(&normalized_path)) + { if !metadata.is_file() { return false; } diff --git a/src/commands/git_ai_handlers.rs b/src/commands/git_ai_handlers.rs index 45826d9a..14905987 100644 --- a/src/commands/git_ai_handlers.rs +++ b/src/commands/git_ai_handlers.rs @@ -332,9 +332,6 @@ fn handle_checkpoint(args: &[String]) { .map(|r| r.checkpoint_kind) .unwrap_or(CheckpointKind::Human); - println!("checkpoint_kind: {:?}", checkpoint_kind); - println!("files for mock_ai: {:?}", get_all_files_for_mock_ai(&final_working_dir)); - if CheckpointKind::Human == checkpoint_kind && agent_run_result.is_none() { println!( "get_all_files_for_mock_ai HUMAN Checkpoints: {:?}", diff --git a/src/git/repo_storage.rs b/src/git/repo_storage.rs index 355ac7a4..d83f6687 100644 --- a/src/git/repo_storage.rs +++ b/src/git/repo_storage.rs @@ -157,8 +157,6 @@ impl PersistedWorkingLog { } pub fn set_dirty_files(&mut self, dirty_files: Option>) { - println!("pre_transform dirty_files: {:?}", dirty_files); - let normalized_dirty_files = dirty_files.map(|map| { map.into_iter() .map(|(file_path, content)| { @@ -170,8 +168,6 @@ impl PersistedWorkingLog { }); self.dirty_files = normalized_dirty_files; - - println!("setdirty_files: {:?}", self.dirty_files); } pub fn reset_working_log(&self) -> Result<(), GitAiError> { diff --git a/tests/ai_tab.rs b/tests/ai_tab.rs index 9aae3987..0ec6637e 100644 --- a/tests/ai_tab.rs +++ b/tests/ai_tab.rs @@ -135,10 +135,7 @@ fn test_ai_tab_after_edit_checkpoint_includes_dirty_files_and_paths() { let edited = result .edited_filepaths .expect("after_edit should include edited filepaths"); - assert_eq!( - edited, - vec!["/Users/test/project/src/main.rs".to_string()] - ); + assert_eq!(edited, vec!["/Users/test/project/src/main.rs".to_string()]); let dirty_files = result .dirty_files @@ -173,7 +170,10 @@ fn test_ai_tab_rejects_invalid_hook_event() { message ); } - other => panic!("expected PresetError for invalid hook_event_name, got {:?}", other), + other => panic!( + "expected PresetError for invalid hook_event_name, got {:?}", + other + ), } } @@ -252,7 +252,9 @@ fn test_ai_tab_e2e_marks_ai_lines() { ); // AI tab inserts new lines alongside the existing content - let ai_content = "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n".to_string(); + let ai_content = + "console.log(\"hello world\");\n// Log hello world\nconsole.log(\"hello from ai\");\n" + .to_string(); fs::write(&file_path, &ai_content).unwrap(); run_ai_tab_checkpoint( @@ -269,7 +271,8 @@ fn test_ai_tab_e2e_marks_ai_lines() { }), ); - repo.stage_all_and_commit("Accept AI tab completion").unwrap(); + repo.stage_all_and_commit("Accept AI tab completion") + .unwrap(); let mut file = repo.filename(relative_path); file.assert_lines_and_blame(lines![ @@ -304,11 +307,7 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { let lib_file_path_str = lib_file_path.to_string_lossy().to_string(); let readme_file_path_str = readme_file_path.to_string_lossy().to_string(); - println!("lib_file_path_str: {}", lib_file_path_str); - println!("readme_file_path_str: {}", readme_file_path_str); - let working_logs = repo.current_working_logs(); - println!("dirty_files: {:?}", working_logs.dirty_files); // Before edit snapshot includes all dirty files (AI target plus unrelated human edits) run_ai_tab_checkpoint( @@ -333,7 +332,6 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { fs::write(&lib_file_path, &ai_content).unwrap(); let working_logs = repo.current_working_logs(); - println!("dirty_files: {:?}", working_logs.dirty_files); run_ai_tab_checkpoint( &repo, @@ -350,13 +348,11 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { }), ); - let working_logs = repo.current_working_logs(); - println!("dirty_files: {:?}", working_logs.dirty_files); - - let commit_result = repo.stage_all_and_commit("Record AI tab completion while other files dirty").unwrap(); - println!("COMMIT OUTPUT: {}", commit_result.stdout); + let commit_result = repo + .stage_all_and_commit("Record AI tab completion while other files dirty") + .unwrap(); commit_result.print_authorship(); @@ -369,4 +365,4 @@ fn test_ai_tab_e2e_handles_dirty_files_map() { " println!(\"from ai\");".ai(), "}".ai(), ]); -} \ No newline at end of file +} diff --git a/tests/repos/test_file.rs b/tests/repos/test_file.rs index f0de88de..994a41ee 100644 --- a/tests/repos/test_file.rs +++ b/tests/repos/test_file.rs @@ -727,12 +727,10 @@ impl<'a> TestFile<'a> { fn write_and_checkpoint(&self, author_type: &AuthorType) { let contents = self.contents(); fs::write(&self.file_path, contents).unwrap(); - let _ = if author_type == &AuthorType::Ai { - let a = self.repo.git_ai(&["checkpoint", "mock_ai"]); - println!("checkpoint result: {:?}", a); - a + if author_type == &AuthorType::Ai { + self.repo.git_ai(&["checkpoint", "mock_ai"]).unwrap(); } else { - self.repo.git_ai(&["checkpoint"]) + self.repo.git_ai(&["checkpoint"]).unwrap(); }; } diff --git a/tests/repos/test_repo.rs b/tests/repos/test_repo.rs index da6798de..7df7d361 100644 --- a/tests/repos/test_repo.rs +++ b/tests/repos/test_repo.rs @@ -40,7 +40,9 @@ impl TestRepo { } pub fn canonical_path(&self) -> PathBuf { - self.path.canonicalize().expect("failed to canonicalize test repo path") + self.path + .canonicalize() + .expect("failed to canonicalize test repo path") } pub fn stats(&self) -> Result { @@ -228,7 +230,7 @@ impl TestRepo { impl Drop for TestRepo { fn drop(&mut self) { - // fs::remove_dir_all(self.path.clone()).expect("failed to remove test repo"); + fs::remove_dir_all(self.path.clone()).expect("failed to remove test repo"); } } From 1e755c640030c1ff10dadcc86be2494e1765314f Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 10 Nov 2025 20:56:55 -0500 Subject: [PATCH 19/19] parallel tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bb6911d..0e21cdad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,6 @@ jobs: ${{ runner.os }}-cargo- - name: Run tests - run: cargo test -- --test-threads=1 + run: cargo test env: CARGO_INCREMENTAL: 0