mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Stage/unstage lines (#575)
This commit is contained in:
parent
64acf1c13b
commit
b5ef9b10f1
13 changed files with 636 additions and 450 deletions
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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,),),
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
344
asyncgit/src/sync/staging/discard_tracked.rs
Normal file
344
asyncgit/src/sync/staging/discard_tracked.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
157
asyncgit/src/sync/staging/stage_tracked.rs
Normal file
157
asyncgit/src/sync/staging/stage_tracked.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()},
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue