This commit is contained in:
吴杨帆 2026-05-17 17:10:17 +08:00 committed by GitHub
commit d3eff6240c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 141 additions and 13 deletions

View file

@ -6,7 +6,7 @@ use crate::{
sync::{get_stashes, repository::repo},
StatusItem, StatusItemType,
};
use git2::{Diff, Repository};
use git2::{Delta, Diff, Repository};
use scopetime::scope_time;
use std::collections::HashSet;
@ -67,17 +67,28 @@ pub fn get_commit_files(
)?
};
fn path_from_delta(delta: git2::DiffDelta) -> String {
let path = if delta.status() == Delta::Deleted {
delta.old_file().path()
} else {
delta
.new_file()
.path()
.or_else(|| delta.old_file().path())
};
path
.map(|p| p.to_str().unwrap_or("").to_string())
.unwrap_or_default()
}
let res = diff
.deltas()
.map(|delta| {
let status = StatusItemType::from(delta.status());
StatusItem {
path: delta
.new_file()
.path()
.map(|p| p.to_str().unwrap_or("").to_string())
.unwrap_or_default(),
path: path_from_delta(delta),
status,
}
})

View file

@ -103,7 +103,7 @@ pub use tags::{
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
TagWithMetadata, Tags,
};
pub use tree::{tree_file_content, tree_files, TreeFile};
pub use tree::{file_content_at_commit, tree_file_content, tree_files, TreeFile};
pub use utils::{
get_head, get_head_tuple, repo_dir, repo_open_error,
stage_add_all, stage_add_file, stage_addremoved, Head,

View file

@ -71,6 +71,40 @@ fn path_cmp(a: &Path, b: &Path) -> Ordering {
}
}
/// UTF-8 text content of `path` in `commit`, or in its first parent when `from_parent`.
pub fn file_content_at_commit(
repo_path: &RepoPath,
commit: CommitId,
path: &Path,
from_parent: bool,
) -> Result<String> {
scope_time!("file_content_at_commit");
let repo = repo(repo_path)?;
let commit = repo.find_commit(commit.into())?;
let object_id = if from_parent {
if commit.parent_count() == 0 {
return Err(Error::Generic(
"commit has no parent".into(),
));
}
commit.parent(0)?.id()
} else {
commit.id()
};
let commit = repo.find_commit(object_id)?;
let tree = commit.tree()?;
let entry = tree.get_path(path)?;
let blob = repo.find_blob(entry.id())?;
if blob.is_binary() {
return Err(Error::BinaryFile);
}
Ok(String::from_utf8_lossy(blob.content()).to_string())
}
/// will only work on utf8 content
pub fn tree_file_content(
repo_path: &RepoPath,

View file

@ -119,6 +119,7 @@ pub struct App {
// "Flags"
requires_redraw: Cell<bool>,
file_to_open: Option<String>,
pending_external_editor: Option<(String, Option<asyncgit::sync::CommitId>, bool)>,
}
pub struct Environment {
@ -244,6 +245,7 @@ impl App {
key_config: env.key_config,
requires_redraw: Cell::new(false),
file_to_open: None,
pending_external_editor: None,
repo: env.repo,
repo_path_text,
popup_stack: PopupStack::default(),
@ -372,10 +374,19 @@ impl App {
self.external_editor_popup.hide();
if matches!(polling_state, InputState::Paused) {
let result =
if let Some(path) = self.file_to_open.take() {
if let Some((path, commit, from_parent)) =
self.pending_external_editor.take()
{
ExternalEditorPopup::open_file_in_editor(
&self.repo.borrow(),
Path::new(&path),
commit.map(|c| (c, from_parent)),
)
} else if let Some(path) = self.file_to_open.take() {
ExternalEditorPopup::open_file_in_editor(
&self.repo.borrow(),
Path::new(&path),
None,
)
} else {
let changes =
@ -816,11 +827,24 @@ impl App {
}
}
InternalEvent::OpenExternalEditor(path) => {
self.pending_external_editor = None;
self.input.set_polling(false);
self.external_editor_popup.show()?;
self.file_to_open = path;
flags.insert(NeedsUpdate::COMMANDS);
}
InternalEvent::OpenExternalEditorAtCommit {
path,
commit,
from_parent,
} => {
self.file_to_open = None;
self.pending_external_editor =
Some((path, Some(commit), from_parent));
self.input.set_polling(false);
self.external_editor_popup.show()?;
flags.insert(NeedsUpdate::COMMANDS);
}
InternalEvent::Push(branch, push_type, force, delete) => {
self.push_popup
.push(branch, push_type, force, delete)?;

View file

@ -15,7 +15,11 @@ use crate::{
ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::{hash, sync::CommitId, StatusItem, StatusItemType};
use asyncgit::{
hash,
sync::{utils::repo_work_dir, CommitId, RepoPathRef},
StatusItem, StatusItemType,
};
use crossterm::event::Event;
use ratatui::{layout::Rect, text::Span, Frame};
use std::{borrow::Cow, cell::Cell, path::Path};
@ -32,6 +36,7 @@ pub struct StatusTreeComponent {
focused: bool,
show_selection: bool,
queue: Queue,
repo: RepoPathRef,
theme: SharedTheme,
key_config: SharedKeyConfig,
scroll_top: Cell<usize>,
@ -49,6 +54,7 @@ impl StatusTreeComponent {
focused: focus,
show_selection: focus,
queue: env.queue.clone(),
repo: env.repo.clone(),
theme: env.theme.clone(),
key_config: env.key_config.clone(),
scroll_top: Cell::new(0),
@ -508,6 +514,31 @@ impl Component for StatusTreeComponent {
} else if key_match(e, self.key_config.keys.edit_file)
{
if let Some(status_item) = self.selection_file() {
if let Some(commit_id) = self.revision {
let from_parent = matches!(
status_item.status,
StatusItemType::Deleted
);
let missing = repo_work_dir(
&self.repo.borrow(),
)
.ok()
.map(|wd| {
std::path::Path::new(&wd)
.join(&status_item.path)
})
.is_none_or(|p| !p.exists());
if from_parent || missing {
self.queue.push(
InternalEvent::OpenExternalEditorAtCommit {
path: status_item.path,
commit: commit_id,
from_parent,
},
);
return Ok(EventState::Consumed);
}
}
self.queue.push(
InternalEvent::OpenExternalEditor(Some(
status_item.path,

View file

@ -189,6 +189,7 @@ impl CommitPopup {
ExternalEditorPopup::open_file_in_editor(
&self.repo.borrow(),
&file_path,
None,
)?;
let mut message = String::new();

View file

@ -10,7 +10,8 @@ use crate::{
};
use anyhow::{anyhow, bail, Result};
use asyncgit::sync::{
get_config_string, utils::repo_work_dir, RepoPath,
file_content_at_commit, get_config_string, utils::repo_work_dir,
CommitId, RepoPath,
};
use crossterm::{
event::Event,
@ -25,6 +26,7 @@ use ratatui::{
};
use scopeguard::defer;
use std::ffi::OsStr;
use std::fs;
use std::{env, io, path::Path, process::Command};
///
@ -48,6 +50,7 @@ impl ExternalEditorPopup {
pub fn open_file_in_editor(
repo: &RepoPath,
path: &Path,
at_commit: Option<(CommitId, bool)>,
) -> Result<()> {
let work_dir = repo_work_dir(repo)?;
@ -57,9 +60,27 @@ impl ExternalEditorPopup {
path.into()
};
if !path.exists() {
let editor_path = if path.exists() {
path
} else if let Some((commit, from_parent)) = at_commit {
let content = file_content_at_commit(
repo,
commit,
&path,
from_parent,
)?;
let file_name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| String::from("file"));
let editor_path = Path::new(&work_dir).join(".git").join(
format!("gitui-edit-{commit}-{file_name}"),
);
fs::write(&editor_path, content.as_bytes())?;
editor_path
} else {
bail!("file not found: {path:?}");
}
};
io::stdout().execute(LeaveAlternateScreen)?;
defer! {
@ -107,7 +128,7 @@ impl ExternalEditorPopup {
let mut args: Vec<&OsStr> =
remainder.map(OsStr::new).collect();
args.push(path.as_os_str());
args.push(editor_path.as_os_str());
Command::new(command.clone())
.current_dir(work_dir)

View file

@ -123,6 +123,12 @@ pub enum InternalEvent {
///
OpenExternalEditor(Option<String>),
///
OpenExternalEditorAtCommit {
path: String,
commit: CommitId,
from_parent: bool,
},
///
Push(String, PushType, bool, bool),
///
Pull(String),