support commit amend (#89)

This commit is contained in:
Stephan Dilly 2020-06-13 01:20:53 +02:00
parent 63e449fca9
commit 5185f1c4d4
9 changed files with 220 additions and 12 deletions

View File

@ -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
View 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(())
}
}

View File

@ -53,6 +53,15 @@ impl CommitMessage {
}
}
}
///
pub fn combine(self) -> String {
if let Some(body) = self.body {
format!("{}{}", self.subject, body)
} else {
self.subject
}
}
}
///

View File

@ -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)]

View File

@ -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");

View File

@ -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(())
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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",