Stage/unstage lines (#575)

This commit is contained in:
Stephan Dilly 2021-03-10 22:27:02 +01:00 committed by GitHub
parent 64acf1c13b
commit b5ef9b10f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 636 additions and 450 deletions

View file

@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- `[s]` key repurposed to trigger line based (un)stage
### Added
- support discarding diff by lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support stage/unstage selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support discarding selected lines ([#59](https://github.com/extrawurst/gitui/issues/59))
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
- visualize *conflicted* files differently ([#576](https://github.com/extrawurst/gitui/issues/576))

View file

@ -17,8 +17,6 @@
tab_toggle: ( code: Tab, modifiers: ( bits: 0,),),
tab_toggle_reverse: ( code: BackTab, modifiers: ( bits: 1,),),
focus_workdir: ( code: Char('w'), modifiers: ( bits: 0,),),
focus_stage: ( code: Char('s'), modifiers: ( bits: 0,),),
focus_right: ( code: Char('l'), modifiers: ( bits: 0,),),
focus_left: ( code: Char('h'), modifiers: ( bits: 0,),),
focus_above: ( code: Char('k'), modifiers: ( bits: 0,),),
@ -53,6 +51,7 @@
status_reset_item: ( code: Char('U'), modifiers: ( bits: 1,),),
status_reset_lines: ( code: Char('u'), modifiers: ( bits: 0,),),
status_ignore_file: ( code: Char('i'), modifiers: ( bits: 0,),),
diff_stage_lines: ( code: Char('s'), modifiers: ( bits: 0,),),
stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),),
stashing_toggle_untracked: ( code: Char('u'), modifiers: ( bits: 0,),),

View file

@ -157,6 +157,7 @@ pub(crate) fn get_diff_raw<'a>(
/// returns diff of a specific file either in `stage` or workdir
pub fn get_diff(
repo_path: &str,
//TODO: make &str
p: String,
stage: bool,
) -> Result<FileDiff> {

View file

@ -49,7 +49,7 @@ pub use remotes::{
tags::PushTagsProgress,
};
pub use reset::{reset_stage, reset_workdir};
pub use staging::discard_lines;
pub use staging::{discard_lines, stage_lines};
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
pub use state::{repo_state, RepoState};
pub use tags::{get_tags, CommitTags, Tags};
@ -62,8 +62,8 @@ pub use utils::{
mod tests {
use super::{
commit, stage_add_file,
staging::repo_write_file,
status::{get_status, StatusType},
utils::repo_write_file,
CommitId, LogWalker,
};
use crate::error::Result;

View file

@ -0,0 +1,344 @@
use super::{apply_selection, load_file};
use crate::error::Result;
use crate::sync::{
diff::DiffLinePosition,
patches::get_file_diff_patch_and_hunklines,
utils::{repo, repo_write_file},
};
use scopetime::scope_time;
/// discards specific lines in an unstaged hunk of a diff
pub fn discard_lines(
repo_path: &str,
file_path: &str,
lines: &[DiffLinePosition],
) -> Result<()> {
scope_time!("discard_lines");
if lines.is_empty() {
return Ok(());
}
let repo = repo(repo_path)?;
repo.index()?.read(true)?;
//TODO: check that file is not new (status modified)
let new_content = {
let (_patch, hunks) = get_file_diff_patch_and_hunklines(
&repo, file_path, false, false,
)?;
let working_content = load_file(&repo, file_path)?;
let old_lines = working_content.lines().collect::<Vec<_>>();
apply_selection(lines, &hunks, old_lines, false, true)?
};
repo_write_file(&repo, file_path, new_content.as_str())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::tests::{repo_init, write_commit_file};
#[test]
fn test_discard() {
static FILE_1: &str = r"0
1
2
3
4
";
static FILE_2: &str = r"0
3
4
";
static FILE_3: &str = r"0
2
3
4
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(3),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard2() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(3),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard3() {
static FILE_1: &str = r"start
1
end
";
static FILE_2: &str = r"start
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard4() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
1
mid
2
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(4),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard_if_first_selected_line_is_not_in_any_hunk() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
end
";
static FILE_3: &str = r"start
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(1),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
//this test shows that we require at least a diff context around add/removes of 1
#[test]
fn test_discard_deletions_filestart_breaking_with_zero_context() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard5() {
static FILE_1: &str = r"start
";
static FILE_2: &str = r"start
1";
static FILE_3: &str = r"start
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
}

View file

@ -1,48 +1,19 @@
mod discard_tracked;
mod stage_tracked;
pub use discard_tracked::discard_lines;
pub use stage_tracked::stage_lines;
use super::{
diff::DiffLinePosition,
patches::{get_file_diff_patch_and_hunklines, HunkLines},
utils::{repo, work_dir},
diff::DiffLinePosition, patches::HunkLines, utils::work_dir,
};
use crate::error::{Error, Result};
use crate::error::Result;
use git2::{DiffLine, Repository};
use scopetime::scope_time;
use std::{
collections::HashSet,
convert::TryFrom,
fs::File,
io::{Read, Write},
collections::HashSet, convert::TryFrom, fs::File, io::Read,
};
/// discards specific lines in an unstaged hunk of a diff
pub fn discard_lines(
repo_path: &str,
file_path: &str,
lines: &[DiffLinePosition],
) -> Result<()> {
scope_time!("discard_lines");
if lines.is_empty() {
return Ok(());
}
let repo = repo(repo_path)?;
//TODO: check that file is not new (status modified)
let new_content = {
let (_patch, hunks) = get_file_diff_patch_and_hunklines(
&repo, file_path, false, false,
)?;
let working_content = load_file(&repo, file_path)?;
let old_lines = working_content.lines().collect::<Vec<_>>();
apply_selection(lines, &hunks, old_lines, false, true)?
};
repo_write_file(&repo, file_path, new_content.as_str())?;
Ok(())
}
const NEWLINE: char = '\n';
#[derive(Default)]
struct NewFromOldContent {
@ -54,7 +25,7 @@ impl NewFromOldContent {
fn add_from_hunk(&mut self, line: &DiffLine) -> Result<()> {
let line = String::from_utf8(line.content().into())?;
let line = if line.ends_with('\n') {
let line = if line.ends_with(NEWLINE) {
line[0..line.len() - 1].to_string()
} else {
line
@ -89,18 +60,18 @@ impl NewFromOldContent {
self.lines.push(line.to_string());
}
let lines = self.lines.join("\n");
if lines.ends_with('\n') {
if lines.ends_with(NEWLINE) {
lines
} else {
let mut lines = lines;
lines.push('\n');
lines.push(NEWLINE);
lines
}
}
}
// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in nodegit: https://github.com/nodegit/nodegit
fn apply_selection(
pub(crate) fn apply_selection(
lines: &[DiffLinePosition],
hunks: &[HunkLines],
old_lines: Vec<&str>,
@ -132,7 +103,6 @@ fn apply_selection(
}
if first_hunk_encountered {
// catchup until this hunk
new_content.catchup_to_hunkstart(hunk_start, &old_lines);
for hunk_line in &hunk.lines {
@ -190,7 +160,10 @@ fn apply_selection(
Ok(new_content.finish(&old_lines))
}
fn load_file(repo: &Repository, file_path: &str) -> Result<String> {
pub(crate) fn load_file(
repo: &Repository,
file_path: &str,
) -> Result<String> {
let repo_path = work_dir(repo)?;
let mut file = File::open(repo_path.join(file_path).as_path())?;
let mut res = String::new();
@ -198,322 +171,3 @@ fn load_file(repo: &Repository, file_path: &str) -> Result<String> {
Ok(res)
}
//TODO: use this in unittests instead of the test specific one
/// write a file in repo
pub(crate) fn repo_write_file(
repo: &Repository,
file: &str,
content: &str,
) -> Result<()> {
let dir = work_dir(repo)?.join(file);
let file_path = dir.to_str().ok_or_else(|| {
Error::Generic(String::from("invalid file path"))
})?;
let mut file = File::create(file_path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::tests::{repo_init, write_commit_file};
#[test]
fn test_discard() {
static FILE_1: &str = r"0
1
2
3
4
";
static FILE_2: &str = r"0
3
4
";
static FILE_3: &str = r"0
2
3
4
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(3),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard2() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(3),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard3() {
static FILE_1: &str = r"start
1
end
";
static FILE_2: &str = r"start
2
end
";
static FILE_3: &str = r"start
1
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard4() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
1
mid
2
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(4),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard_if_first_selected_line_is_not_in_any_hunk() {
static FILE_1: &str = r"start
end
";
static FILE_2: &str = r"start
1
end
";
static FILE_3: &str = r"start
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[
DiffLinePosition {
old_lineno: None,
new_lineno: Some(1),
},
DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
},
],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
//this test shows that we require at least a diff context around add/removes of 1
#[test]
fn test_discard_deletions_filestart_breaking_with_zero_context() {
static FILE_1: &str = r"start
mid
end
";
static FILE_2: &str = r"start
end
";
static FILE_3: &str = r"start
mid
end
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: Some(2),
new_lineno: None,
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
#[test]
fn test_discard5() {
static FILE_1: &str = r"start
";
static FILE_2: &str = r"start
1";
static FILE_3: &str = r"start
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
discard_lines(
path,
"test.txt",
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
}],
)
.unwrap();
let result_file = load_file(&repo, "test.txt").unwrap();
assert_eq!(result_file.as_str(), FILE_3);
}
}

View file

@ -0,0 +1,157 @@
use super::apply_selection;
use crate::{
error::{Error, Result},
sync::{
diff::DiffLinePosition,
patches::get_file_diff_patch_and_hunklines, utils::repo,
},
};
use scopetime::scope_time;
use std::path::Path;
///
pub fn stage_lines(
repo_path: &str,
file_path: &str,
is_stage: bool,
lines: &[DiffLinePosition],
) -> Result<()> {
scope_time!("stage_lines");
if lines.is_empty() {
return Ok(());
}
let repo = repo(repo_path)?;
// log::debug!("stage_lines: {:?}", lines);
let mut index = repo.index()?;
index.read(true)?;
let mut idx =
index.get_path(Path::new(file_path), 0).ok_or_else(|| {
Error::Generic(String::from(
"only non new files supported",
))
})?;
let blob = repo.find_blob(idx.id)?;
let indexed_content = String::from_utf8(blob.content().into())?;
let new_content = {
let (_patch, hunks) = get_file_diff_patch_and_hunklines(
&repo, file_path, is_stage, false,
)?;
let old_lines = indexed_content.lines().collect::<Vec<_>>();
apply_selection(lines, &hunks, old_lines, is_stage, false)?
};
let blob_id = repo.blob(new_content.as_bytes())?;
idx.id = blob_id;
idx.file_size = new_content.as_bytes().len() as u32;
//TODO: can we simply use add_frombuffer?
index.add(&idx)?;
index.write()?;
index.read(true)?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::sync::{
diff::get_diff,
tests::{get_statuses, repo_init, write_commit_file},
utils::{repo_write_file, stage_add_file},
};
#[test]
fn test_stage() {
static FILE_1: &str = r"0
";
static FILE_2: &str = r"0
1
2
3
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
stage_lines(
path,
"test.txt",
false,
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
}],
)
.unwrap();
let diff =
get_diff(path, String::from("test.txt"), true).unwrap();
assert_eq!(diff.lines, 3);
assert_eq!(
diff.hunks[0].lines[0].content,
String::from("@@ -1 +1,2 @@\n")
);
}
#[test]
fn test_unstage() {
static FILE_1: &str = r"0
";
static FILE_2: &str = r"0
1
2
3
";
let (path, repo) = repo_init().unwrap();
let path = path.path().to_str().unwrap();
write_commit_file(&repo, "test.txt", FILE_1, "c1");
repo_write_file(&repo, "test.txt", FILE_2).unwrap();
assert_eq!(get_statuses(path), (1, 0));
stage_add_file(path, &Path::new("test.txt")).unwrap();
assert_eq!(get_statuses(path), (0, 1));
let diff_before =
get_diff(path, String::from("test.txt"), true).unwrap();
assert_eq!(diff_before.lines, 5);
stage_lines(
path,
"test.txt",
true,
&[DiffLinePosition {
old_lineno: None,
new_lineno: Some(2),
}],
)
.unwrap();
assert_eq!(get_statuses(path), (1, 1));
let diff =
get_diff(path, String::from("test.txt"), true).unwrap();
assert_eq!(diff.lines, 4);
}
}

View file

@ -118,11 +118,11 @@ mod tests {
use super::*;
use crate::sync::{
commit, get_commit_files, get_commits_info, stage_add_file,
staging::repo_write_file,
tests::{
debug_cmd_print, get_statuses, repo_init,
write_commit_file,
},
utils::repo_write_file,
};
use std::{fs::File, io::Write, path::Path};

View file

@ -4,7 +4,7 @@ use super::CommitId;
use crate::error::{Error, Result};
use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
use scopetime::scope_time;
use std::path::Path;
use std::{fs::File, io::Write, path::Path};
///
#[derive(PartialEq, Debug, Clone)]
@ -167,11 +167,26 @@ pub fn get_config_string(
Ok(entry.value().map(|s| s.to_string()))
}
}
/// helper function
pub(crate) fn bytes2string(bytes: &[u8]) -> Result<String> {
Ok(String::from_utf8(bytes.to_vec())?)
}
/// write a file in repo
pub(crate) fn repo_write_file(
repo: &Repository,
file: &str,
content: &str,
) -> Result<()> {
let dir = work_dir(repo)?.join(file);
let file_path = dir.to_str().ok_or_else(|| {
Error::Generic(String::from("invalid file path"))
})?;
let mut file = File::create(file_path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -491,7 +491,7 @@ impl DiffComponent {
Ok(())
}
fn queue_update(&mut self) {
fn queue_update(&self) {
self.queue
.as_ref()
.borrow_mut()
@ -514,10 +514,41 @@ impl DiffComponent {
}
fn reset_lines(&self) {
self.queue.as_ref().borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::ResetLines(
self.current.path.clone(),
self.selected_lines(),
)),
);
}
fn stage_lines(&self) {
if let Some(diff) = &self.diff {
if self.selected_hunk.is_some() {
let selected_lines: Vec<DiffLinePosition> = diff
.hunks
//TODO: support untracked files aswell
if !diff.untracked {
let selected_lines = self.selected_lines();
try_or_popup!(
self,
"(un)stage lines:",
sync::stage_lines(
CWD,
&self.current.path,
self.is_stage(),
&selected_lines,
)
);
self.queue_update();
}
}
}
fn selected_lines(&self) -> Vec<DiffLinePosition> {
self.diff
.as_ref()
.map(|diff| {
diff.hunks
.iter()
.flat_map(|hunk| hunk.lines.iter())
.enumerate()
@ -533,16 +564,9 @@ impl DiffComponent {
None
}
})
.collect();
self.queue.as_ref().borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::ResetLines(
self.current.path.clone(),
selected_lines,
)),
);
}
}
.collect()
})
.unwrap_or_default()
}
fn reset_untracked(&self) {
@ -678,6 +702,20 @@ impl Component for DiffComponent {
true,
self.focused && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_stage(&self.key_config),
//TODO: only if any modifications are selected
true,
self.focused && !self.is_stage(),
));
out.push(CommandInfo::new(
strings::commands::diff_lines_unstage(
&self.key_config,
),
//TODO: only if any modifications are selected
true,
self.focused && self.is_stage(),
));
}
CommandBlocking::PassingOn
@ -733,11 +771,17 @@ impl Component for DiffComponent {
}
}
Ok(true)
} else if e == self.key_config.diff_stage_lines
&& !self.is_immutable
{
self.stage_lines();
Ok(true)
} else if e == self.key_config.status_reset_lines
&& !self.is_immutable
&& !self.is_stage()
{
if let Some(diff) = &self.diff {
//TODO: reset untracked lines
if !diff.untracked {
self.reset_lines();
}

View file

@ -26,8 +26,6 @@ pub struct KeyConfig {
pub tab_stashes: KeyEvent,
pub tab_toggle: KeyEvent,
pub tab_toggle_reverse: KeyEvent,
pub focus_workdir: KeyEvent,
pub focus_stage: KeyEvent,
pub focus_right: KeyEvent,
pub focus_left: KeyEvent,
pub focus_above: KeyEvent,
@ -53,6 +51,7 @@ pub struct KeyConfig {
pub status_reset_item: KeyEvent,
pub status_reset_lines: KeyEvent,
pub status_ignore_file: KeyEvent,
pub diff_stage_lines: KeyEvent,
pub stashing_save: KeyEvent,
pub stashing_toggle_untracked: KeyEvent,
pub stashing_toggle_index: KeyEvent,
@ -81,8 +80,6 @@ impl Default for KeyConfig {
tab_stashes: KeyEvent { code: KeyCode::Char('4'), modifiers: KeyModifiers::empty()},
tab_toggle: KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::empty()},
tab_toggle_reverse: KeyEvent { code: KeyCode::BackTab, modifiers: KeyModifiers::SHIFT},
focus_workdir: KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::empty()},
focus_stage: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
focus_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()},
focus_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()},
focus_above: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::empty()},
@ -108,6 +105,7 @@ impl Default for KeyConfig {
status_reset_item: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
status_reset_lines: KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::empty()},
status_ignore_file: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()},
diff_stage_lines: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()},
stashing_toggle_index: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()},

View file

@ -21,20 +21,14 @@ pub static PUSH_TAGS_STATES_DONE: &str = "done";
pub static SELECT_BRANCH_POPUP_MSG: &str = "Switch Branch";
pub fn title_status(key_config: &SharedKeyConfig) -> String {
format!(
"Unstaged Changes [{}]",
key_config.get_hint(key_config.focus_workdir)
)
pub fn title_status(_key_config: &SharedKeyConfig) -> String {
"Unstaged Changes".to_string()
}
pub fn title_diff(_key_config: &SharedKeyConfig) -> String {
"Diff: ".to_string()
}
pub fn title_index(key_config: &SharedKeyConfig) -> String {
format!(
"Staged Changes [{}]",
key_config.get_hint(key_config.focus_stage)
)
pub fn title_index(_key_config: &SharedKeyConfig) -> String {
"Staged Changes".to_string()
}
pub fn tab_status(key_config: &SharedKeyConfig) -> String {
format!("Status [{}]", key_config.get_hint(key_config.tab_status))
@ -416,6 +410,30 @@ pub mod commands {
CMD_GROUP_DIFF,
)
}
pub fn diff_lines_stage(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Stage lines [{}]",
key_config.get_hint(key_config.diff_stage_lines),
),
"stage selected lines",
CMD_GROUP_DIFF,
)
}
pub fn diff_lines_unstage(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Unstage lines [{}]",
key_config.get_hint(key_config.diff_stage_lines),
),
"unstage selected lines",
CMD_GROUP_DIFF,
)
}
pub fn diff_hunk_remove(
key_config: &SharedKeyConfig,
) -> CommandText {
@ -460,18 +478,6 @@ pub mod commands {
)
.hide_help()
}
pub fn select_staging(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"To stage [{}]",
key_config.get_hint(key_config.focus_stage),
),
"focus/select staging area",
CMD_GROUP_GENERAL,
)
}
pub fn select_status(
key_config: &SharedKeyConfig,
) -> CommandText {
@ -485,18 +491,6 @@ pub mod commands {
CMD_GROUP_GENERAL,
)
}
pub fn select_unstaged(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"To unstaged [{}]",
key_config.get_hint(key_config.focus_workdir),
),
"focus/select unstaged area",
CMD_GROUP_GENERAL,
)
}
pub fn commit_open(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(

View file

@ -7,7 +7,7 @@ use crate::{
},
keys::SharedKeyConfig,
queue::{Action, InternalEvent, Queue, ResetItem},
strings::{self, order},
strings,
ui::style::SharedTheme,
};
use anyhow::Result;
@ -522,26 +522,6 @@ impl Component for Status {
.hidden(),
);
out.push(
CommandInfo::new(
strings::commands::select_staging(&self.key_config),
true,
(self.visible && self.focus == Focus::WorkDir)
|| force_all,
)
.order(order::NAV),
);
out.push(
CommandInfo::new(
strings::commands::select_unstaged(&self.key_config),
true,
(self.visible && self.focus == Focus::Stage)
|| force_all,
)
.order(order::NAV),
);
visibility_blocking(self)
}
@ -553,11 +533,7 @@ impl Component for Status {
}
if let Event::Key(k) = ev {
return if k == self.key_config.focus_workdir {
self.switch_focus(Focus::WorkDir)
} else if k == self.key_config.focus_stage {
self.switch_focus(Focus::Stage)
} else if k == self.key_config.edit_file
return if k == self.key_config.edit_file
&& (self.can_focus_diff()
|| self.focus == Focus::Diff)
{