mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-23 03:32:30 +03:00
support commit amend (#89)
This commit is contained in:
parent
63e449fca9
commit
5185f1c4d4
@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Commit Amend (`ctrl+a`) when in commit popup ([#89](https://github.com/extrawurst/gitui/issues/89))
|
||||
|
||||
### Changed
|
||||
- file trees: `arrow-right` on expanded folder moves down into folder
|
||||
- better scrolling in diff ([#52](https://github.com/extrawurst/gitui/issues/52))
|
||||
|
121
asyncgit/src/sync/commit.rs
Normal file
121
asyncgit/src/sync/commit.rs
Normal file
@ -0,0 +1,121 @@
|
||||
use super::{utils::repo, CommitId};
|
||||
use crate::error::Result;
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
pub fn get_head(repo_path: &str) -> Result<CommitId> {
|
||||
scope_time!("get_head");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let head_id = repo.head()?.target().expect("head target error");
|
||||
|
||||
Ok(CommitId::new(head_id))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn amend(
|
||||
repo_path: &str,
|
||||
id: CommitId,
|
||||
msg: &str,
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("commit");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
|
||||
let new_id = commit.amend(
|
||||
Some("HEAD"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(msg),
|
||||
Some(&tree),
|
||||
)?;
|
||||
|
||||
Ok(CommitId::new(new_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, get_commit_details, get_commit_files, stage_add_file,
|
||||
tests::{repo_init, repo_init_empty},
|
||||
CommitId, LogWalker,
|
||||
};
|
||||
use commit::{amend, get_head};
|
||||
use git2::Repository;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
fn count_commits(repo: &Repository, max: usize) -> usize {
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo);
|
||||
walk.read(&mut items, max).unwrap();
|
||||
items.len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_amend() -> Result<()> {
|
||||
let file_path1 = Path::new("foo");
|
||||
let file_path2 = Path::new("foo2");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
File::create(&root.join(file_path1))?.write_all(b"test1")?;
|
||||
|
||||
stage_add_file(repo_path, file_path1)?;
|
||||
let id = commit(repo_path, "commit msg")?;
|
||||
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
File::create(&root.join(file_path2))?.write_all(b"test2")?;
|
||||
|
||||
stage_add_file(repo_path, file_path2)?;
|
||||
|
||||
let new_id = amend(repo_path, CommitId::new(id), "amended")?;
|
||||
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
let details = get_commit_details(repo_path, new_id)?;
|
||||
assert_eq!(details.message.unwrap().subject, "amended");
|
||||
|
||||
let files = get_commit_files(repo_path, new_id)?;
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
|
||||
let head = get_head(repo_path)?;
|
||||
|
||||
assert_eq!(head, new_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_head_empty() -> Result<()> {
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
assert_eq!(get_head(repo_path).is_ok(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_head() -> Result<()> {
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
assert_eq!(get_head(repo_path).is_ok(), true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -53,6 +53,15 @@ impl CommitMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn combine(self) -> String {
|
||||
if let Some(body) = self.body {
|
||||
format!("{}{}", self.subject, body)
|
||||
} else {
|
||||
self.subject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -1,6 +1,7 @@
|
||||
//! sync git api
|
||||
|
||||
mod branch;
|
||||
mod commit;
|
||||
mod commit_details;
|
||||
mod commit_files;
|
||||
mod commits_info;
|
||||
@ -16,6 +17,7 @@ mod tags;
|
||||
pub mod utils;
|
||||
|
||||
pub use branch::get_branch_name;
|
||||
pub use commit::{amend, get_head};
|
||||
pub use commit_details::{get_commit_details, CommitDetails};
|
||||
pub use commit_files::get_commit_files;
|
||||
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
|
||||
@ -28,8 +30,8 @@ pub use reset::{reset_stage, reset_workdir};
|
||||
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
|
||||
pub use tags::{get_tags, Tags};
|
||||
pub use utils::{
|
||||
commit, is_bare_repo, is_repo, stage_add_all, stage_add_file,
|
||||
stage_addremoved,
|
||||
commit, commit_new, is_bare_repo, is_repo, stage_add_all,
|
||||
stage_add_file, stage_addremoved,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! sync git api (various methods)
|
||||
|
||||
use super::CommitId;
|
||||
use crate::error::{Error, Result};
|
||||
use git2::{IndexAddOption, Oid, Repository, RepositoryOpenFlags};
|
||||
use scopetime::scope_time;
|
||||
@ -46,6 +47,11 @@ pub fn work_dir(repo: &Repository) -> &Path {
|
||||
repo.workdir().expect("unable to query workdir")
|
||||
}
|
||||
|
||||
/// ditto
|
||||
pub fn commit_new(repo_path: &str, msg: &str) -> Result<CommitId> {
|
||||
commit(repo_path, msg).map(CommitId::new)
|
||||
}
|
||||
|
||||
/// this does not run any git hooks
|
||||
pub fn commit(repo_path: &str, msg: &str) -> Result<Oid> {
|
||||
scope_time!("commit");
|
||||
|
@ -3,19 +3,24 @@ use super::{
|
||||
CommandBlocking, CommandInfo, Component, DrawableComponent,
|
||||
};
|
||||
use crate::{
|
||||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
ui::style::Theme,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use asyncgit::{sync, CWD};
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
use asyncgit::{
|
||||
sync::{self, CommitId},
|
||||
CWD,
|
||||
};
|
||||
use crossterm::event::Event;
|
||||
use strings::commands;
|
||||
use sync::HookResult;
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
pub struct CommitComponent {
|
||||
input: TextInputComponent,
|
||||
amend: Option<CommitId>,
|
||||
queue: Queue,
|
||||
}
|
||||
|
||||
@ -42,8 +47,15 @@ impl Component for CommitComponent {
|
||||
out.push(CommandInfo::new(
|
||||
commands::COMMIT_ENTER,
|
||||
self.can_commit(),
|
||||
self.is_visible(),
|
||||
self.is_visible() || force_all,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
commands::COMMIT_AMEND,
|
||||
self.can_amend(),
|
||||
self.is_visible() || force_all,
|
||||
));
|
||||
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
@ -54,11 +66,15 @@ impl Component for CommitComponent {
|
||||
}
|
||||
|
||||
if let Event::Key(e) = ev {
|
||||
match e.code {
|
||||
KeyCode::Enter if self.can_commit() => {
|
||||
match e {
|
||||
keys::ENTER if self.can_commit() => {
|
||||
self.commit()?;
|
||||
}
|
||||
|
||||
keys::COMMIT_AMEND if self.can_amend() => {
|
||||
self.amend()?;
|
||||
}
|
||||
|
||||
_ => (),
|
||||
};
|
||||
|
||||
@ -79,6 +95,10 @@ impl Component for CommitComponent {
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.amend = None;
|
||||
|
||||
self.input.clear();
|
||||
self.input.set_title(strings::COMMIT_TITLE.into());
|
||||
self.input.show()?;
|
||||
|
||||
Ok(())
|
||||
@ -90,9 +110,10 @@ impl CommitComponent {
|
||||
pub fn new(queue: Queue, theme: &Theme) -> Self {
|
||||
Self {
|
||||
queue,
|
||||
amend: None,
|
||||
input: TextInputComponent::new(
|
||||
theme,
|
||||
strings::COMMIT_TITLE,
|
||||
"",
|
||||
strings::COMMIT_MSG,
|
||||
),
|
||||
}
|
||||
@ -113,7 +134,12 @@ impl CommitComponent {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = sync::commit(CWD, &msg) {
|
||||
let res = if let Some(amend) = self.amend {
|
||||
sync::amend(CWD, amend, &msg)
|
||||
} else {
|
||||
sync::commit_new(CWD, &msg)
|
||||
};
|
||||
if let Err(e) = res {
|
||||
log::error!("commit error: {}", &e);
|
||||
self.queue.borrow_mut().push_back(
|
||||
InternalEvent::ShowErrorMsg(format!(
|
||||
@ -134,7 +160,6 @@ impl CommitComponent {
|
||||
);
|
||||
}
|
||||
|
||||
self.input.clear();
|
||||
self.hide();
|
||||
|
||||
self.queue
|
||||
@ -147,4 +172,25 @@ impl CommitComponent {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use crate::{
|
||||
ui::style::Theme,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use std::borrow::Cow;
|
||||
use strings::commands;
|
||||
use tui::{
|
||||
@ -52,6 +52,16 @@ impl TextInputComponent {
|
||||
pub const fn get_text(&self) -> &String {
|
||||
&self.msg
|
||||
}
|
||||
|
||||
///
|
||||
pub fn set_text(&mut self, msg: String) {
|
||||
self.msg = msg;
|
||||
}
|
||||
|
||||
///
|
||||
pub fn set_title(&mut self, t: String) {
|
||||
self.title = t;
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for TextInputComponent {
|
||||
@ -110,12 +120,14 @@ impl Component for TextInputComponent {
|
||||
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||
if self.visible {
|
||||
if let Event::Key(e) = ev {
|
||||
let is_ctrl =
|
||||
e.modifiers.contains(KeyModifiers::CONTROL);
|
||||
match e.code {
|
||||
KeyCode::Esc => {
|
||||
self.hide();
|
||||
return Ok(true);
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
KeyCode::Char(c) if !is_ctrl => {
|
||||
self.msg.push(c);
|
||||
return Ok(true);
|
||||
}
|
||||
|
@ -61,3 +61,5 @@ pub const STASH_DROP: KeyEvent =
|
||||
with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT);
|
||||
pub const CMD_BAR_TOGGLE: KeyEvent = no_mod(KeyCode::Char('.'));
|
||||
pub const LOG_COMMIT_DETAILS: KeyEvent = no_mod(KeyCode::Enter);
|
||||
pub const COMMIT_AMEND: KeyEvent =
|
||||
with_mod(KeyCode::Char('a'), KeyModifiers::CONTROL);
|
||||
|
@ -12,6 +12,7 @@ pub static CMD_SPLITTER: &str = " ";
|
||||
|
||||
pub static MSG_TITLE_ERROR: &str = "Error";
|
||||
pub static COMMIT_TITLE: &str = "Commit";
|
||||
pub static COMMIT_TITLE_AMEND: &str = "Commit (Amend)";
|
||||
pub static COMMIT_MSG: &str = "type commit message..";
|
||||
pub static STASH_POPUP_TITLE: &str = "Stash";
|
||||
pub static STASH_POPUP_MSG: &str = "type name (optional)";
|
||||
@ -148,6 +149,12 @@ pub mod commands {
|
||||
CMD_GROUP_COMMIT,
|
||||
);
|
||||
///
|
||||
pub static COMMIT_AMEND: CommandText = CommandText::new(
|
||||
"Amend [^a]",
|
||||
"amend last commit",
|
||||
CMD_GROUP_COMMIT,
|
||||
);
|
||||
///
|
||||
pub static STAGE_ITEM: CommandText = CommandText::new(
|
||||
"Stage Item [enter]",
|
||||
"stage currently selected file or entire path",
|
||||
|
Loading…
Reference in New Issue
Block a user