Add folder actions unstage/stage/reset

This commit is contained in:
Stephan Dilly 2020-04-28 11:49:14 +02:00
parent d22571c919
commit 1ca04496cb
11 changed files with 273 additions and 131 deletions

View File

@ -238,7 +238,7 @@ fn new_file_content(path: &Path) -> String {
mod tests {
use super::get_diff;
use crate::sync::{
stage_add,
stage_add_file,
status::{get_status, StatusType},
tests::{repo_init, repo_init_empty},
};
@ -288,7 +288,7 @@ mod tests {
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
let diff = get_diff(
repo_path,
@ -347,7 +347,7 @@ mod tests {
assert_eq!(res.len(), 1);
assert_eq!(res[0].path, "bar.txt");
let res = stage_add(repo_path, Path::new("bar.txt"));
let res = stage_add_file(repo_path, Path::new("bar.txt"));
assert_eq!(res, true);
assert_eq!(get_status(repo_path, StatusType::Stage).len(), 1);
assert_eq!(

View File

@ -9,8 +9,12 @@ pub mod utils;
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
pub use hunks::{stage_hunk, unstage_hunk};
pub use reset::{reset_stage, reset_workdir};
pub use utils::{commit, stage_add, stage_addremoved};
pub use reset::{
reset_stage, reset_workdir_file, reset_workdir_folder,
};
pub use utils::{
commit, stage_add_all, stage_add_file, stage_addremoved,
};
#[cfg(test)]
mod tests {

View File

@ -26,14 +26,14 @@ pub fn reset_stage(repo_path: &str, path: &Path) -> bool {
}
///
pub fn reset_workdir(repo_path: &str, path: &Path) -> bool {
scope_time!("reset_workdir");
pub fn reset_workdir_file(repo_path: &str, path: &str) -> bool {
scope_time!("reset_workdir_file");
let repo = repo(repo_path);
// Note: early out for removing untracked files, due to bug in checkout_head code:
// see https://github.com/libgit2/libgit2/issues/5089
if let Ok(status) = repo.status_file(&path) {
if let Ok(status) = repo.status_file(Path::new(path)) {
let removed_file_wd = if status == Status::WT_NEW
|| (status == Status::WT_MODIFIED | Status::INDEX_NEW)
{
@ -51,7 +51,7 @@ pub fn reset_workdir(repo_path: &str, path: &Path) -> bool {
.update_index(true) // windows: needs this to be true WTF?!
.allow_conflicts(true)
.force()
.path(&path);
.path(path);
repo.checkout_index(None, Some(&mut checkout_opts)).is_ok()
} else {
@ -59,17 +59,36 @@ pub fn reset_workdir(repo_path: &str, path: &Path) -> bool {
}
}
///
pub fn reset_workdir_folder(repo_path: &str, path: &str) -> bool {
scope_time!("reset_workdir_folder");
let repo = repo(repo_path);
let mut checkout_opts = CheckoutBuilder::new();
checkout_opts
.update_index(true) // windows: needs this to be true WTF?!
.allow_conflicts(true)
.remove_untracked(true)
.force()
.path(path);
repo.checkout_index(None, Some(&mut checkout_opts)).is_ok()
}
#[cfg(test)]
mod tests {
use super::{reset_stage, reset_workdir};
use super::{
reset_stage, reset_workdir_file, reset_workdir_folder,
};
use crate::sync::{
status::{get_status, StatusType},
tests::{debug_cmd_print, repo_init, repo_init_empty},
utils::stage_add,
utils::{commit, stage_add_all, stage_add_file},
};
use std::{
fs::{self, File},
io::Write,
io::{Error, Write},
path::Path,
};
@ -119,7 +138,7 @@ mod tests {
debug_cmd_print(repo_path, "git status");
stage_add(repo_path, Path::new("bar.txt"));
stage_add_file(repo_path, Path::new("bar.txt"));
debug_cmd_print(repo_path, "git status");
@ -139,7 +158,7 @@ mod tests {
1
);
let res = reset_workdir(repo_path, Path::new("bar.txt"));
let res = reset_workdir_file(repo_path, "bar.txt");
assert_eq!(res, true);
debug_cmd_print(repo_path, "git status");
@ -172,7 +191,7 @@ mod tests {
1
);
let res = reset_workdir(repo_path, Path::new("foo/bar.txt"));
let res = reset_workdir_file(repo_path, "foo/bar.txt");
assert_eq!(res, true);
debug_cmd_print(repo_path, "git status");
@ -183,6 +202,56 @@ mod tests {
);
}
#[test]
fn test_reset_folder() -> Result<(), Error> {
let (_td, repo) = repo_init();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
{
fs::create_dir(&root.join("foo"))?;
File::create(&root.join("foo/file1.txt"))?
.write_all(b"file1")?;
File::create(&root.join("foo/file2.txt"))?
.write_all(b"file1")?;
File::create(&root.join("file3.txt"))?
.write_all(b"file3")?;
}
assert!(stage_add_all(repo_path, "*"));
commit(repo_path, "msg");
{
File::create(&root.join("foo/file1.txt"))?
.write_all(b"file1\nadded line")?;
fs::remove_file(&root.join("foo/file2.txt"))?;
File::create(&root.join("foo/file4.txt"))?
.write_all(b"file4")?;
File::create(&root.join("foo/file5.txt"))?
.write_all(b"file5")?;
File::create(&root.join("file3.txt"))?
.write_all(b"file3\nadded line")?;
}
stage_add_file(repo_path, Path::new("foo/file5.txt"));
assert_eq!(
get_status(repo_path, StatusType::WorkingDir).len(),
4
);
assert_eq!(get_status(repo_path, StatusType::Stage).len(), 1);
assert!(reset_workdir_folder(repo_path, "foo"));
assert_eq!(
get_status(repo_path, StatusType::WorkingDir).len(),
1
);
assert_eq!(get_status(repo_path, StatusType::Stage).len(), 1);
Ok(())
}
#[test]
fn test_reset_untracked_in_subdir_and_index() {
let (_td, repo) = repo_init();
@ -219,7 +288,7 @@ mod tests {
1
);
let res = reset_workdir(repo_path, Path::new(file));
let res = reset_workdir_file(repo_path, file);
assert_eq!(res, true);
debug_cmd_print(repo_path, "git status");
@ -243,7 +312,7 @@ mod tests {
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
assert_eq!(reset_stage(repo_path, file_path), true);
}

View File

@ -1,6 +1,6 @@
//! sync git api (various methods)
use git2::{Repository, RepositoryOpenFlags};
use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
use scopetime::scope_time;
use std::path::Path;
@ -63,8 +63,8 @@ pub fn commit(repo_path: &str, msg: &str) {
}
/// add a file diff from workingdir to stage (will not add removed files see `stage_addremoved`)
pub fn stage_add(repo_path: &str, path: &Path) -> bool {
scope_time!("stage_add");
pub fn stage_add_file(repo_path: &str, path: &Path) -> bool {
scope_time!("stage_add_file");
let repo = repo(repo_path);
@ -78,6 +78,25 @@ pub fn stage_add(repo_path: &str, path: &Path) -> bool {
false
}
/// like `stage_add_file` but uses a pattern to match/glob multiple files/folders
pub fn stage_add_all(repo_path: &str, pattern: &str) -> bool {
scope_time!("stage_add_all");
let repo = repo(repo_path);
let mut index = repo.index().unwrap();
if index
.add_all(vec![pattern], IndexAddOption::DEFAULT, None)
.is_ok()
{
index.write().unwrap();
return true;
}
false
}
/// stage a removed file
pub fn stage_addremoved(repo_path: &str, path: &Path) -> bool {
scope_time!("stage_addremoved");
@ -98,13 +117,12 @@ pub fn stage_addremoved(repo_path: &str, path: &Path) -> bool {
mod tests {
use super::*;
use crate::sync::{
stage_add,
status::{get_status, StatusType},
tests::{repo_init, repo_init_empty},
};
use std::{
fs::{remove_file, File},
io::Write,
fs::{self, remove_file, File},
io::{Error, Write},
path::Path,
};
@ -126,7 +144,7 @@ mod tests {
assert_eq!(status_count(StatusType::WorkingDir), 1);
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
assert_eq!(status_count(StatusType::WorkingDir), 0);
assert_eq!(status_count(StatusType::Stage), 1);
@ -149,7 +167,7 @@ mod tests {
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
commit(repo_path, "commit msg");
}
@ -161,7 +179,7 @@ mod tests {
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(stage_add(repo_path, file_path), false);
assert_eq!(stage_add_file(repo_path, file_path), false);
}
#[test]
@ -187,12 +205,40 @@ mod tests {
assert_eq!(status_count(StatusType::WorkingDir), 2);
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
assert_eq!(status_count(StatusType::WorkingDir), 1);
assert_eq!(status_count(StatusType::Stage), 1);
}
#[test]
fn test_staging_folder() -> Result<(), Error> {
let (_td, repo) = repo_init();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let status_count = |s: StatusType| -> usize {
get_status(repo_path, s).len()
};
fs::create_dir_all(&root.join("a/d"))?;
File::create(&root.join(Path::new("a/d/f1.txt")))?
.write_all(b"foo")?;
File::create(&root.join(Path::new("a/d/f2.txt")))?
.write_all(b"foo")?;
File::create(&root.join(Path::new("a/f3.txt")))?
.write_all(b"foo")?;
assert_eq!(status_count(StatusType::WorkingDir), 3);
assert_eq!(stage_add_all(repo_path, "a/d"), true);
assert_eq!(status_count(StatusType::WorkingDir), 1);
assert_eq!(status_count(StatusType::Stage), 2);
Ok(())
}
#[test]
fn test_staging_deleted_file() {
let file_path = Path::new("file1.txt");
@ -211,7 +257,7 @@ mod tests {
.write_all(b"test file1 content")
.unwrap();
assert_eq!(stage_add(repo_path, file_path), true);
assert_eq!(stage_add_file(repo_path, file_path), true);
commit(repo_path, "commit msg");

View File

@ -17,7 +17,7 @@ use crossbeam_channel::Sender;
use crossterm::event::Event;
use itertools::Itertools;
use log::trace;
use std::{borrow::Cow, path::Path};
use std::borrow::Cow;
use strings::commands;
use tui::{
backend::Backend,
@ -299,7 +299,7 @@ impl App {
loop {
let front = self.queue.borrow_mut().pop_front();
if let Some(e) = front {
flags.insert(self.process_internal_event(&e));
flags.insert(self.process_internal_event(e));
} else {
break;
}
@ -311,35 +311,45 @@ impl App {
fn process_internal_event(
&mut self,
ev: &InternalEvent,
ev: InternalEvent,
) -> NeedsUpdate {
let mut flags = NeedsUpdate::empty();
match ev {
InternalEvent::ResetFile(p) => {
if sync::reset_workdir(CWD, Path::new(p.as_str())) {
InternalEvent::ResetItem(reset_item) => {
if reset_item.is_folder {
if sync::reset_workdir_folder(
CWD,
reset_item.path.as_str(),
) {
flags.insert(NeedsUpdate::ALL);
}
} else if sync::reset_workdir_file(
CWD,
reset_item.path.as_str(),
) {
flags.insert(NeedsUpdate::ALL);
}
}
InternalEvent::ConfirmResetFile(p) => {
self.reset.open_for_path(p);
InternalEvent::ConfirmResetItem(reset_item) => {
self.reset.open_for_path(reset_item);
flags.insert(NeedsUpdate::COMMANDS);
}
InternalEvent::AddHunk(hash) => {
if let Some((path, is_stage)) = self.selected_path() {
if is_stage {
if sync::unstage_hunk(CWD, path, *hash) {
if sync::unstage_hunk(CWD, path, hash) {
flags.insert(NeedsUpdate::ALL);
}
} else if sync::stage_hunk(CWD, path, *hash) {
} else if sync::stage_hunk(CWD, path, hash) {
flags.insert(NeedsUpdate::ALL);
}
}
}
InternalEvent::ShowMsg(msg) => {
self.msg.show_msg(msg);
self.msg.show_msg(msg.as_str());
flags.insert(NeedsUpdate::ALL);
}
InternalEvent::Update(u) => flags.insert(*u),
InternalEvent::Update(u) => flags.insert(u),
};
flags

View File

@ -6,12 +6,11 @@ use super::{
use crate::{
components::{CommandInfo, Component},
keys,
queue::{InternalEvent, NeedsUpdate, Queue},
queue::{InternalEvent, NeedsUpdate, Queue, ResetItem},
strings, ui,
};
use asyncgit::{hash, sync, StatusItem, StatusItemType, CWD};
use crossterm::event::Event;
use log::trace;
use std::{borrow::Cow, convert::From, path::Path};
use strings::commands;
use tui::{
@ -103,25 +102,28 @@ impl ChangesComponent {
fn index_add_remove(&mut self) -> bool {
if let Some(tree_item) = self.selection() {
if let FileTreeItemKind::File(i) = tree_item.kind {
if self.is_working_dir {
if let FileTreeItemKind::File(i) = tree_item.kind {
if let Some(status) = i.status {
let path = Path::new(i.path.as_str());
return match status {
StatusItemType::Deleted => {
sync::stage_addremoved(CWD, path)
}
_ => sync::stage_add(CWD, path),
_ => sync::stage_add_file(CWD, path),
};
}
} else {
let path = Path::new(i.path.as_str());
return sync::reset_stage(CWD, path);
//TODO: check if we can handle the one file case with it aswell
return sync::stage_add_all(
CWD,
tree_item.info.full_path.as_str(),
);
}
} else {
//TODO:
trace!("tbd");
let path =
Path::new(tree_item.info.full_path.as_str());
return sync::reset_stage(CWD, path);
}
}
@ -130,16 +132,16 @@ impl ChangesComponent {
fn dispatch_reset_workdir(&mut self) -> bool {
if let Some(tree_item) = self.selection() {
if let FileTreeItemKind::File(i) = tree_item.kind {
let is_folder =
matches!(tree_item.kind, FileTreeItemKind::Path(_));
self.queue.borrow_mut().push_back(
InternalEvent::ConfirmResetFile(i.path),
InternalEvent::ConfirmResetItem(ResetItem {
path: tree_item.info.full_path,
is_folder,
}),
);
return true;
} else {
//TODO:
trace!("tbd");
}
}
false
}
@ -282,22 +284,22 @@ impl Component for ChangesComponent {
out: &mut Vec<CommandInfo>,
_force_all: bool,
) -> CommandBlocking {
let some_selection =
self.selection().is_some() && self.is_file_seleted();
let some_selection = self.selection().is_some();
if self.is_working_dir {
out.push(CommandInfo::new(
commands::STAGE_FILE,
commands::STAGE_ITEM,
some_selection,
self.focused,
));
out.push(CommandInfo::new(
commands::RESET_FILE,
commands::RESET_ITEM,
some_selection,
self.focused,
));
} else {
out.push(CommandInfo::new(
commands::UNSTAGE_FILE,
commands::UNSTAGE_ITEM,
some_selection,
self.focused,
));

View File

@ -151,17 +151,37 @@ impl FileTreeItems {
self.0.len()
}
fn push_dirs(
item_path: &Path,
///
pub(crate) fn find_parent_index(
&self,
path: &str,
index: usize,
) -> usize {
if let Some(parent_path) = Path::new(path).parent() {
let parent_path = parent_path.to_str().unwrap();
for i in (0..=index).rev() {
let item = &self.0[i];
let item_path = &item.info.full_path;
if item_path == parent_path {
return i;
}
}
}
0
}
fn push_dirs<'a>(
item_path: &'a Path,
nodes: &mut BinaryHeap<FileTreeItem>,
paths_added: &mut BTreeSet<String>, //TODO: use a ref string here
paths_added: &mut BTreeSet<&'a Path>,
collapsed: &BTreeSet<&String>,
) {
for c in item_path.ancestors().skip(1) {
if c.parent().is_some() {
let path_string = String::from(c.to_str().unwrap());
if !paths_added.contains(&path_string) {
paths_added.insert(path_string.clone());
if !paths_added.contains(c) {
paths_added.insert(c);
let is_collapsed =
collapsed.contains(&path_string);
nodes.push(FileTreeItem::new_path(
@ -292,4 +312,25 @@ mod tests {
]
);
}
#[test]
fn test_find_parent() {
//0 a/
//1 b/
//2 c
//3 d
let res = FileTreeItems::new(
&string_vec_to_status(&[
"a/b/c", //
"a/b/d", //
]),
&BTreeSet::new(),
);
assert_eq!(
res.find_parent_index(&String::from("a/b/c"), 3),
1
);
}
}

View File

@ -3,7 +3,7 @@ use super::{
DrawableComponent,
};
use crate::{
queue::{InternalEvent, Queue},
queue::{InternalEvent, Queue, ResetItem},
strings, ui,
};
@ -20,7 +20,7 @@ use tui::{
///
pub struct ResetComponent {
path: String,
target: Option<ResetItem>,
visible: bool,
queue: Queue,
}
@ -107,21 +107,24 @@ impl ResetComponent {
///
pub fn new(queue: Queue) -> Self {
Self {
path: String::default(),
target: None,
visible: false,
queue,
}
}
///
pub fn open_for_path(&mut self, path: &str) {
self.path = path.to_string();
pub fn open_for_path(&mut self, item: ResetItem) {
self.target = Some(item);
self.show();
}
///
pub fn confirm(&mut self) {
self.hide();
if let Some(target) = self.target.take() {
self.queue
.borrow_mut()
.push_back(InternalEvent::ResetFile(self.path.clone()));
.push_back(InternalEvent::ResetItem(target));
}
self.hide();
}
}

View File

@ -2,7 +2,7 @@ use super::filetree::{
FileTreeItem, FileTreeItemKind, FileTreeItems, PathCollapsed,
};
use asyncgit::StatusItem;
use std::{cmp, collections::BTreeSet, path::Path};
use std::{cmp, collections::BTreeSet};
///
#[derive(Default)]
@ -193,7 +193,8 @@ impl StatusTree {
if collapsed)
{
SelectionChange::new(
self.find_parent_index(&item_path, current_selection),
self.tree
.find_parent_index(&item_path, current_selection),
false,
)
} else if matches!(item_kind, FileTreeItemKind::Path(PathCollapsed(collapsed))
@ -290,27 +291,6 @@ impl StatusTree {
}
}
}
fn find_parent_index(
&self,
path: &str,
current_index: usize,
) -> usize {
let path = Path::new(path);
if let Some(path) = path.parent() {
for i in (0..=current_index).rev() {
let item = self.tree.items().get(i).unwrap();
let item_path = &item.info.full_path;
//TODO: use parameter path here
if item_path.ends_with(path.to_str().unwrap()) {
return i;
}
}
}
0
}
}
#[cfg(test)]
@ -353,27 +333,6 @@ mod tests {
assert_eq!(res.selection, Some(0));
}
#[test]
fn test_select_parent() {
let items = string_vec_to_status(&[
"a/b/c", //
"a/b/d", //
]);
//0 a/
//1 b/
//2 c
//3 d
let mut res = StatusTree::default();
res.update(&items);
assert_eq!(
res.find_parent_index(&String::from("a/b/c"), 3),
1
);
}
#[test]
fn test_keep_selected_item() {
let mut res = StatusTree::default();

View File

@ -13,12 +13,20 @@ bitflags! {
}
}
/// data of item that is supposed to be reset
pub struct ResetItem {
/// path to the item (folder/file)
pub path: String,
/// are talking about a folder here? otherwise it's a single file
pub is_folder: bool,
}
///
pub enum InternalEvent {
///
ConfirmResetFile(String),
ConfirmResetItem(ResetItem),
///
ResetFile(String),
ResetItem(ResetItem),
///
AddHunk(u64),
///

View File

@ -79,21 +79,21 @@ pub mod commands {
CMD_GROUP_COMMIT,
);
///
pub static STAGE_FILE: CommandText = CommandText::new(
"Stage File [enter]",
"stage currently selected file",
pub static STAGE_ITEM: CommandText = CommandText::new(
"Stage Item [enter]",
"stage currently selected file or entire path",
CMD_GROUP_CHANGES,
);
///
pub static UNSTAGE_FILE: CommandText = CommandText::new(
"Unstage File [enter]",
"remove currently selected file from stage",
pub static UNSTAGE_ITEM: CommandText = CommandText::new(
"Unstage Item [enter]",
"unstage currently selected file or entire path",
CMD_GROUP_CHANGES,
);
///
pub static RESET_FILE: CommandText = CommandText::new(
"Reset File [D]",
"revert changes in selected file",
pub static RESET_ITEM: CommandText = CommandText::new(
"Reset Item [D]",
"revert changes in selected file or entire path",
CMD_GROUP_CHANGES,
);
///