gitu 0.41.0

A git client inspired by Magit
Documentation
use super::OpTrait;
use crate::{
    Action,
    app::State,
    error::Error,
    item_data::{ItemData, Ref},
    screen,
};
use core::str;
use std::{path::Path, process::Command, rc::Rc, sync::Arc};

pub(crate) struct Show;
impl OpTrait for Show {
    fn get_action(&self, target: &ItemData) -> Option<Action> {
        match target {
            ItemData::Commit { oid, .. }
            | ItemData::Reference {
                kind: Ref::Tag(oid),
                ..
            }
            | ItemData::Reference {
                kind: Ref::Head(oid),
                ..
            } => goto_show_screen(oid.clone()),
            ItemData::Untracked(u) => editor(u.as_path(), None),
            ItemData::Delta { diff, file_i } => {
                let file_path = &diff.file_diffs[*file_i].header.new_file;
                let path: &str = &file_path.fmt(&diff.text);
                editor(Path::new(path), None)
            }
            ItemData::Hunk {
                diff,
                file_i,
                hunk_i,
            } => {
                let file_path = &diff.file_diffs[*file_i].header.new_file;
                let path: &str = &file_path.fmt(&diff.text);
                editor(
                    Path::new(path),
                    Some(diff.file_line_of_first_diff(*file_i, *hunk_i) as u32),
                )
            }
            ItemData::Stash { stash_ref, .. } => goto_show_stash_screen(stash_ref.clone()),
            _ => None,
        }
    }
    fn is_target_op(&self) -> bool {
        true
    }

    fn display(&self, _state: &State) -> String {
        "Show".into()
    }
}

fn goto_show_screen(r: String) -> Option<Action> {
    Some(Rc::new(move |app, term| {
        app.close_menu();
        app.state.screens.push(
            screen::show::create(
                Arc::clone(&app.state.config),
                Rc::clone(&app.state.repo),
                term.size().map_err(Error::Term)?,
                r.clone(),
            )
            .expect("Couldn't create screen"),
        );
        Ok(())
    }))
}

fn goto_show_stash_screen(stash_ref: String) -> Option<Action> {
    Some(Rc::new(move |app, term| {
        app.close_menu();
        app.state.screens.push(
            screen::show_stash::create(
                Arc::clone(&app.state.config),
                Rc::clone(&app.state.repo),
                term.size().map_err(Error::Term)?,
                stash_ref.clone(),
            )
            .expect("Couldn't create stash screen"),
        );
        Ok(())
    }))
}

pub(crate) const EDITOR_VARS: [&str; 4] = ["GITU_SHOW_EDITOR", "VISUAL", "EDITOR", "GIT_EDITOR"];
fn editor(file: &Path, maybe_line: Option<u32>) -> Option<Action> {
    let file = file.to_str().unwrap().to_string();

    Some(Rc::new(move |app, term| {
        let configured_editor = EDITOR_VARS
            .into_iter()
            .find_map(|var| std::env::var(var).ok());

        let Some(editor) = configured_editor else {
            return Err(Error::NoEditorSet);
        };

        let cmd = if cfg!(windows) {
            parse_editor_command_windows(&editor, &file, maybe_line)
        } else {
            parse_editor_command(&editor, &file, maybe_line)
        };

        app.close_menu();
        app.run_cmd_interactive(term, cmd)?;
        app.update_screens()
    }))
}

fn parse_editor_command(editor: &str, file: &str, maybe_line: Option<u32>) -> Command {
    let args = &editor.split_whitespace().collect::<Vec<_>>();
    let mut cmd = Command::new(args[0]);

    cmd.args(&args[1..]);
    cmd.args(line_args(file, maybe_line, args[0].to_lowercase()));
    cmd
}

fn parse_editor_command_windows(editor: &str, file: &str, maybe_line: Option<u32>) -> Command {
    let args = &editor.split_whitespace().collect::<Vec<_>>();
    let mut cmd = Command::new("cmd");
    cmd.arg("/C");
    cmd.arg(args[0]);

    cmd.args(&args[1..]);
    cmd.args(line_args(file, maybe_line, args[0].to_lowercase()));
    cmd
}

fn line_args(file: &str, maybe_line: Option<u32>, lower: String) -> Vec<String> {
    if let Some(line) = maybe_line {
        if lower.ends_with("vi")
            || lower.ends_with("vim")
            || lower.ends_with("nvim")
            || lower.ends_with("nano")
            || lower.ends_with("micro")
            || lower.ends_with("nvr")
        {
            vec![format!("+{line}"), file.to_string()]
        } else {
            vec![format!("{file}:{line}")]
        }
    } else {
        vec![file.to_string()]
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::OsStr;

    #[test]
    fn parse_editor_command_test() {
        let cmd = super::parse_editor_command("/bin/nAnO -f", "README.md", Some(42));
        assert_eq!(cmd.get_program(), OsStr::new("/bin/nAnO"));
        assert_eq!(
            &cmd.get_args().collect::<Vec<_>>(),
            &["-f", "+42", "README.md"]
        );
    }

    #[test]
    fn parse_editor_command_test_windows() {
        let cmd = super::parse_editor_command_windows("/bin/nAnO -f", "README.md", Some(42));
        assert_eq!(cmd.get_program(), OsStr::new("cmd"));
        assert_eq!(
            &cmd.get_args().collect::<Vec<_>>(),
            &["/C", "/bin/nAnO", "-f", "+42", "README.md"]
        );
    }
}