stax 0.96.3

Fast stacked Git branches and PRs
Documentation
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
use std::process::Command;

fn gui_command(cwd: &Path) -> Command {
    let mut command = Command::new(crate::common::stax_bin());
    command
        .current_dir(cwd)
        .env_remove("STAX_GUI_OPEN_EXECUTABLE")
        .env_remove("STAX_CONFIG_DIR")
        .env_remove("STAX_GITHUB_TOKEN")
        .env_remove("GITHUB_TOKEN")
        .env_remove("GH_TOKEN")
        .env("STAX_DISABLE_UPDATE_CHECK", "1");
    command
}

#[cfg(target_os = "macos")]
fn recording_launcher(root: &Path, exit_code: i32) -> (PathBuf, PathBuf) {
    use std::os::unix::fs::PermissionsExt;

    let launcher = root.join("record-open");
    let arguments = root.join("arguments");
    std::fs::write(
        &launcher,
        format!(
            "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\nexit {}\n",
            arguments.display(),
            exit_code
        ),
    )
    .unwrap();
    let mut permissions = std::fs::metadata(&launcher).unwrap().permissions();
    permissions.set_mode(0o755);
    std::fs::set_permissions(&launcher, permissions).unwrap();
    (launcher, arguments)
}

#[test]
fn gui_help_works_outside_a_repository() {
    let temp = tempfile::tempdir().unwrap();
    let output = gui_command(temp.path())
        .args(["gui", "--help"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("[PATH]"));
    assert!(stdout.contains("native Stax macOS app"));
}

#[test]
fn gui_missing_path_fails_before_platform_launch() {
    let temp = tempfile::tempdir().unwrap();
    let output = gui_command(temp.path())
        .args(["gui", "missing path"])
        .output()
        .unwrap();
    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("missing path"));
}

#[cfg(all(target_os = "macos", unix))]
#[test]
fn gui_bypasses_initialization_in_plain_git_repository() {
    let repo = crate::common::TestRepo::new();
    let recorder = tempfile::tempdir().unwrap();
    let (launcher, arguments) = recording_launcher(recorder.path(), 0);
    let refs_before = crate::common::TestRepo::stdout(&repo.git(&["show-ref"]));
    let status_before = crate::common::TestRepo::stdout(&repo.git(&["status", "--porcelain=v1"]));
    let output = gui_command(&repo.path())
        .env("STAX_GUI_OPEN_EXECUTABLE", &launcher)
        .args(["gui", repo.path().to_str().unwrap()])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(
        std::fs::read_to_string(arguments)
            .unwrap()
            .lines()
            .collect::<Vec<_>>(),
        vec![
            "-n",
            "-b",
            "com.cesarferreira.stax",
            "--args",
            repo.path().canonicalize().unwrap().to_str().unwrap(),
        ],
    );
    assert_eq!(
        crate::common::TestRepo::stdout(&repo.git(&["show-ref"])),
        refs_before
    );
    assert_eq!(
        crate::common::TestRepo::stdout(&repo.git(&["status", "--porcelain=v1"])),
        status_before
    );
}

#[cfg(all(target_os = "macos", unix))]
#[test]
fn launcher_error_wins_over_active_rebase_and_leaves_repository_untouched() {
    let repo = crate::common::TestRepo::new();
    std::fs::create_dir_all(repo.path().join(".git/rebase-merge")).unwrap();
    let recorder = tempfile::tempdir().unwrap();
    let (launcher, _arguments) = recording_launcher(recorder.path(), 1);
    let refs_before = crate::common::TestRepo::stdout(&repo.git(&["show-ref"]));
    let output = gui_command(&repo.path())
        .env("STAX_GUI_OPEN_EXECUTABLE", &launcher)
        .args(["gui", repo.path().to_str().unwrap()])
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("make install-gui-app"));
    assert!(stderr.contains("GitHub Releases"));
    assert!(!stderr.contains("rebase"));
    assert!(!stderr.contains("st init"));
    assert!(repo.path().join(".git/rebase-merge").is_dir());
    assert_eq!(
        crate::common::TestRepo::stdout(&repo.git(&["show-ref"])),
        refs_before
    );
}

#[cfg(all(target_os = "macos", unix))]
#[test]
fn gui_missing_app_result_is_actionable() {
    let repo = crate::common::TestRepo::new();
    let recorder = tempfile::tempdir().unwrap();
    let (launcher, _arguments) = recording_launcher(recorder.path(), 1);
    let output = gui_command(&repo.path())
        .env("STAX_GUI_OPEN_EXECUTABLE", &launcher)
        .args(["gui", repo.path().to_str().unwrap()])
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("make install-gui-app"));
    assert!(stderr.contains("$HOME/Applications/Stax.app"));
    assert!(stderr.contains("GitHub Releases"));
}

#[cfg(target_os = "macos")]
#[test]
fn gui_spawn_error_wins_over_repository_state() {
    let repo = crate::common::TestRepo::new();
    std::fs::create_dir_all(repo.path().join(".git/rebase-merge")).unwrap();
    let missing_launcher = tempfile::tempdir()
        .unwrap()
        .path()
        .join("missing-open")
        .canonicalize()
        .unwrap_or_else(|_| PathBuf::from("/tmp/stax-missing-open"));
    let output = gui_command(&repo.path())
        .env("STAX_GUI_OPEN_EXECUTABLE", &missing_launcher)
        .args(["gui", repo.path().to_str().unwrap()])
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("make install-gui-app"));
    assert!(stderr.contains("failed to spawn"));
    assert!(!stderr.contains("rebase"));
    assert!(!stderr.contains("st init"));
    assert!(repo.path().join(".git/rebase-merge").is_dir());
}

#[cfg(not(target_os = "macos"))]
#[test]
fn gui_unsupported_platform_never_runs_override() {
    let temp = tempfile::tempdir().unwrap();
    let recorder = tempfile::tempdir().unwrap();
    let launcher = recorder.path().join("record-open");
    let output = gui_command(temp.path())
        .env("STAX_GUI_OPEN_EXECUTABLE", &launcher)
        .args(["gui", temp.path().to_str().unwrap()])
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("only supported on macOS"));
    assert!(!launcher.exists());
}