gitstack 5.3.0

Git history viewer with insights - Author stats, file heatmap, code ownership
Documentation
//! One-key PR creation workflow
//!
//! Provides PR creation via `gh` CLI with auto-generated title and body
//! from the current branch's review pack.

use std::process::Command;

/// PR creation state
#[derive(Debug, Clone, Default)]
pub struct PrCreateState {
    /// PR title
    pub title: String,
    /// PR body (markdown)
    pub body: String,
    /// Base branch name
    pub base_branch: String,
    /// Whether `gh` CLI is available
    pub gh_available: bool,
    /// Cursor position in title input
    pub cursor_pos: usize,
    /// Whether we are editing the body (vs title)
    pub editing_body: bool,
}

/// Check if `gh` CLI is available
pub fn check_gh_available() -> bool {
    Command::new("which")
        .arg("gh")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

/// Create a pull request using `gh` CLI
///
/// Returns the URL of the created PR on success.
pub fn create_pr(state: &PrCreateState) -> Result<String, String> {
    if !state.gh_available {
        return Err("gh CLI is not available. Install it from https://cli.github.com/".to_string());
    }

    if state.title.trim().is_empty() {
        return Err("PR title cannot be empty".to_string());
    }

    let output = Command::new("gh")
        .args([
            "pr",
            "create",
            "--title",
            &state.title,
            "--body",
            &state.body,
            "--base",
            &state.base_branch,
        ])
        .output()
        .map_err(|e| format!("Failed to execute gh: {}", e))?;

    if output.status.success() {
        let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
        Ok(url)
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        Err(format!("gh pr create failed: {}", stderr))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pr_create_state_default() {
        let state = PrCreateState::default();
        assert!(state.title.is_empty());
        assert!(state.body.is_empty());
        assert!(state.base_branch.is_empty());
        assert!(!state.gh_available);
        assert_eq!(state.cursor_pos, 0);
        assert!(!state.editing_body);
    }

    #[test]
    fn test_pr_create_state_clone() {
        let state = PrCreateState {
            title: "My PR".to_string(),
            body: "Description".to_string(),
            base_branch: "main".to_string(),
            gh_available: true,
            cursor_pos: 5,
            editing_body: true,
        };
        let cloned = state.clone();
        assert_eq!(cloned.title, "My PR");
        assert_eq!(cloned.body, "Description");
        assert_eq!(cloned.base_branch, "main");
        assert!(cloned.gh_available);
        assert_eq!(cloned.cursor_pos, 5);
        assert!(cloned.editing_body);
    }

    #[test]
    fn test_create_pr_empty_title() {
        let state = PrCreateState {
            gh_available: true,
            title: "".to_string(),
            ..Default::default()
        };
        let result = create_pr(&state);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("empty"));
    }

    #[test]
    fn test_create_pr_whitespace_title() {
        let state = PrCreateState {
            gh_available: true,
            title: "   ".to_string(),
            ..Default::default()
        };
        let result = create_pr(&state);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("empty"));
    }

    #[test]
    fn test_create_pr_no_gh() {
        let state = PrCreateState {
            gh_available: false,
            title: "test PR".to_string(),
            ..Default::default()
        };
        let result = create_pr(&state);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not available"));
    }

    #[test]
    fn test_check_gh_available_returns_bool() {
        // This just verifies it doesn't panic - actual result depends on environment
        let _result = check_gh_available();
    }
}