//! Code review diff state management.//!//! Some of the code in this module is adapted from GitHub Desktop, which is licensed under the MIT license,//! Copyright (c) GitHub, Inc. See GITHUB-DESKTOP-LICENSE in this directory. use std::collections::HashMap;use std::fs::File;use std::io::{BufRead, BufReader, Read, Seek};use std::path::{Path, PathBuf};use std::sync::Arc; use anyhow::{Result, anyhow};use instant::Instant;#[cfg(feature = "local_fs")]use warp_util::standardized_path::StandardizedPath; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use std::fs; }}#[cfg(not(target_family = "wasm"))]use warp_core::channel::ChannelState;use warp_core::send_telemetry_from_ctx;#[cfg(feature = "local_fs")]use warp_core::sync_queue::SyncQueue;use warp_util::git::run_git_command;#[cfg(not(target_arch = "wasm32"))]use warpui::AppContext;use warpui::r#async::SpawnedFutureHandle;use warpui::{ModelContext, SingletonEntity}; use crate::code_review::diff_size_limits::{ DiffSize, MAX_DIFF_SIZE, UnrenderableReason, compute_diff_size,};use crate::code_review::{CodeReviewTelemetryEvent, git_actions, is_file_autogenerated};use crate::features::FeatureFlag;use crate::server::server_api::ServerApiProvider;#[cfg(feature = "local_tty")]use crate::terminal::local_shell::LocalShellState;#[cfg(feature = "local_fs")]use crate::throttle::throttle;use crate::util::git::{ Commit, FileChangeEntry, detect_current_branch, detect_main_branch, get_unpushed_commits, parse_unified_diff_header,};#[cfg(feature = "local_fs")]use crate::util::git::{get_all_branches, git_operation_in_progress}; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use std::collections::HashSet; use crate::code_review::file_invalidation_queue::FileInvalidationTask; use repo_metadata::repositories::{DetectedRepositories, RepoDetectionSource}; use repo_metadata::{ repository::{RepositorySubscriber, SubscriberId}, RepoMetadataError, Repository, RepositoryUpdate, RepositoryWatchMode, }; use async_channel::Sender; use warpui::ModelHandle; }}#[cfg(feature = "local_fs")]use warp_errors::report_error; #[cfg(feature = "local_fs")]use super::DiffOperation;use super::{ BackendOrigin, CommitChainMode, DiffHunk, DiffLine, DiffLineType, DiffMetadata, DiffMetadataAgainstBase, DiffMode, DiffState, DiffStateError, DiffStateModelEvent, DiffStats, FileDiff, FileDiffAndContent, FileStatusInfo, GitDiffData, GitDiffWithBaseContent, GitFileStatus, GitOpResult,}; // Unicode bidirectional characters that should be flaggedconst BIDI_CHARS: [char; 9] = [ '\u{202A}', // LEFT-TO-RIGHT EMBEDDING '\u{202B}', // RIGHT-TO-LEFT EMBEDDING '\u{202C}', // POP DIRECTIONAL FORMATTING '\u{202D}', // LEFT-TO-RIGHT OVERRIDE '\u{202E}', // RIGHT-TO-LEFT OVERRIDE '\u{2066}', // LEFT-TO-RIGHT ISOLATE '\u{2067}', // RIGHT-TO-LEFT ISOLATE '\u{2068}', // FIRST STRONG ISOLATE '\u{2069}', // POP DIRECTIONAL ISOLATE]; /// Internal representation of the diffs we've loaded against all bases./// This could include changes against both the latest commit/HEAD/// and changes against the main branch.#[derive(Clone, Default)]enum InternalDiffState { /// Repo detection has been kicked off but hasn't completed yet. /// We don't yet know whether the path is inside a git repository, so this is /// surfaced as a loading state rather than a premature "not a repository" verdict. Detecting, #[default] NotInRepository, Loading, Loaded(Diffs),} #[derive(Clone)]struct Diffs { changes: Result<GitDiffData, String>,} impl From<DiffsWithBaseContent> for Diffs { fn from(value: DiffsWithBaseContent) -> Self { Self { changes: value.changes.map(|diff| diff.into()), } }} impl From<&DiffsWithBaseContent> for Diffs { fn from(value: &DiffsWithBaseContent) -> Self { Self { changes: match value.changes.as_ref() { Ok(changes) => Ok(changes.into()), Err(e) => Err(e.clone()), }, } }} /// IMPORTANT: This struct contains expensive data like the full content of diff files/// at base. This should not be cloned at any time.struct DiffsWithBaseContent { changes: Result<GitDiffWithBaseContent, String>,} /// Tracks state for in-flight file invalidation tasks and full-reload coordination.#[cfg(feature = "local_fs")]struct FileInvalidationState { /// Whether a full invalidation is in-flight. /// When true, per-file invalidation requests are deferred to `pending_file_updates`. invalidate_all_pending: bool, /// Merge base commit for the current diff mode, computed eagerly during /// full invalidation. merge_base: Option<String>, /// Handle for the in-flight merge base computation spawned during full /// invalidation. Aborted when a new full invalidation starts. merge_base_handle: Option<SpawnedFutureHandle>, /// Queue for per-file invalidation tasks queue: SyncQueue<FileInvalidationTask>,} #[cfg(feature = "local_fs")]impl FileInvalidationState { fn new(queue: SyncQueue<FileInvalidationTask>) -> Self { Self { invalidate_all_pending: false, merge_base: None, merge_base_handle: None, queue, } } fn cancel_all(&mut self) { self.queue.cancel_all(); self.merge_base = None; if let Some(handle) = self.merge_base_handle.take() { handle.abort(); } }} /// Files that have been invalidated but not yet processed while a full diff/// reload is in-flight.#[cfg(feature = "local_fs")]struct PendingFileUpdate { repo_path: PathBuf, pending_file_edits: HashSet<PathBuf>,} #[cfg(feature = "local_fs")]impl PendingFileUpdate { fn update_with_file_invalidation( &mut self, repo_path: PathBuf, invalidated_files: Vec<PathBuf>, ) { if self.repo_path != repo_path { self.repo_path = repo_path; self.pending_file_edits = HashSet::from_iter(invalidated_files); } else { self.pending_file_edits.extend(invalidated_files); } }} /// Model that contains all state related to the current pane's open git repository.////// This is the local-only implementation. Consumers should use [`DiffStateModel`] (the wrapper)/// rather than this type directly.pub struct LocalDiffStateModel { #[cfg(feature = "local_fs")] repository: Option<ModelHandle<Repository>>, #[cfg(feature = "local_fs")] subscriber_id: Option<SubscriberId>, state: InternalDiffState, backend_origin: BackendOrigin, mode: DiffMode, metadata: Option<DiffMetadata>, computing_diffs_abort_handle: Option<SpawnedFutureHandle>, computing_metadata_abort_handle: Option<SpawnedFutureHandle>, /// Start time for the latest caller-tracked full diff load. tracked_diff_load_start_time: Option<Instant>, /// Controls whether periodic throttled metadata refresh is active. /// Refresh is suppressed when the code review pane is not open. metadata_refresh_enabled: bool, // TODO: Remove pending file invalidations — pause the queue instead. /// Files that have been invalidated but not yet processed when diff is still loading. #[cfg(feature = "local_fs")] pending_file_updates: Option<PendingFileUpdate>, /// State for tracking in-flight file invalidation tasks. #[cfg(feature = "local_fs")] file_invalidation: FileInvalidationState,} #[derive(Debug, Clone, Copy)]struct GitNumStatMetadata { lines_added: usize, lines_removed: usize, is_binary_file: bool,} impl LocalDiffStateModel { #[cfg(feature = "local_fs")] pub fn new( repo_path: Option<String>, backend_origin: BackendOrigin, ctx: &mut ModelContext<Self>, ) -> Self { // Set up file invalidation queue and subscribe to results // so the model can emit SingleFileUpdated events. let queue = SyncQueue::new_streaming(&ctx.background_executor()); let rx = queue.subscribe(); ctx.spawn_stream_local( rx, |me, broadcast_result: Result<_, Arc<DiffStateError>>, ctx| { if me.file_invalidation.invalidate_all_pending { return; } match broadcast_result { Ok(arc_value) => { let (path, diff): (String, Option<Arc<FileDiffAndContent>>) = match Arc::try_unwrap(arc_value) { Ok((path, diff)) => (path, diff), Err(arc_value) => { let (path, diff) = arc_value.as_ref(); (path.clone(), diff.clone()) } }; ctx.emit(DiffStateModelEvent::SingleFileUpdated { path, diff }); } Err(err) => { err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: me.backend_origin, operation: DiffOperation::FileInvalidation, mode: me.mode.clone(), error: err.to_string(), // Per-file invalidation errors are not tied to a full // tracked load, so `load_duration` is intentionally `None`. load_duration: None, }, ctx ); } } }, |_, _| {}, ); let model = Self { repository: None, state: if repo_path.is_some() { InternalDiffState::Detecting } else { InternalDiffState::NotInRepository }, subscriber_id: None, mode: DiffMode::default(), backend_origin, metadata: None, computing_diffs_abort_handle: None, computing_metadata_abort_handle: None, tracked_diff_load_start_time: None, metadata_refresh_enabled: false, file_invalidation: FileInvalidationState::new(queue), pending_file_updates: None, }; if let Some(repo_path) = &repo_path { let fut = DetectedRepositories::handle(ctx).update(ctx, |model, ctx| { model.detect_possible_local_git_repo( repo_path, RepoDetectionSource::CodeReviewInitialization, ctx, ) }); ctx.spawn(fut, move |me, repo_path, ctx| { if let Some(repo_path) = &repo_path && let Some(repo_handle) = DetectedRepositories::as_ref(ctx) .get_local_watched_repo_for_path(repo_path, ctx) { me.set_active_repository(repo_handle, ctx); return; } // Repo detection completed but found no repository. // Emit so subscribers (e.g. the server model) can drain // pending responses with the NotInRepository state. me.state = InternalDiffState::NotInRepository; me.tracked_diff_load_start_time = None; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); }); } model } #[cfg(not(feature = "local_fs"))] pub fn new( _repo_path: Option<String>, backend_origin: BackendOrigin, _ctx: &mut ModelContext<Self>, ) -> Self { Self { state: InternalDiffState::default(), backend_origin, mode: DiffMode::default(), metadata: None, computing_diffs_abort_handle: None, computing_metadata_abort_handle: None, tracked_diff_load_start_time: None, metadata_refresh_enabled: false, } } pub fn get(&self) -> DiffState { match &self.state { InternalDiffState::Detecting | InternalDiffState::Loading => DiffState::Loading, InternalDiffState::NotInRepository => DiffState::NotInRepository, InternalDiffState::Loaded(diffs) => match &diffs.changes { Ok(_) => DiffState::Loaded, Err(err) => DiffState::Error(err.clone()), }, } } /// Returns the current diff metadata, if available. pub fn metadata(&self) -> Option<&DiffMetadata> { self.metadata.as_ref() } pub fn diff_mode(&self) -> DiffMode { self.mode.clone() } pub fn get_uncommitted_stats(&self) -> Option<DiffStats> { self.metadata .as_ref() .map(|metadata| metadata.against_head.aggregate_stats) } /// Per-file entries for uncommitted-vs-HEAD changes, from cached metadata. pub fn uncommitted_file_entries(&self) -> &[FileChangeEntry] { self.metadata .as_ref() .map(|m| m.against_head.files.as_slice()) .unwrap_or(&[]) } /// Get the name of the main branch being used for comparison pub fn get_main_branch_name(&self) -> Option<String> { self.metadata .as_ref() .map(|metadata| metadata.main_branch_name.clone()) } /// Get the name of the current checked-out branch or commit. pub fn get_current_branch_name(&self) -> Option<String> { self.metadata .as_ref() .map(|metadata| metadata.current_branch_name.clone()) } /// Returns true if the current branch is the main/trunk branch. pub fn is_on_main_branch(&self) -> bool { match (self.get_current_branch_name(), self.get_main_branch_name()) { (Some(current), Some(main)) => { let main_short = main.strip_prefix("origin/").unwrap_or(&main); current == main || current == main_short } _ => false, } } /// The unpushed commits on the current branch (empty if none or no upstream). pub fn unpushed_commits(&self) -> &[Commit] { self.metadata .as_ref() .map(|metadata| metadata.unpushed_commits.as_slice()) .unwrap_or_default() } pub fn upstream_ref(&self) -> Option<&str> { self.metadata .as_ref() .and_then(|m| m.upstream_ref.as_deref()) } /// Returns `true` if the branch's upstream tracking ref differs from the /// detected main branch. `false` when either is unknown, or when the /// upstream points directly at main (e.g. after /// `git checkout -b feature origin/master`). pub fn upstream_differs_from_main(&self) -> bool { match (self.upstream_ref(), self.get_main_branch_name().as_deref()) { (Some(upstream), Some(main)) => upstream != main, _ => false, } } /// Checks if git operations like stash or reset would be blocked due to repository state. /// This performs quick file existence checks to detect if git is in the middle of an operation. #[cfg(feature = "local_fs")] pub fn is_git_operation_blocked(&self, app: &AppContext) -> bool { let Some(repo) = &self.repository else { return false; }; let repo_path = repo.as_ref(app).root_dir().to_local_path_lossy(); git_operation_in_progress(&repo_path) } #[cfg(not(feature = "local_fs"))] pub fn is_git_operation_blocked(&self, _app: &warpui::AppContext) -> bool { false } /// Returns `true` once the repository has at least one commit. pub(super) fn has_head(&self) -> bool { cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { self .metadata .as_ref() .is_some_and(|metadata| metadata.has_head_commit) } else { false } } } #[cfg(feature = "local_fs")] fn active_repository_path(&self, app: &AppContext) -> Option<PathBuf> { self.repository .as_ref()? .as_ref(app) .root_dir() .to_local_path() } #[cfg(not(feature = "local_fs"))] fn active_repository_path(&self, _app: &warpui::AppContext) -> Option<PathBuf> { None } /// Cancels in-flight per-file invalidation tasks and marks a full /// invalidation pending so that stale queue results are suppressed /// until the reload completes. #[cfg(feature = "local_fs")] fn queue_full_invalidation(&mut self) { self.file_invalidation.cancel_all(); self.file_invalidation.invalidate_all_pending = true; } /// Fetches branches for the active repository and emits /// `DiffStateModelEvent::BranchesReceived` on completion. #[cfg(feature = "local_fs")] pub fn fetch_branches(&self, ctx: &mut ModelContext<Self>) { let Some(repo_path) = self.active_repository_path(ctx) else { return; }; ctx.spawn( async move { get_all_branches(&repo_path, None, false /* include_remotes */).await }, |_me, branches_result, ctx| { let branches = match branches_result { Ok(branches) => branches, Err(err) => { log::warn!("LocalDiffStateModel: failed to fetch branches: {err}"); vec![] }Lines 1–500 of 3032. Load more
//! Code review diff state management. //! //! Some of the code in this module is adapted from GitHub Desktop, which is licensed under the MIT license, //! Copyright (c) GitHub, Inc. See GITHUB-DESKTOP-LICENSE in this directory. use std::collections::HashMap; use std::fs::File; use std::io::{BufRead, BufReader, Read, Seek}; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; use instant::Instant; #[cfg(feature = "local_fs")] use warp_util::standardized_path::StandardizedPath; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use std::fs; } } #[cfg(not(target_family = "wasm"))] use warp_core::channel::ChannelState; use warp_core::send_telemetry_from_ctx; #[cfg(feature = "local_fs")] use warp_core::sync_queue::SyncQueue; use warp_util::git::run_git_command; #[cfg(not(target_arch = "wasm32"))] use warpui::AppContext; use warpui::r#async::SpawnedFutureHandle; use warpui::{ModelContext, SingletonEntity}; use crate::code_review::diff_size_limits::{ DiffSize, MAX_DIFF_SIZE, UnrenderableReason, compute_diff_size, }; use crate::code_review::{CodeReviewTelemetryEvent, git_actions, is_file_autogenerated}; use crate::features::FeatureFlag; use crate::server::server_api::ServerApiProvider; #[cfg(feature = "local_tty")] use crate::terminal::local_shell::LocalShellState; #[cfg(feature = "local_fs")] use crate::throttle::throttle; use crate::util::git::{ Commit, FileChangeEntry, detect_current_branch, detect_main_branch, get_unpushed_commits, parse_unified_diff_header, }; #[cfg(feature = "local_fs")] use crate::util::git::{get_all_branches, git_operation_in_progress}; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use std::collections::HashSet; use crate::code_review::file_invalidation_queue::FileInvalidationTask; use repo_metadata::repositories::{DetectedRepositories, RepoDetectionSource}; use repo_metadata::{ repository::{RepositorySubscriber, SubscriberId}, RepoMetadataError, Repository, RepositoryUpdate, RepositoryWatchMode, }; use async_channel::Sender; use warpui::ModelHandle; } } #[cfg(feature = "local_fs")] use warp_errors::report_error; #[cfg(feature = "local_fs")] use super::DiffOperation; use super::{ BackendOrigin, CommitChainMode, DiffHunk, DiffLine, DiffLineType, DiffMetadata, DiffMetadataAgainstBase, DiffMode, DiffState, DiffStateError, DiffStateModelEvent, DiffStats, FileDiff, FileDiffAndContent, FileStatusInfo, GitDiffData, GitDiffWithBaseContent, GitFileStatus, GitOpResult, }; // Unicode bidirectional characters that should be flagged const BIDI_CHARS: [char; 9] = [ '\u{202A}', // LEFT-TO-RIGHT EMBEDDING '\u{202B}', // RIGHT-TO-LEFT EMBEDDING '\u{202C}', // POP DIRECTIONAL FORMATTING '\u{202D}', // LEFT-TO-RIGHT OVERRIDE '\u{202E}', // RIGHT-TO-LEFT OVERRIDE '\u{2066}', // LEFT-TO-RIGHT ISOLATE '\u{2067}', // RIGHT-TO-LEFT ISOLATE '\u{2068}', // FIRST STRONG ISOLATE '\u{2069}', // POP DIRECTIONAL ISOLATE ]; /// Internal representation of the diffs we've loaded against all bases. /// This could include changes against both the latest commit/HEAD /// and changes against the main branch. #[derive(Clone, Default)] enum InternalDiffState { /// Repo detection has been kicked off but hasn't completed yet. /// We don't yet know whether the path is inside a git repository, so this is /// surfaced as a loading state rather than a premature "not a repository" verdict. Detecting, #[default] NotInRepository, Loading, Loaded(Diffs), } #[derive(Clone)] struct Diffs { changes: Result<GitDiffData, String>, } impl From<DiffsWithBaseContent> for Diffs { fn from(value: DiffsWithBaseContent) -> Self { Self { changes: value.changes.map(|diff| diff.into()), } } } impl From<&DiffsWithBaseContent> for Diffs { fn from(value: &DiffsWithBaseContent) -> Self { Self { changes: match value.changes.as_ref() { Ok(changes) => Ok(changes.into()), Err(e) => Err(e.clone()), }, } } } /// IMPORTANT: This struct contains expensive data like the full content of diff files /// at base. This should not be cloned at any time. struct DiffsWithBaseContent { changes: Result<GitDiffWithBaseContent, String>, } /// Tracks state for in-flight file invalidation tasks and full-reload coordination. #[cfg(feature = "local_fs")] struct FileInvalidationState { /// Whether a full invalidation is in-flight. /// When true, per-file invalidation requests are deferred to `pending_file_updates`. invalidate_all_pending: bool, /// Merge base commit for the current diff mode, computed eagerly during /// full invalidation. merge_base: Option<String>, /// Handle for the in-flight merge base computation spawned during full /// invalidation. Aborted when a new full invalidation starts. merge_base_handle: Option<SpawnedFutureHandle>, /// Queue for per-file invalidation tasks queue: SyncQueue<FileInvalidationTask>, } #[cfg(feature = "local_fs")] impl FileInvalidationState { fn new(queue: SyncQueue<FileInvalidationTask>) -> Self { Self { invalidate_all_pending: false, merge_base: None, merge_base_handle: None, queue, } } fn cancel_all(&mut self) { self.queue.cancel_all(); self.merge_base = None; if let Some(handle) = self.merge_base_handle.take() { handle.abort(); } } } /// Files that have been invalidated but not yet processed while a full diff /// reload is in-flight. #[cfg(feature = "local_fs")] struct PendingFileUpdate { repo_path: PathBuf, pending_file_edits: HashSet<PathBuf>, } #[cfg(feature = "local_fs")] impl PendingFileUpdate { fn update_with_file_invalidation( &mut self, repo_path: PathBuf, invalidated_files: Vec<PathBuf>, ) { if self.repo_path != repo_path { self.repo_path = repo_path; self.pending_file_edits = HashSet::from_iter(invalidated_files); } else { self.pending_file_edits.extend(invalidated_files); } } } /// Model that contains all state related to the current pane's open git repository. /// /// This is the local-only implementation. Consumers should use [`DiffStateModel`] (the wrapper) /// rather than this type directly. pub struct LocalDiffStateModel { #[cfg(feature = "local_fs")] repository: Option<ModelHandle<Repository>>, #[cfg(feature = "local_fs")] subscriber_id: Option<SubscriberId>, state: InternalDiffState, backend_origin: BackendOrigin, mode: DiffMode, metadata: Option<DiffMetadata>, computing_diffs_abort_handle: Option<SpawnedFutureHandle>, computing_metadata_abort_handle: Option<SpawnedFutureHandle>, /// Start time for the latest caller-tracked full diff load. tracked_diff_load_start_time: Option<Instant>, /// Controls whether periodic throttled metadata refresh is active. /// Refresh is suppressed when the code review pane is not open. metadata_refresh_enabled: bool, // TODO: Remove pending file invalidations — pause the queue instead. /// Files that have been invalidated but not yet processed when diff is still loading. #[cfg(feature = "local_fs")] pending_file_updates: Option<PendingFileUpdate>, /// State for tracking in-flight file invalidation tasks. #[cfg(feature = "local_fs")] file_invalidation: FileInvalidationState, } #[derive(Debug, Clone, Copy)] struct GitNumStatMetadata { lines_added: usize, lines_removed: usize, is_binary_file: bool, } impl LocalDiffStateModel { #[cfg(feature = "local_fs")] pub fn new( repo_path: Option<String>, backend_origin: BackendOrigin, ctx: &mut ModelContext<Self>, ) -> Self { // Set up file invalidation queue and subscribe to results // so the model can emit SingleFileUpdated events. let queue = SyncQueue::new_streaming(&ctx.background_executor()); let rx = queue.subscribe(); ctx.spawn_stream_local( rx, |me, broadcast_result: Result<_, Arc<DiffStateError>>, ctx| { if me.file_invalidation.invalidate_all_pending { return; } match broadcast_result { Ok(arc_value) => { let (path, diff): (String, Option<Arc<FileDiffAndContent>>) = match Arc::try_unwrap(arc_value) { Ok((path, diff)) => (path, diff), Err(arc_value) => { let (path, diff) = arc_value.as_ref(); (path.clone(), diff.clone()) } }; ctx.emit(DiffStateModelEvent::SingleFileUpdated { path, diff }); } Err(err) => { err.report_and_log(); send_telemetry_from_ctx!( CodeReviewTelemetryEvent::LoadDiffFailed { backend_origin: me.backend_origin, operation: DiffOperation::FileInvalidation, mode: me.mode.clone(), error: err.to_string(), // Per-file invalidation errors are not tied to a full // tracked load, so `load_duration` is intentionally `None`. load_duration: None, }, ctx ); } } }, |_, _| {}, ); let model = Self { repository: None, state: if repo_path.is_some() { InternalDiffState::Detecting } else { InternalDiffState::NotInRepository }, subscriber_id: None, mode: DiffMode::default(), backend_origin, metadata: None, computing_diffs_abort_handle: None, computing_metadata_abort_handle: None, tracked_diff_load_start_time: None, metadata_refresh_enabled: false, file_invalidation: FileInvalidationState::new(queue), pending_file_updates: None, }; if let Some(repo_path) = &repo_path { let fut = DetectedRepositories::handle(ctx).update(ctx, |model, ctx| { model.detect_possible_local_git_repo( repo_path, RepoDetectionSource::CodeReviewInitialization, ctx, ) }); ctx.spawn(fut, move |me, repo_path, ctx| { if let Some(repo_path) = &repo_path && let Some(repo_handle) = DetectedRepositories::as_ref(ctx) .get_local_watched_repo_for_path(repo_path, ctx) { me.set_active_repository(repo_handle, ctx); return; } // Repo detection completed but found no repository. // Emit so subscribers (e.g. the server model) can drain // pending responses with the NotInRepository state. me.state = InternalDiffState::NotInRepository; me.tracked_diff_load_start_time = None; ctx.emit(DiffStateModelEvent::NewDiffsComputed { diffs: None, load_duration: None, }); }); } model } #[cfg(not(feature = "local_fs"))] pub fn new( _repo_path: Option<String>, backend_origin: BackendOrigin, _ctx: &mut ModelContext<Self>, ) -> Self { Self { state: InternalDiffState::default(), backend_origin, mode: DiffMode::default(), metadata: None, computing_diffs_abort_handle: None, computing_metadata_abort_handle: None, tracked_diff_load_start_time: None, metadata_refresh_enabled: false, } } pub fn get(&self) -> DiffState { match &self.state { InternalDiffState::Detecting | InternalDiffState::Loading => DiffState::Loading, InternalDiffState::NotInRepository => DiffState::NotInRepository, InternalDiffState::Loaded(diffs) => match &diffs.changes { Ok(_) => DiffState::Loaded, Err(err) => DiffState::Error(err.clone()), }, } } /// Returns the current diff metadata, if available. pub fn metadata(&self) -> Option<&DiffMetadata> { self.metadata.as_ref() } pub fn diff_mode(&self) -> DiffMode { self.mode.clone() } pub fn get_uncommitted_stats(&self) -> Option<DiffStats> { self.metadata .as_ref() .map(|metadata| metadata.against_head.aggregate_stats) } /// Per-file entries for uncommitted-vs-HEAD changes, from cached metadata. pub fn uncommitted_file_entries(&self) -> &[FileChangeEntry] { self.metadata .as_ref() .map(|m| m.against_head.files.as_slice()) .unwrap_or(&[]) } /// Get the name of the main branch being used for comparison pub fn get_main_branch_name(&self) -> Option<String> { self.metadata .as_ref() .map(|metadata| metadata.main_branch_name.clone()) } /// Get the name of the current checked-out branch or commit. pub fn get_current_branch_name(&self) -> Option<String> { self.metadata .as_ref() .map(|metadata| metadata.current_branch_name.clone()) } /// Returns true if the current branch is the main/trunk branch. pub fn is_on_main_branch(&self) -> bool { match (self.get_current_branch_name(), self.get_main_branch_name()) { (Some(current), Some(main)) => { let main_short = main.strip_prefix("origin/").unwrap_or(&main); current == main || current == main_short } _ => false, } } /// The unpushed commits on the current branch (empty if none or no upstream). pub fn unpushed_commits(&self) -> &[Commit] { self.metadata .as_ref() .map(|metadata| metadata.unpushed_commits.as_slice()) .unwrap_or_default() } pub fn upstream_ref(&self) -> Option<&str> { self.metadata .as_ref() .and_then(|m| m.upstream_ref.as_deref()) } /// Returns `true` if the branch's upstream tracking ref differs from the /// detected main branch. `false` when either is unknown, or when the /// upstream points directly at main (e.g. after /// `git checkout -b feature origin/master`). pub fn upstream_differs_from_main(&self) -> bool { match (self.upstream_ref(), self.get_main_branch_name().as_deref()) { (Some(upstream), Some(main)) => upstream != main, _ => false, } } /// Checks if git operations like stash or reset would be blocked due to repository state. /// This performs quick file existence checks to detect if git is in the middle of an operation. #[cfg(feature = "local_fs")] pub fn is_git_operation_blocked(&self, app: &AppContext) -> bool { let Some(repo) = &self.repository else { return false; }; let repo_path = repo.as_ref(app).root_dir().to_local_path_lossy(); git_operation_in_progress(&repo_path) } #[cfg(not(feature = "local_fs"))] pub fn is_git_operation_blocked(&self, _app: &warpui::AppContext) -> bool { false } /// Returns `true` once the repository has at least one commit. pub(super) fn has_head(&self) -> bool { cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { self .metadata .as_ref() .is_some_and(|metadata| metadata.has_head_commit) } else { false } } } #[cfg(feature = "local_fs")] fn active_repository_path(&self, app: &AppContext) -> Option<PathBuf> { self.repository .as_ref()? .as_ref(app) .root_dir() .to_local_path() } #[cfg(not(feature = "local_fs"))] fn active_repository_path(&self, _app: &warpui::AppContext) -> Option<PathBuf> { None } /// Cancels in-flight per-file invalidation tasks and marks a full /// invalidation pending so that stale queue results are suppressed /// until the reload completes. #[cfg(feature = "local_fs")] fn queue_full_invalidation(&mut self) { self.file_invalidation.cancel_all(); self.file_invalidation.invalidate_all_pending = true; } /// Fetches branches for the active repository and emits /// `DiffStateModelEvent::BranchesReceived` on completion. #[cfg(feature = "local_fs")] pub fn fetch_branches(&self, ctx: &mut ModelContext<Self>) { let Some(repo_path) = self.active_repository_path(ctx) else { return; }; ctx.spawn( async move { get_all_branches(&repo_path, None, false /* include_remotes */).await }, |_me, branches_result, ctx| { let branches = match branches_result { Ok(branches) => branches, Err(err) => { log::warn!("LocalDiffStateModel: failed to fetch branches: {err}"); vec![] }