Skip to content

Commit 676ecdb

Browse files
author
Stephan Dilly
committed
support commit amend (#89)
1 parent 63e449f commit 676ecdb

File tree

9 files changed

+220
-12
lines changed

9 files changed

+220
-12
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
- Commit Amend (`ctrl+a`) when in commit popup ([#89](https://github.com/extrawurst/gitui/issues/89))
11+
912
### Changed
1013
- file trees: `arrow-right` on expanded folder moves down into folder
1114
- better scrolling in diff ([#52](https://github.com/extrawurst/gitui/issues/52))

Diff for: asyncgit/src/sync/commit.rs

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use super::{utils::repo, CommitId};
2+
use crate::error::Result;
3+
use scopetime::scope_time;
4+
5+
///
6+
pub fn get_head(repo_path: &str) -> Result<CommitId> {
7+
scope_time!("get_head");
8+
9+
let repo = repo(repo_path)?;
10+
11+
let head_id = repo.head()?.target().expect("head target error");
12+
13+
Ok(CommitId::new(head_id))
14+
}
15+
16+
///
17+
pub fn amend(
18+
repo_path: &str,
19+
id: CommitId,
20+
msg: &str,
21+
) -> Result<CommitId> {
22+
scope_time!("commit");
23+
24+
let repo = repo(repo_path)?;
25+
let commit = repo.find_commit(id.into())?;
26+
27+
let mut index = repo.index()?;
28+
let tree_id = index.write_tree()?;
29+
let tree = repo.find_tree(tree_id)?;
30+
31+
let new_id = commit.amend(
32+
Some("HEAD"),
33+
None,
34+
None,
35+
None,
36+
Some(msg),
37+
Some(&tree),
38+
)?;
39+
40+
Ok(CommitId::new(new_id))
41+
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
46+
use crate::error::Result;
47+
use crate::sync::{
48+
commit, get_commit_details, get_commit_files, stage_add_file,
49+
tests::{repo_init, repo_init_empty},
50+
CommitId, LogWalker,
51+
};
52+
use commit::{amend, get_head};
53+
use git2::Repository;
54+
use std::{fs::File, io::Write, path::Path};
55+
56+
fn count_commits(repo: &Repository, max: usize) -> usize {
57+
let mut items = Vec::new();
58+
let mut walk = LogWalker::new(&repo);
59+
walk.read(&mut items, max).unwrap();
60+
items.len()
61+
}
62+
63+
#[test]
64+
fn test_amend() -> Result<()> {
65+
let file_path1 = Path::new("foo");
66+
let file_path2 = Path::new("foo2");
67+
let (_td, repo) = repo_init_empty()?;
68+
let root = repo.path().parent().unwrap();
69+
let repo_path = root.as_os_str().to_str().unwrap();
70+
71+
File::create(&root.join(file_path1))?.write_all(b"test1")?;
72+
73+
stage_add_file(repo_path, file_path1)?;
74+
let id = commit(repo_path, "commit msg")?;
75+
76+
assert_eq!(count_commits(&repo, 10), 1);
77+
78+
File::create(&root.join(file_path2))?.write_all(b"test2")?;
79+
80+
stage_add_file(repo_path, file_path2)?;
81+
82+
let new_id = amend(repo_path, CommitId::new(id), "amended")?;
83+
84+
assert_eq!(count_commits(&repo, 10), 1);
85+
86+
let details = get_commit_details(repo_path, new_id)?;
87+
assert_eq!(details.message.unwrap().subject, "amended");
88+
89+
let files = get_commit_files(repo_path, new_id)?;
90+
91+
assert_eq!(files.len(), 2);
92+
93+
let head = get_head(repo_path)?;
94+
95+
assert_eq!(head, new_id);
96+
97+
Ok(())
98+
}
99+
100+
#[test]
101+
fn test_head_empty() -> Result<()> {
102+
let (_td, repo) = repo_init_empty()?;
103+
let root = repo.path().parent().unwrap();
104+
let repo_path = root.as_os_str().to_str().unwrap();
105+
106+
assert_eq!(get_head(repo_path).is_ok(), false);
107+
108+
Ok(())
109+
}
110+
111+
#[test]
112+
fn test_head() -> Result<()> {
113+
let (_td, repo) = repo_init()?;
114+
let root = repo.path().parent().unwrap();
115+
let repo_path = root.as_os_str().to_str().unwrap();
116+
117+
assert_eq!(get_head(repo_path).is_ok(), true);
118+
119+
Ok(())
120+
}
121+
}

Diff for: asyncgit/src/sync/commit_details.rs

+9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ impl CommitMessage {
5353
}
5454
}
5555
}
56+
57+
///
58+
pub fn combine(self) -> String {
59+
if let Some(body) = self.body {
60+
format!("{}{}", self.subject, body)
61+
} else {
62+
self.subject
63+
}
64+
}
5665
}
5766

5867
///

Diff for: asyncgit/src/sync/mod.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! sync git api
22
33
mod branch;
4+
mod commit;
45
mod commit_details;
56
mod commit_files;
67
mod commits_info;
@@ -16,6 +17,7 @@ mod tags;
1617
pub mod utils;
1718

1819
pub use branch::get_branch_name;
20+
pub use commit::{amend, get_head};
1921
pub use commit_details::{get_commit_details, CommitDetails};
2022
pub use commit_files::get_commit_files;
2123
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
@@ -28,8 +30,8 @@ pub use reset::{reset_stage, reset_workdir};
2830
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
2931
pub use tags::{get_tags, Tags};
3032
pub use utils::{
31-
commit, is_bare_repo, is_repo, stage_add_all, stage_add_file,
32-
stage_addremoved,
33+
commit, commit_new, is_bare_repo, is_repo, stage_add_all,
34+
stage_add_file, stage_addremoved,
3335
};
3436

3537
#[cfg(test)]

Diff for: asyncgit/src/sync/utils.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! sync git api (various methods)
22
3+
use super::CommitId;
34
use crate::error::{Error, Result};
45
use git2::{IndexAddOption, Oid, Repository, RepositoryOpenFlags};
56
use scopetime::scope_time;
@@ -46,6 +47,11 @@ pub fn work_dir(repo: &Repository) -> &Path {
4647
repo.workdir().expect("unable to query workdir")
4748
}
4849

50+
/// ditto
51+
pub fn commit_new(repo_path: &str, msg: &str) -> Result<CommitId> {
52+
commit(repo_path, msg).map(CommitId::new)
53+
}
54+
4955
/// this does not run any git hooks
5056
pub fn commit(repo_path: &str, msg: &str) -> Result<Oid> {
5157
scope_time!("commit");

Diff for: src/components/commit.rs

+54-8
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ use super::{
33
CommandBlocking, CommandInfo, Component, DrawableComponent,
44
};
55
use crate::{
6+
keys,
67
queue::{InternalEvent, NeedsUpdate, Queue},
78
strings,
89
ui::style::Theme,
910
};
1011
use anyhow::Result;
11-
use asyncgit::{sync, CWD};
12-
use crossterm::event::{Event, KeyCode};
12+
use asyncgit::{
13+
sync::{self, CommitId},
14+
CWD,
15+
};
16+
use crossterm::event::Event;
1317
use strings::commands;
1418
use sync::HookResult;
1519
use tui::{backend::Backend, layout::Rect, Frame};
1620

1721
pub struct CommitComponent {
1822
input: TextInputComponent,
23+
amend: Option<CommitId>,
1924
queue: Queue,
2025
}
2126

@@ -42,8 +47,15 @@ impl Component for CommitComponent {
4247
out.push(CommandInfo::new(
4348
commands::COMMIT_ENTER,
4449
self.can_commit(),
45-
self.is_visible(),
50+
self.is_visible() || force_all,
4651
));
52+
53+
out.push(CommandInfo::new(
54+
commands::COMMIT_AMEND,
55+
self.can_amend(),
56+
self.is_visible() || force_all,
57+
));
58+
4759
visibility_blocking(self)
4860
}
4961

@@ -54,11 +66,15 @@ impl Component for CommitComponent {
5466
}
5567

5668
if let Event::Key(e) = ev {
57-
match e.code {
58-
KeyCode::Enter if self.can_commit() => {
69+
match e {
70+
keys::ENTER if self.can_commit() => {
5971
self.commit()?;
6072
}
6173

74+
keys::COMMIT_AMEND if self.can_amend() => {
75+
self.amend()?;
76+
}
77+
6278
_ => (),
6379
};
6480

@@ -79,6 +95,10 @@ impl Component for CommitComponent {
7995
}
8096

8197
fn show(&mut self) -> Result<()> {
98+
self.amend = None;
99+
100+
self.input.clear();
101+
self.input.set_title(strings::COMMIT_TITLE.into());
82102
self.input.show()?;
83103

84104
Ok(())
@@ -90,9 +110,10 @@ impl CommitComponent {
90110
pub fn new(queue: Queue, theme: &Theme) -> Self {
91111
Self {
92112
queue,
113+
amend: None,
93114
input: TextInputComponent::new(
94115
theme,
95-
strings::COMMIT_TITLE,
116+
"",
96117
strings::COMMIT_MSG,
97118
),
98119
}
@@ -113,7 +134,12 @@ impl CommitComponent {
113134
return Ok(());
114135
}
115136

116-
if let Err(e) = sync::commit(CWD, &msg) {
137+
let res = if let Some(amend) = self.amend {
138+
sync::amend(CWD, amend, &msg)
139+
} else {
140+
sync::commit_new(CWD, &msg)
141+
};
142+
if let Err(e) = res {
117143
log::error!("commit error: {}", &e);
118144
self.queue.borrow_mut().push_back(
119145
InternalEvent::ShowErrorMsg(format!(
@@ -134,7 +160,6 @@ impl CommitComponent {
134160
);
135161
}
136162

137-
self.input.clear();
138163
self.hide();
139164

140165
self.queue
@@ -147,4 +172,25 @@ impl CommitComponent {
147172
fn can_commit(&self) -> bool {
148173
!self.input.get_text().is_empty()
149174
}
175+
176+
fn can_amend(&self) -> bool {
177+
self.amend.is_none()
178+
&& sync::get_head(CWD).is_ok()
179+
&& self.input.get_text().is_empty()
180+
}
181+
182+
fn amend(&mut self) -> Result<()> {
183+
let id = sync::get_head(CWD)?;
184+
self.amend = Some(id);
185+
186+
let details = sync::get_commit_details(CWD, id)?;
187+
188+
self.input.set_title(strings::COMMIT_TITLE_AMEND.into());
189+
190+
if let Some(msg) = details.message {
191+
self.input.set_text(msg.combine());
192+
}
193+
194+
Ok(())
195+
}
150196
}

Diff for: src/components/textinput.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
ui::style::Theme,
88
};
99
use anyhow::Result;
10-
use crossterm::event::{Event, KeyCode};
10+
use crossterm::event::{Event, KeyCode, KeyModifiers};
1111
use std::borrow::Cow;
1212
use strings::commands;
1313
use tui::{
@@ -52,6 +52,16 @@ impl TextInputComponent {
5252
pub const fn get_text(&self) -> &String {
5353
&self.msg
5454
}
55+
56+
///
57+
pub fn set_text(&mut self, msg: String) {
58+
self.msg = msg;
59+
}
60+
61+
///
62+
pub fn set_title(&mut self, t: String) {
63+
self.title = t;
64+
}
5565
}
5666

5767
impl DrawableComponent for TextInputComponent {
@@ -110,12 +120,14 @@ impl Component for TextInputComponent {
110120
fn event(&mut self, ev: Event) -> Result<bool> {
111121
if self.visible {
112122
if let Event::Key(e) = ev {
123+
let is_ctrl =
124+
e.modifiers.contains(KeyModifiers::CONTROL);
113125
match e.code {
114126
KeyCode::Esc => {
115127
self.hide();
116128
return Ok(true);
117129
}
118-
KeyCode::Char(c) => {
130+
KeyCode::Char(c) if !is_ctrl => {
119131
self.msg.push(c);
120132
return Ok(true);
121133
}

Diff for: src/keys.rs

+2
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ pub const STASH_DROP: KeyEvent =
6161
with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT);
6262
pub const CMD_BAR_TOGGLE: KeyEvent = no_mod(KeyCode::Char('.'));
6363
pub const LOG_COMMIT_DETAILS: KeyEvent = no_mod(KeyCode::Enter);
64+
pub const COMMIT_AMEND: KeyEvent =
65+
with_mod(KeyCode::Char('a'), KeyModifiers::CONTROL);

Diff for: src/strings.rs

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub static CMD_SPLITTER: &str = " ";
1212

1313
pub static MSG_TITLE_ERROR: &str = "Error";
1414
pub static COMMIT_TITLE: &str = "Commit";
15+
pub static COMMIT_TITLE_AMEND: &str = "Commit (Amend)";
1516
pub static COMMIT_MSG: &str = "type commit message..";
1617
pub static STASH_POPUP_TITLE: &str = "Stash";
1718
pub static STASH_POPUP_MSG: &str = "type name (optional)";
@@ -148,6 +149,12 @@ pub mod commands {
148149
CMD_GROUP_COMMIT,
149150
);
150151
///
152+
pub static COMMIT_AMEND: CommandText = CommandText::new(
153+
"Amend [^a]",
154+
"amend last commit",
155+
CMD_GROUP_COMMIT,
156+
);
157+
///
151158
pub static STAGE_ITEM: CommandText = CommandText::new(
152159
"Stage Item [enter]",
153160
"stage currently selected file or entire path",

0 commit comments

Comments
 (0)