eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Pull Request (PR) commands.
//!
//! Handles GitHub-style pull request operations including listing, checkout, and opening in browser.

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_github_pr_url;
use std::process::Command as StdCommand;
use tracing::instrument;

/// List PRs (GitHub-style) and update state
pub struct PrListCommand {
    pub remote: String,
}

impl Command for PrListCommand {
    #[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!("list PRs on {}", self.remote))));
        match git.list_pull_requests(&state.repo_path, &self.remote) {
            Ok(list) => {
                let mut pr_items = Vec::new();
                for (num, refname) in list.into_iter().take(50) {
                    pr_items.push(crate::app::state::PrItem {
                        number: num.clone(),
                        title: format!("PR #{} ({})", num, refname),
                        _remote: self.remote.clone(),
                        _branch: format!("pr/{}", num),
                    });
                }
                new_state = reducer(new_state, Action::SetPrList(pr_items));
                new_state = reducer(new_state, Action::ShowPrHelper);
                new_state = reducer(new_state, Action::AppendOpLog(format!("Listed PRs from {}", self.remote)));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("pr list error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("pr list error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Checkout selected PR branch
pub struct PrCheckoutCommand {
    pub remote: String,
    pub pr: String,
}

impl Command for PrCheckoutCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote, pr = %self.pr))]
    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!("fetch PR {}", self.pr))));
        match git.fetch_pr(&state.repo_path, &self.remote, &self.pr) {
            Ok(_) => {
                match git.checkout_pr_branch(&state.repo_path, &self.pr) {
                    Ok(_) => {
                        new_state = reducer(new_state, Action::AppendOpLog(format!("Checked out pr/{}", self.pr)));
                        new_state = reducer(new_state, Action::SetFeedback(Some(format!("Checked out pr/{}", self.pr))));
                    }
                    Err(e) => {
                        new_state = reducer(new_state, Action::AppendOpLog(format!("checkout pr error: {e}")));
                        new_state = reducer(new_state, Action::SetStatusError(Some(format!("checkout pr error: {e}"))));
                    }
                }
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("fetch pr error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("fetch pr error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

/// Open PR in browser (GitHub-style)
pub struct PrOpenCommand {
    pub remote: String,
    pub pr: String,
}

impl Command for PrOpenCommand {
    #[instrument(skip(self, git, state), fields(remote = %self.remote, pr = %self.pr))]
    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!("open PR {}", self.pr))));
        match git.remote_url(&state.repo_path, &self.remote) {
            Ok(url) => {
                if let Some(open_url) = to_github_pr_url(&url, &self.pr) {
                    let cmd = if cfg!(target_os = "macos") { "open" } else { "xdg-open" };
                    let status = StdCommand::new(cmd).arg(&open_url).status();
                    match status {
                        Ok(st) if st.success() => {
                            new_state = reducer(new_state, Action::AppendOpLog(format!("opened {}", open_url)));
                        }
                        Ok(st) => {
                            new_state = reducer(new_state, Action::SetStatusError(Some(format!("browser exit {}", st))));
                        }
                        Err(e) => {
                            new_state = reducer(new_state, Action::SetStatusError(Some(format!("browser error: {e}"))));
                        }
                    }
                } else {
                    new_state = reducer(new_state, Action::SetStatusError(Some("unsupported remote for PR URL".into())));
                }
            }
            Err(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))
    }
}