(
session_store: &crate::global_db::GlobalDb,
git: &dyn GitReflogSource,
opts: &BackfillOptions,
row: &SessionActivityRow,
analytics_ts: &std::collections::HashMap<(String, String)
| 383 | } |
| 384 | |
| 385 | async fn backfill_one_session( |
| 386 | session_store: &crate::global_db::GlobalDb, |
| 387 | git: &dyn GitReflogSource, |
| 388 | opts: &BackfillOptions, |
| 389 | row: &SessionActivityRow, |
| 390 | analytics_ts: &std::collections::HashMap<(String, String), Vec<i64>>, |
| 391 | stats: &mut BackfillStats, |
| 392 | ) -> Result<(), BackfillSkipReason> { |
| 393 | let (mut win_start, win_end) = row.window().ok_or(BackfillSkipReason::NoActivityWindow)?; |
| 394 | if win_end < opts.since { |
| 395 | return Err(BackfillSkipReason::NoActivityWindow); |
| 396 | } |
| 397 | win_start = win_start.max(opts.since); |
| 398 | if win_start > win_end { |
| 399 | return Err(BackfillSkipReason::NoActivityWindow); |
| 400 | } |
| 401 | |
| 402 | if row.project_path.trim().is_empty() { |
| 403 | return Err(BackfillSkipReason::NotAWorktree); |
| 404 | } |
| 405 | let worktree_path = std::path::Path::new(row.project_path.trim()); |
| 406 | let worktree_root = crate::worktree::git_worktree_root(worktree_path) |
| 407 | .ok_or(BackfillSkipReason::NotAWorktree)?; |
| 408 | let worktree = normalize_worktree(&worktree_root.to_string_lossy()); |
| 409 | |
| 410 | let reflog_text = git |
| 411 | .reflog(&worktree_root) |
| 412 | .ok_or(BackfillSkipReason::GitError)?; |
| 413 | let timeline = branch_timeline_from_reflog(&reflog_text); |
| 414 | let current_branch = git.current_branch(&worktree_root); |
| 415 | |
| 416 | // Extra observation timestamps: analytics event times inside the |
| 417 | // (since-clamped) window, which refine span boundaries within a segment. |
| 418 | let mut analytics_within: Vec<i64> = Vec::new(); |
| 419 | if let Some(times) = analytics_ts.get(&(row.provider.clone(), row.session_id.clone())) { |
| 420 | for &ts in times { |
| 421 | if ts >= win_start && ts <= win_end { |
| 422 | analytics_within.push(ts); |
| 423 | } |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | let segments = window_branch_segments(win_start, win_end, &timeline, current_branch.as_deref()); |
| 428 | |
| 429 | for segment in &segments { |
| 430 | // Every segment yields a span: seed it with its own clamped edges so an |
| 431 | // interior segment (e.g. a mid-session branch switch) is recorded even |
| 432 | // when the global window edges fall outside it. Analytics timestamps |
| 433 | // inside the segment refine the boundaries; record_span_observation |
| 434 | // merges observations on the same branch within the merge gap. |
| 435 | let mut segment_ts = vec![segment.start, segment.end]; |
| 436 | segment_ts.extend( |
| 437 | analytics_within |
| 438 | .iter() |
| 439 | .copied() |
| 440 | .filter(|&ts| ts >= segment.start && ts <= segment.end), |
| 441 | ); |
| 442 | for &ts in &segment_ts { |
no test coverage detected