mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
272 lines
6.7 KiB
Rust
272 lines
6.7 KiB
Rust
use super::{
|
|
textinput::TextInputComponent, visibility_blocking,
|
|
CommandBlocking, CommandInfo, Component, DrawableComponent,
|
|
};
|
|
use crate::{
|
|
get_app_config_path, keys,
|
|
queue::{InternalEvent, NeedsUpdate, Queue},
|
|
strings,
|
|
strings::{commands, COMMIT_EDITOR_MSG},
|
|
ui::style::SharedTheme,
|
|
};
|
|
use anyhow::{anyhow, Result};
|
|
use asyncgit::{
|
|
sync::{self, CommitId, HookResult},
|
|
CWD,
|
|
};
|
|
use crossterm::{
|
|
event::Event,
|
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
|
ExecutableCommand,
|
|
};
|
|
use scopeguard::defer;
|
|
use std::{
|
|
env,
|
|
fs::File,
|
|
io::{self, Read, Write},
|
|
path::PathBuf,
|
|
process::Command,
|
|
};
|
|
use tui::{backend::Backend, layout::Rect, Frame};
|
|
|
|
pub struct CommitComponent {
|
|
input: TextInputComponent,
|
|
amend: Option<CommitId>,
|
|
queue: Queue,
|
|
}
|
|
|
|
impl DrawableComponent for CommitComponent {
|
|
fn draw<B: Backend>(
|
|
&self,
|
|
f: &mut Frame<B>,
|
|
rect: Rect,
|
|
) -> Result<()> {
|
|
self.input.draw(f, rect)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Component for CommitComponent {
|
|
fn commands(
|
|
&self,
|
|
out: &mut Vec<CommandInfo>,
|
|
force_all: bool,
|
|
) -> CommandBlocking {
|
|
self.input.commands(out, force_all);
|
|
|
|
if self.is_visible() || force_all {
|
|
out.push(CommandInfo::new(
|
|
commands::COMMIT_ENTER,
|
|
self.can_commit(),
|
|
true,
|
|
));
|
|
|
|
out.push(CommandInfo::new(
|
|
commands::COMMIT_AMEND,
|
|
self.can_amend(),
|
|
true,
|
|
));
|
|
}
|
|
|
|
visibility_blocking(self)
|
|
}
|
|
|
|
fn event(&mut self, ev: Event) -> Result<bool> {
|
|
if self.is_visible() {
|
|
if self.input.event(ev)? {
|
|
return Ok(true);
|
|
}
|
|
|
|
if let Event::Key(e) = ev {
|
|
match e {
|
|
keys::ENTER if self.can_commit() => {
|
|
self.commit()?;
|
|
}
|
|
|
|
keys::COMMIT_AMEND if self.can_amend() => {
|
|
self.amend()?;
|
|
}
|
|
|
|
_ => (),
|
|
};
|
|
|
|
// stop key event propagation
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
fn is_visible(&self) -> bool {
|
|
self.input.is_visible()
|
|
}
|
|
|
|
fn hide(&mut self) {
|
|
self.input.hide()
|
|
}
|
|
|
|
fn show(&mut self) -> Result<()> {
|
|
self.amend = None;
|
|
|
|
self.input.clear();
|
|
self.input.set_title(strings::COMMIT_TITLE.into());
|
|
self.input.show()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl CommitComponent {
|
|
///
|
|
pub fn new(queue: Queue, theme: SharedTheme) -> Self {
|
|
Self {
|
|
queue,
|
|
amend: None,
|
|
input: TextInputComponent::new(
|
|
theme,
|
|
"",
|
|
strings::COMMIT_MSG,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub fn show_editor(&mut self) -> Result<()> {
|
|
const COMMIT_MSG_FILE_NAME: &str = "COMMITMSG_EDITOR";
|
|
let mut config_path: PathBuf = get_app_config_path()?;
|
|
config_path.push(COMMIT_MSG_FILE_NAME);
|
|
|
|
let mut file = File::create(&config_path)?;
|
|
file.write_all(COMMIT_EDITOR_MSG.as_bytes())?;
|
|
drop(file);
|
|
|
|
let mut editor = env::var("GIT_EDTIOR")
|
|
.ok()
|
|
.or_else(|| env::var("VISUAL").ok())
|
|
.or_else(|| env::var("EDITOR").ok())
|
|
.unwrap_or_else(|| String::from("vi"));
|
|
editor
|
|
.push_str(&format!(" {}", config_path.to_string_lossy()));
|
|
|
|
let mut editor = editor.split_whitespace();
|
|
|
|
let command = editor.next().ok_or_else(|| {
|
|
anyhow!("unable to read editor command")
|
|
})?;
|
|
|
|
io::stdout().execute(LeaveAlternateScreen)?;
|
|
|
|
defer! {
|
|
io::stdout().execute(EnterAlternateScreen).expect("failed to reset terminal");
|
|
}
|
|
|
|
Command::new(command)
|
|
.args(editor)
|
|
.status()
|
|
.map_err(|e| anyhow!("\"{}\": {}", command, e))?;
|
|
|
|
let mut message = String::new();
|
|
|
|
let mut file = File::open(&config_path)?;
|
|
file.read_to_string(&mut message)?;
|
|
drop(file);
|
|
std::fs::remove_file(&config_path)?;
|
|
|
|
let message: String = message
|
|
.lines()
|
|
.flat_map(|l| {
|
|
if l.starts_with('#') {
|
|
vec![]
|
|
} else {
|
|
vec![l, "\n"]
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
if !message.chars().all(char::is_whitespace) {
|
|
return self.commit_msg(message);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn commit(&mut self) -> Result<()> {
|
|
self.commit_msg(self.input.get_text().clone())
|
|
}
|
|
|
|
fn commit_msg(&mut self, msg: String) -> Result<()> {
|
|
let mut msg = msg;
|
|
if let HookResult::NotOk(e) =
|
|
sync::hooks_commit_msg(CWD, &mut msg)?
|
|
{
|
|
log::error!("commit-msg hook error: {}", e);
|
|
self.queue.borrow_mut().push_back(
|
|
InternalEvent::ShowErrorMsg(format!(
|
|
"commit-msg hook error:\n{}",
|
|
e
|
|
)),
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let res = if let Some(amend) = self.amend {
|
|
sync::amend(CWD, amend, &msg)
|
|
} else {
|
|
sync::commit(CWD, &msg)
|
|
};
|
|
if let Err(e) = res {
|
|
log::error!("commit error: {}", &e);
|
|
self.queue.borrow_mut().push_back(
|
|
InternalEvent::ShowErrorMsg(format!(
|
|
"commit failed:\n{}",
|
|
&e
|
|
)),
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
if let HookResult::NotOk(e) = sync::hooks_post_commit(CWD)? {
|
|
log::error!("post-commit hook error: {}", e);
|
|
self.queue.borrow_mut().push_back(
|
|
InternalEvent::ShowErrorMsg(format!(
|
|
"post-commit hook error:\n{}",
|
|
e
|
|
)),
|
|
);
|
|
}
|
|
|
|
self.hide();
|
|
|
|
self.queue
|
|
.borrow_mut()
|
|
.push_back(InternalEvent::Update(NeedsUpdate::ALL));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn can_commit(&self) -> bool {
|
|
!self.input.get_text().is_empty()
|
|
}
|
|
|
|
fn can_amend(&self) -> bool {
|
|
self.amend.is_none()
|
|
&& sync::get_head(CWD).is_ok()
|
|
&& self.input.get_text().is_empty()
|
|
}
|
|
|
|
fn amend(&mut self) -> Result<()> {
|
|
let id = sync::get_head(CWD)?;
|
|
self.amend = Some(id);
|
|
|
|
let details = sync::get_commit_details(CWD, id)?;
|
|
|
|
self.input.set_title(strings::COMMIT_TITLE_AMEND.into());
|
|
|
|
if let Some(msg) = details.message {
|
|
self.input.set_text(msg.combine());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|