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
diff --git a/src/authorship/post_commit.rs b/src/authorship/post_commit.rs
index c8ff18da..1132702f 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/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 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-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.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/commands/blame.rs b/src/commands/blame.rs
index 69795efb..ce024af7 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,26 @@ impl Repository {
file_path.to_string()
};
+ // Normalize the file path before use
+ #[cfg(windows)]
+ 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 = {
+ // Also strip leading ./ on non-Windows for consistency
+ relative_file_path
+ .strip_prefix("./")
+ .unwrap_or(&relative_file_path)
+ .to_string()
+ };
+
// Read file content either from a specific commit or from working directory
let (file_content, total_lines) = if let Some(ref commit) = options.newest_commit {
// Read file content from the specified commit
diff --git a/src/commands/checkpoint.rs b/src/commands/checkpoint.rs
index eb065ef5..2ba7390d 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};
@@ -73,45 +73,45 @@ pub fn run(
paths.and_then(|p| {
let repo_workdir = repo.workdir().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) {
- 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() {
- if !canonical.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) {
+ // 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 {
- // 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
- },
- );
- if !normalized_joined.starts_with(&repo_workdir) {
- return None;
- }
+ // Normalize path separators to forward slashes for git
+ Some(normalize_to_posix(path))
}
+ } else {
+ None
}
- Some(path.clone())
})
.collect();
@@ -372,18 +372,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);
}
}
}
@@ -407,11 +411,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);
}
}
}
@@ -453,7 +459,9 @@ fn get_checkpoint_entry_for_file(
initial_attributions: Arc>>,
ts: u128,
) -> Result