From 72638162347f12e066c345a56ec91b9546999e00 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Date: Sat, 8 Aug 2026 21:31:23 +0200 Subject: [PATCH] only offer edit on head revision, open older revisions as tempfile In the revision file tree the `edit [e]` command always opened the file as it exists on disk, regardless of the revision being browsed. That shows content that does not match the revision, or fails outright when the file no longer exists. `edit [e]` is now only offered when the browsed revision is HEAD. For any other revision `open [o]` dumps that revision's blob into a temporary file and opens that in the external editor instead. closes #2147 --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/components/revision_files.rs | 94 +++++++++++++++++++++++++++++--- src/keys/key_list.rs | 2 + src/strings.rs | 12 ++++ 5 files changed, 102 insertions(+), 8 deletions(-) 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!(