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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ use crate::git::repo_state::{
resolve_stash_target_oid_for_worktree, resolve_worktree_head_reflog_old_oid_for_new_head,
worktree_root_for_path,
};
use crate::git::repository::{Repository, discover_repository_in_path_no_git_exec, exec_git};
use crate::git::repository::{
Repository, discover_repository_in_path_no_git_exec, exec_git,
is_no_repository_discovered_error,
};
use crate::git::rewrite_log::{
CherryPickAbortEvent, CherryPickCompleteEvent, MergeSquashEvent, RebaseAbortEvent,
RebaseCompleteEvent, ResetEvent, ResetKind, RewriteLogEvent, StashEvent, StashOperation,
Expand Down Expand Up @@ -4996,8 +4999,16 @@ impl ActorDaemonCoordinator {
carryover_capture_stage_error(stage, command, input.exit_code, error)
};

let repo = discover_repository_in_path_no_git_exec(input.worktree)
.map_err(|error| with_stage("repo_discovery", error))?;
// The traced command may have run in a directory that is not a git
// repository — for example a temporary scratch directory. There is no
// carryover state to capture there, so treat "no repository found" as a
// benign no-snapshot case rather than a reported error. Real discovery
// faults (e.g. an unreadable git config) still surface.
let repo = match discover_repository_in_path_no_git_exec(input.worktree) {
Ok(repo) => repo,
Err(error) if is_no_repository_discovered_error(&error) => return Ok(None),
Err(error) => return Err(with_stage("repo_discovery", error)),
};
let stable_heads = stable_carryover_heads_for_command(&repo, &input, &parsed)
.map_err(|error| with_stage("stable_heads", error))?;

Expand Down Expand Up @@ -9460,6 +9471,31 @@ mod tests {
);
}

#[tokio::test]
async fn carryover_capture_in_non_repository_worktree_returns_no_snapshot() {
// A git command traced in a temporary, non-repository directory has no
// carryover state to capture. Repo discovery finds nothing, which must
// be a benign no-snapshot result rather than a reported error — the
// daemon otherwise files its own failure into error tracking.
// ActorDaemonCoordinator::new() spawns Tokio tasks, so this runs inside
// a runtime.
let coordinator = ActorDaemonCoordinator::new();
let tmp = tempfile::tempdir().unwrap();
let argv = vec!["git".to_string(), "commit".to_string()];
let result = coordinator.capture_carryover_snapshot_for_command(CarryoverCaptureInput {
root_sid: "sid-123",
worktree: tmp.path(),
primary_command: Some("commit"),
argv: &argv,
exit_code: 0,
finished_at_ns: 0,
post_repo: None,
ref_changes: &[],
});

assert!(matches!(result, Ok(None)), "{result:?}");
}

#[test]
fn ai_replay_checkpoint_request_preserves_active_bash_agent_identity() {
let agent_id = AgentId {
Expand Down
42 changes: 41 additions & 1 deletion src/git/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2346,11 +2346,30 @@ fn discover_repository_paths_no_git_exec(
}

Err(AutterError::Generic(format!(
"No git repository found for path without exec: {}",
"{NO_REPOSITORY_DISCOVERED_PREFIX}: {}",
path.display()
)))
}

/// Message prefix for the "no git repository at or above this path" outcome of
/// [`discover_repository_in_path_no_git_exec`]. Callers that treat a
/// non-repository path as benign match on it through
/// [`is_no_repository_discovered_error`].
const NO_REPOSITORY_DISCOVERED_PREFIX: &str = "No git repository found for path without exec";

/// Returns `true` when `error` is the benign "no repository at this path"
/// outcome of [`discover_repository_in_path_no_git_exec`], as opposed to a real
/// discovery fault such as an unreadable git config. A caller for which a
/// non-repository path is not an error — for example a git command traced in a
/// temporary scratch directory — uses this to skip quietly instead of
/// reporting.
pub fn is_no_repository_discovered_error(error: &AutterError) -> bool {
matches!(
error,
AutterError::Generic(message) if message.starts_with(NO_REPOSITORY_DISCOVERED_PREFIX)
)
}

fn git_config_file_for_repo_paths(
git_dir: &Path,
git_common_dir: &Path,
Expand Down Expand Up @@ -3154,6 +3173,27 @@ mod tests {
assert_eq!(repo.merge_base(a.clone(), a.clone()).unwrap(), Some(a));
}

#[test]
fn discovery_in_non_repository_path_is_recognised_as_benign() {
// A directory that is not a git repository — a temporary scratch dir
// here — yields the "no repository found" outcome, which callers treat
// as benign rather than a fault.
let tmp = tempfile::tempdir().unwrap();
let error = discover_repository_in_path_no_git_exec(tmp.path()).unwrap_err();
assert!(is_no_repository_discovered_error(&error), "{error}");
}

#[test]
fn other_errors_are_not_recognised_as_no_repository() {
// A real fault must not be mistaken for the benign no-repository case.
assert!(!is_no_repository_discovered_error(&AutterError::Generic(
"Git directory has no parent: /tmp/x".to_string()
)));
assert!(!is_no_repository_discovered_error(&AutterError::IoError(
std::io::Error::new(std::io::ErrorKind::NotFound, "gone")
)));
}

#[test]
fn author_config_overlays_full_identity() {
let git_identity = GitAuthorIdentity {
Expand Down
Loading