mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
Merge ed93629908 into 8619c07f3f
This commit is contained in:
commit
d3eff6240c
8 changed files with 141 additions and 13 deletions
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
26
src/app.rs
26
src/app.rs
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ impl CommitPopup {
|
|||
ExternalEditorPopup::open_file_in_editor(
|
||||
&self.repo.borrow(),
|
||||
&file_path,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut message = String::new();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue