eazygit/commands/git_commands/
remote.rs

1//! Remote repository commands.
2//!
3//! Handles remote operations including pruning, URL management, and fetching.
4
5use crate::commands::{Command, CommandResult};
6use crate::services::GitService;
7use crate::app::{AppState, Action, reducer};
8use crate::errors::CommandError;
9use crate::commands::git_commands::utils::to_ssh_url;
10use tracing::instrument;
11
12/// Remote prune (remove stale tracking branches)
13pub struct RemotePruneCommand {
14    pub remote: String,
15}
16
17impl Command for RemotePruneCommand {
18    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
19    fn execute(
20        &self,
21        git: &GitService,
22        state: &AppState,
23    ) -> Result<CommandResult, CommandError> {
24        let mut new_state = state.clone();
25        let remote = if self.remote.is_empty() { "origin" } else { self.remote.as_str() };
26        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("remote prune {}…", remote))));
27        match git.remote_prune(&state.repo_path, remote) {
28            Ok(output) => {
29                new_state = reducer(new_state, Action::AppendOpLog(format!("remote prune {} ok", remote)));
30                if !output.trim().is_empty() {
31                    new_state = reducer(new_state, Action::AppendOpLog(output));
32                }
33                new_state = reducer(new_state, Action::SetFeedback(Some(format!("Pruned stale tracking on {}", remote))));
34                new_state = reducer(new_state, Action::SetRefreshing(true));
35            }
36            Err(e) => {
37                new_state = reducer(new_state, Action::AppendOpLog(format!("remote prune error: {e}")));
38                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote prune error: {e}"))));
39            }
40        }
41        new_state = reducer(new_state, Action::SetOpStatus(None));
42        Ok(CommandResult::StateUpdate(new_state))
43    }
44}
45
46/// Show remote URL
47pub struct ShowRemoteUrlCommand {
48    pub remote: String,
49}
50
51impl Command for ShowRemoteUrlCommand {
52    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
53    fn execute(
54        &self,
55        git: &GitService,
56        state: &AppState,
57    ) -> Result<CommandResult, CommandError> {
58        let mut new_state = state.clone();
59        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("remote url {}…", self.remote))));
60        match git.remote_url(&state.repo_path, &self.remote) {
61            Ok(url) => {
62                new_state = reducer(new_state, Action::AppendOpLog(format!("{} url: {}", self.remote, url)));
63                new_state = reducer(new_state, Action::SetFeedback(Some(format!("{} url: {}", self.remote, url))));
64            }
65            Err(e) => {
66                new_state = reducer(new_state, Action::AppendOpLog(format!("remote url error: {e}")));
67                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote url error: {e}"))));
68            }
69        }
70        new_state = reducer(new_state, Action::SetOpStatus(None));
71        Ok(CommandResult::StateUpdate(new_state))
72    }
73}
74
75/// Set remote URL to SSH form if needed
76pub struct SetRemoteSshCommand {
77    pub remote: String,
78}
79
80impl Command for SetRemoteSshCommand {
81    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
82    fn execute(
83        &self,
84        git: &GitService,
85        state: &AppState,
86    ) -> Result<CommandResult, CommandError> {
87        let mut new_state = state.clone();
88        let remote = self.remote.as_str();
89        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("ensure ssh remote {}…", remote))));
90        match git.remote_url(&state.repo_path, remote) {
91            Ok(url) => {
92                if url.starts_with("git@") || url.starts_with("ssh://") {
93                    new_state = reducer(new_state, Action::AppendOpLog(format!("{} already SSH: {}", remote, url)));
94                    new_state = reducer(new_state, Action::SetFeedback(Some(format!("{} already SSH", remote))));
95                } else if let Some(ssh_url) = to_ssh_url(&url) {
96                    match git.set_remote_url(&state.repo_path, remote, &ssh_url) {
97                        Ok(_) => {
98                            new_state = reducer(new_state, Action::AppendOpLog(format!("{} -> {}", url, ssh_url)));
99                            new_state = reducer(new_state, Action::SetFeedback(Some(format!("Set {} to SSH", remote))));
100                        }
101                        Err(e) => {
102                            new_state = reducer(new_state, Action::AppendOpLog(format!("set-url error: {e}")));
103                            new_state = reducer(new_state, Action::SetStatusError(Some(format!("set-url error: {e}"))));
104                        }
105                    }
106                } else {
107                    new_state = reducer(new_state, Action::AppendOpLog(format!("cannot convert to ssh: {}", url)));
108                    new_state = reducer(new_state, Action::SetStatusError(Some("cannot convert remote to ssh".into())));
109                }
110            }
111            Err(e) => {
112                new_state = reducer(new_state, Action::AppendOpLog(format!("remote url error: {e}")));
113                new_state = reducer(new_state, Action::SetStatusError(Some(format!("remote url error: {e}"))));
114            }
115        }
116        new_state = reducer(new_state, Action::SetOpStatus(None));
117        Ok(CommandResult::StateUpdate(new_state))
118    }
119}
120
121/// Auto-fetch from remote
122pub struct AutoFetchCommand {
123    pub remote: String,
124}
125
126impl Command for AutoFetchCommand {
127    #[instrument(skip(self, git, state), fields(remote = %self.remote))]
128    fn execute(
129        &self,
130        git: &GitService,
131        state: &AppState,
132    ) -> Result<CommandResult, CommandError> {
133        let mut new_state = state.clone();
134        let remote = if self.remote.is_empty() { "origin" } else { self.remote.as_str() };
135        new_state = reducer(new_state, Action::SetOpStatus(Some(format!("fetching {}…", remote))));
136        match git.fetch_dry_run(&state.repo_path, remote) {
137            Ok(output) => {
138                if !output.trim().is_empty() {
139                    for line in output.lines() {
140                        new_state = reducer(new_state, Action::AppendOpLog(line.to_string()));
141                    }
142                } else {
143                    new_state = reducer(new_state, Action::AppendOpLog(format!("fetch {}: up to date", remote)));
144                }
145                new_state = reducer(new_state, Action::SetRefreshing(true));
146            }
147            Err(e) => {
148                new_state = reducer(new_state, Action::AppendOpLog(format!("fetch error: {e}")));
149                new_state = reducer(new_state, Action::SetStatusError(Some(format!("fetch error: {e}"))));
150            }
151        }
152        new_state = reducer(new_state, Action::SetOpStatus(None));
153        Ok(CommandResult::StateUpdate(new_state))
154    }
155}
156