eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Remote repository commands.
//!
//! Handles remote operations including pruning, URL management, and fetching.

use crate::commands::{Command, CommandResult};
use crate::services::GitService;
use crate::app::{AppState, Action, reducer};
use crate::errors::CommandError;
use crate::commands::git_commands::utils::to_ssh_url;
use tracing::instrument;

/// Remote prune (remove stale tracking branches)
pub struct RemotePruneCommand {
    pub remote: String,
}

impl Command for RemotePruneCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        let remote = if self.remote.is_empty() { "origin" } else { self.remote.as_str() };
        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("remote prune {}", remote))));
        match git.remote_prune(&state.repo_path, remote) {
            Ok(output) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("remote prune {} ok", remote)));
                if !output.trim().is_empty() {
                    new_state = reducer(new_state, Action::AppendOpLog(output));
                }
                new_state = reducer(new_state, Action::SetFeedback(Some(format!("Pruned stale tracking on {}", remote))));
                new_state = reducer(new_state, Action::SetRefreshing(true));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("remote prune error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote prune error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Show remote URL
pub struct ShowRemoteUrlCommand {
    pub remote: String,
}

impl Command for ShowRemoteUrlCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("remote url {}", self.remote))));
        match git.remote_url(&state.repo_path, &self.remote) {
            Ok(url) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("{} url: {}", self.remote, url)));
                new_state = reducer(new_state, Action::SetFeedback(Some(format!("{} url: {}", self.remote, url))));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("remote url error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote url error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Set remote URL to SSH form if needed
pub struct SetRemoteSshCommand {
    pub remote: String,
}

impl Command for SetRemoteSshCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        let remote = self.remote.as_str();
        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("ensure ssh remote {}", remote))));
        match git.remote_url(&state.repo_path, remote) {
            Ok(url) => {
                if url.starts_with("git@") || url.starts_with("ssh://") {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("{} already SSH: {}", remote, url)));
                    new_state = reducer(new_state, Action::SetFeedback(Some(format!("{} already SSH", remote))));
                } else if let Some(ssh_url) = to_ssh_url(&url) {
                    match git.set_remote_url(&state.repo_path, remote, &ssh_url) {
                        Ok(_) => {
                            new_state = reducer(new_state, Action::AppendOpLog(format!("{} -> {}", url, ssh_url)));
                            new_state = reducer(new_state, Action::SetFeedback(Some(format!("Set {} to SSH", remote))));
                        }
                        Err(e) => {
                            new_state = reducer(new_state, Action::AppendOpLog(format!("set-url error: {e}")));
                            new_state = reducer(new_state, Action::SetStatusError(Some(format!("set-url error: {e}"))));
                        }
                    }
                } else {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("cannot convert to ssh: {}", url)));
                    new_state = reducer(new_state, Action::SetStatusError(Some("cannot convert remote to ssh".into())));
                }
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("remote url error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote url error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Auto-fetch from remote
pub struct AutoFetchCommand {
    pub remote: String,
}

impl Command for AutoFetchCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        let remote = if self.remote.is_empty() { "origin" } else { self.remote.as_str() };
        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("fetching {}", remote))));
        match git.fetch_dry_run(&state.repo_path, remote) {
            Ok(output) => {
                if !output.trim().is_empty() {
                    for line in output.lines() {
                        new_state = reducer(new_state, Action::AppendOpLog(line.to_string()));
                    }
                } else {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("fetch {}: up to date", remote)));
                }
                new_state = reducer(new_state, Action::SetRefreshing(true));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("fetch error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("fetch error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}