Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support commit amend #122

Merged
merged 1 commit into from
Jun 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
121 changes: 121 additions & 0 deletions asyncgit/src/sync/commit.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
9 changes: 9 additions & 0 deletions asyncgit/src/sync/commit_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ impl CommitMessage {
}
}
}

///
pub fn combine(self) -> String {
if let Some(body) = self.body {
format!("{}{}", self.subject, body)
} else {
self.subject
}
}
}

///
Expand Down
6 changes: 4 additions & 2 deletions asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! sync git api

mod branch;
mod commit;
mod commit_details;
mod commit_files;
mod commits_info;
Expand All @@ -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};
Expand All @@ -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)]
Expand Down
6 changes: 6 additions & 0 deletions asyncgit/src/sync/utils.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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");
Expand Down
62 changes: 54 additions & 8 deletions src/components/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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)
}

Expand All @@ -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()?;
}

_ => (),
};

Expand All @@ -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(())
Expand All @@ -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,
),
}
Expand All @@ -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!(
Expand All @@ -134,7 +160,6 @@ impl CommitComponent {
);
}

self.input.clear();
self.hide();

self.queue
Expand All @@ -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(())
}
}
16 changes: 14 additions & 2 deletions src/components/textinput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
7 changes: 7 additions & 0 deletions src/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down Expand Up @@ -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",
Expand Down