diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ca0eaac2..a7787e03db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805)) * automatically convert spaces to dashes when creating or renaming a branch [[@pbouillon]](https//pbouillon.github.io)] ([#2916](https://github.com/gitui-org/gitui/pull/2916)) * support rewording non-HEAD commits when `commit.gpgsign` is enabled (gpg format only) [[@guerinoni](https://github.com/guerinoni)] ([#2959](https://github.com/gitui-org/gitui/pull/2959)) +* in the revision file tree `edit [e]` is now only offered on the head revision, older revisions offer `open [o]` which dumps that revision of the file into a temporary file and opens it ([#2147](https://github.com/gitui-org/gitui/issues/2147)) ### Fixes * crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895)) diff --git a/Cargo.toml b/Cargo.toml index 585f94a147..dd3fb2ce8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ shellexpand = "3.1" simplelog = { version = "0.12", default-features = false } struct-patch = "0.10" syntect = { version = "5.3", default-features = false, features = ["default-syntaxes", "default-themes", "html", "parsing", "plist-load"] } +tempfile = "3" two-face = { version = "0.4.4", default-features = false } unicode-segmentation = "1.12" unicode-truncate = "2.0" diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 1e15ec086f..7d31c38b92 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -17,7 +17,8 @@ use anyhow::Result; use asyncgit::{ asyncjob::AsyncSingleJob, sync::{ - get_commit_info, CommitId, CommitInfo, RepoPathRef, TreeFile, + get_commit_info, get_head, tree_file_content, CommitId, + CommitInfo, RepoPathRef, TreeFile, }, AsyncGitNotification, AsyncTreeFilesJob, }; @@ -54,6 +55,7 @@ pub struct RevisionFilesComponent { scroll: VerticalScroll, visible: bool, revision: Option, + revision_is_head: bool, focus: Focus, key_config: SharedKeyConfig, select_file: Option, @@ -76,6 +78,7 @@ impl RevisionFilesComponent { env.sender_git.clone(), ), revision: None, + revision_is_head: false, focus: Focus::Tree, key_config: env.key_config.clone(), repo: env.repo.clone(), @@ -100,6 +103,9 @@ impl RevisionFilesComponent { Some(get_commit_info(&self.repo.borrow(), &commit)?); } + self.revision_is_head = get_head(&self.repo.borrow()) + .is_ok_and(|head| head == commit); + Ok(()) } @@ -265,6 +271,54 @@ impl RevisionFilesComponent { }) } + /// dumps the selected file at the currently viewed revision into a + /// temporary file and opens that in the external editor + fn open_file_revision(&self) -> Result<()> { + let Some(file_path) = self.selected_file_path_with_prefix() + else { + return Ok(()); + }; + + let path = Path::new(&file_path); + + let Some(file) = self + .files + .as_ref() + .and_then(|files| files.iter().find(|f| f.path == path)) + else { + return Ok(()); + }; + + let content = tree_file_content(&self.repo.borrow(), file)?; + + let file_name = path + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(); + + let temp_file = tempfile::Builder::new() + .prefix(&format!("gitui-{}-", self.revision_short_id())) + .suffix(&format!("-{file_name}")) + .tempfile()? + .into_temp_path() + .keep()?; + + std::fs::write(&temp_file, content)?; + + self.queue.push(InternalEvent::OpenExternalEditor(Some( + temp_file.to_string_lossy().to_string(), + ))); + + Ok(()) + } + + fn revision_short_id(&self) -> String { + self.revision + .as_ref() + .map(|commit| commit.id.get_short_string()) + .unwrap_or_default() + } + fn selection_changed(&mut self) { //TODO: retrieve TreeFile from tree datastructure if let Some(file) = self.selected_file_path_with_prefix() { @@ -437,11 +491,21 @@ impl Component for RevisionFilesComponent { ) .order(order::NAV), ); - out.push(CommandInfo::new( - strings::commands::edit_item(&self.key_config), - self.tree.selected_file().is_some(), - true, - )); + if self.revision_is_head { + out.push(CommandInfo::new( + strings::commands::edit_item(&self.key_config), + self.tree.selected_file().is_some(), + true, + )); + } else { + out.push(CommandInfo::new( + strings::commands::open_file_revision( + &self.key_config, + ), + self.tree.selected_file().is_some(), + true, + )); + } out.push( CommandInfo::new( strings::commands::open_file_history( @@ -516,9 +580,23 @@ impl Component for RevisionFilesComponent { self.open_finder(); return Ok(EventState::Consumed); } + } else if key_match( + key, + self.key_config.keys.open_file_revision, + ) { + if !self.revision_is_head { + try_or_popup!( + self, + "failed to open file revision:", + self.open_file_revision() + ); + return Ok(EventState::Consumed); + } } else if key_match(key, self.key_config.keys.edit_file) { - if let Some(file) = - self.selected_file_path_with_prefix() + if let Some(file) = self + .revision_is_head + .then(|| self.selected_file_path_with_prefix()) + .flatten() { //Note: switch to status tab so its clear we are // not altering a file inside a revision here diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 24a9507a49..8377cf2706 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -70,6 +70,7 @@ pub struct KeysList { pub blame: GituiKeyEvent, pub file_history: GituiKeyEvent, pub edit_file: GituiKeyEvent, + pub open_file_revision: GituiKeyEvent, pub status_stage_all: GituiKeyEvent, pub status_reset_item: GituiKeyEvent, pub status_ignore_file: GituiKeyEvent, @@ -168,6 +169,7 @@ impl Default for KeysList { blame: GituiKeyEvent::new(KeyCode::Char('B'), KeyModifiers::SHIFT), file_history: GituiKeyEvent::new(KeyCode::Char('H'), KeyModifiers::SHIFT), edit_file: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()), + open_file_revision: GituiKeyEvent::new(KeyCode::Char('o'), KeyModifiers::empty()), status_stage_all: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), status_reset_item: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), diff_reset_lines: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty()), diff --git a/src/strings.rs b/src/strings.rs index 93496cd2ca..0788ec584b 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1232,6 +1232,18 @@ pub mod commands { CMD_GROUP_CHANGES, ) } + pub fn open_file_revision( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Open [{}]", + key_config.get_hint(key_config.keys.open_file_revision), + ), + "open the selected revision of the file in an external editor (as a temporary file)", + CMD_GROUP_CHANGES, + ) + } pub fn stage_item(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!(