use std::path::Path;
use super::App;
use super::rowsel::RowSelect;
use crate::editor::{self, EditorPurpose, EditorRequest};
impl App {
pub(super) fn editor_command(&self) -> String {
editor::resolve(
self.config.editor.command.as_deref(),
std::env::var("DIFFLER_EDITOR").ok().as_deref(),
std::env::var("EDITOR").ok().as_deref(),
)
}
pub(crate) fn request_editor(&mut self, path: &str, line: Option<u32>) {
let absolute = self.review.repo_root.join(path);
let cmd = editor::command_for(&self.editor_command(), &absolute, line);
self.pending_editor = Some(EditorRequest {
cmd,
purpose: EditorPurpose::OpenFile {
path: path.to_owned(),
},
});
}
pub(crate) fn commit_flow(&mut self) {
let selected = self.selected_working_copy_paths();
if selected.as_ref().is_some_and(Vec::is_empty) {
self.info("select file rows to commit them, or press esc to commit everything");
return;
}
let paths = selected.unwrap_or_default();
let staged = &self.review.status.staged.files;
if staged.is_empty() {
self.info("nothing staged");
return;
}
let picked: Vec<diffler_core::model::FileDiff> = staged
.iter()
.filter(|file| {
paths.is_empty() || paths.iter().any(|p| Path::new(&file.path).starts_with(p))
})
.cloned()
.collect();
let template = editor::commit_template(&picked);
if !paths.is_empty() {
self.status.set_anchor(None);
}
self.queue_message_editor("COMMIT_EDITMSG", template, |msg_path| {
EditorPurpose::Commit { msg_path, paths }
});
}
pub(crate) fn commit_extend(&mut self) {
if self.review.status.staged.files.is_empty() {
self.info("nothing staged");
return;
}
if self.head.oid7.is_empty() {
self.info("no commit to extend");
return;
}
self.apply_amend(None, true);
}
pub(crate) fn commit_amend(&mut self) {
self.amend_via_editor(true);
}
pub(crate) fn commit_reword(&mut self) {
self.amend_via_editor(false);
}
pub(super) fn amend_via_editor(&mut self, use_index: bool) {
if self.head.oid7.is_empty() {
self.info("no commit to amend");
return;
}
let existing = match self.review.vcs.head_message() {
Ok(message) => message,
Err(err) => {
self.error(err.to_string());
return;
}
};
let staged: &[diffler_core::model::FileDiff] = if use_index {
&self.review.status.staged.files
} else {
&[]
};
let template = editor::amend_template(&existing, staged);
self.queue_message_editor("COMMIT_EDITMSG", template, move |msg_path| {
EditorPurpose::Amend {
msg_path,
use_index,
}
});
}
pub(super) fn queue_message_editor(
&mut self,
file_name: &str,
template: String,
purpose: impl FnOnce(std::path::PathBuf) -> EditorPurpose,
) -> bool {
let git_dir = match self.review.vcs.git_dir() {
Ok(dir) => dir,
Err(err) => {
self.error(err.to_string());
return false;
}
};
let msg_path = git_dir.join(file_name);
if let Err(err) = std::fs::write(&msg_path, template) {
self.error(format!("cannot write {}: {err}", msg_path.display()));
return false;
}
let cmd = editor::command_for(&self.editor_command(), &msg_path, None);
self.pending_editor = Some(EditorRequest {
cmd,
purpose: purpose(msg_path),
});
true
}
pub(super) fn queue_scratch_editor(
&mut self,
template: &str,
purpose: impl FnOnce(std::path::PathBuf) -> EditorPurpose,
) -> bool {
let path = std::env::temp_dir().join(format!("diffler-edit-{}.md", uuid::Uuid::new_v4()));
if let Err(err) = std::fs::write(&path, template) {
self.error(format!("cannot write {}: {err}", path.display()));
return false;
}
let cmd = editor::command_for(&self.editor_command(), &path, None);
self.pending_editor = Some(EditorRequest {
cmd,
purpose: purpose(path),
});
true
}
pub(super) fn take_scratch_edit(
&mut self,
path: &Path,
outcome: Result<bool, String>,
) -> Option<String> {
let read = match outcome {
Ok(true) => std::fs::read_to_string(path),
Ok(false) => {
self.info("edit aborted");
let _ = std::fs::remove_file(path);
return None;
}
Err(err) => {
self.error(format!("editor failed: {err}"));
let _ = std::fs::remove_file(path);
return None;
}
};
let _ = std::fs::remove_file(path);
match read {
Ok(text) => Some(text.strip_suffix('\n').unwrap_or(&text).to_owned()),
Err(err) => {
self.error(format!("cannot read {}: {err}", path.display()));
None
}
}
}
pub(super) fn apply_text_box_edit(
&mut self,
target: crate::editor::TextBoxTarget,
text: String,
) {
use crate::editor::TextBoxTarget;
match target {
TextBoxTarget::Composer => {
let Some(composer) = self.diff.as_mut().and_then(|d| d.composer.as_mut()) else {
return;
};
composer.buffer = text;
composer.cursor = composer.buffer.chars().count();
if let Some(diff) = self.diff.as_mut() {
diff.mark_reflow();
diff.ensure_rows(&self.review);
}
}
TextBoxTarget::Input => {
let Some(super::Modal::Input { buffer, cursor, .. }) = self.modal.as_mut() else {
return;
};
*buffer = text;
*cursor = buffer.chars().count();
}
}
}
pub(super) fn apply_amend(&mut self, message: Option<&str>, use_index: bool) {
match self.review.vcs.amend(message, use_index) {
Ok(oid) => {
let subject = message
.and_then(|text| text.lines().next())
.unwrap_or(&self.head.subject)
.to_owned();
self.queue_refresh();
let oid7 = oid.get(..7).unwrap_or(&oid).to_owned();
if self.message.is_none() {
self.info(format!("amended {oid7} {subject}"));
}
}
Err(err) => self.error(err.to_string()),
}
}
pub fn editor_finished(&mut self, purpose: EditorPurpose, outcome: Result<bool, String>) {
self.message = None;
match purpose {
EditorPurpose::Commit { msg_path, paths } => {
self.finish_commit(&msg_path, &paths, outcome);
}
EditorPurpose::Amend {
msg_path,
use_index,
} => self.finish_amend(&msg_path, use_index, outcome),
EditorPurpose::PrBody {
msg_path,
mut draft,
field,
} => {
if let Some(text) = self.take_scratch_edit(&msg_path, outcome) {
match field {
crate::app::pr_create::PrTextField::Title => draft.title = text,
crate::app::pr_create::PrTextField::Body => draft.body = text,
}
}
self.modal = Some(super::Modal::CreatePr { draft });
}
EditorPurpose::TextBox { path, target } => {
if let Some(text) = self.take_scratch_edit(&path, outcome) {
self.apply_text_box_edit(target, text);
}
}
EditorPurpose::OpenFile { path } => {
if let Err(err) = outcome {
self.error(format!("editor failed: {err}"));
}
self.queue_refresh();
if self.message.is_none() {
self.info(format!("edited {path}"));
}
}
}
}
pub(super) fn finish_commit(
&mut self,
msg_path: &Path,
paths: &[String],
outcome: Result<bool, String>,
) {
match outcome {
Err(err) => {
self.error(format!("editor failed: {err}"));
return;
}
Ok(false) => {
self.info("commit aborted");
return;
}
Ok(true) => {}
}
let raw = match std::fs::read_to_string(msg_path) {
Ok(raw) => raw,
Err(err) => {
self.error(format!("cannot read {}: {err}", msg_path.display()));
return;
}
};
let Some(message) = editor::strip_commit_message(&raw) else {
self.info("commit aborted");
return;
};
let committed = if paths.is_empty() {
self.review.vcs.commit(&message)
} else {
self.review.vcs.commit_paths(&message, paths)
};
match committed {
Ok(oid) => {
let subject = message.lines().next().unwrap_or_default().to_owned();
let oid7 = oid.get(..7).unwrap_or(&oid).to_owned();
self.queue_refresh();
if self.message.is_none() {
self.info(format!("committed {oid7} {subject}"));
}
}
Err(err) => self.error(err.to_string()),
}
}
pub(super) fn finish_amend(
&mut self,
msg_path: &Path,
use_index: bool,
outcome: Result<bool, String>,
) {
match outcome {
Err(err) => {
self.error(format!("editor failed: {err}"));
return;
}
Ok(false) => {
self.info("amend aborted");
return;
}
Ok(true) => {}
}
let raw = match std::fs::read_to_string(msg_path) {
Ok(raw) => raw,
Err(err) => {
self.error(format!("cannot read {}: {err}", msg_path.display()));
return;
}
};
let Some(message) = editor::strip_commit_message(&raw) else {
self.info("amend aborted");
return;
};
self.apply_amend(Some(&message), use_index);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LoadedConfig;
use crate::test_support::standard_fixture;
#[test]
fn committing_reports_the_ordinary_commit_message() {
let fixture = standard_fixture();
let mut app = App::new(fixture.review(), LoadedConfig::default());
let msg_path = app.review.repo_root.join("MSG");
std::fs::write(&msg_path, "a plain commit\n").expect("write commit message");
app.finish_commit(&msg_path, &[], Ok(true));
assert!(
app.message
.as_ref()
.is_some_and(|m| m.text.starts_with("committed ")),
"{:?}",
app.message
);
}
}