3032 lines · 114.8 KB · Rust
RawDownload
//! 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.