mirror of
https://github.com/extrawurst/gitui.git
synced 2024-12-27 11:03:03 +03:00
support discard selected lines (#571)
This commit is contained in:
parent
f86faf6406
commit
6e5db96c19
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- support discarding diff by lines ([#59](https://github.com/extrawurst/gitui/issues/59))
|
||||
- support for pushing tags ([#568](https://github.com/extrawurst/gitui/issues/568))
|
||||
|
||||
## [0.12.0] - 2020-03-03
|
||||
|
@ -50,8 +50,8 @@
|
||||
edit_file: ( code: Char('I'), modifiers: ( bits: 1,),),
|
||||
|
||||
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
|
||||
|
||||
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,),),
|
||||
|
||||
stashing_save: ( code: Char('w'), modifiers: ( bits: 0,),),
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::string::FromUtf8Error;
|
||||
use std::{num::TryFromIntError, string::FromUtf8Error};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@ -26,6 +26,9 @@ pub enum Error {
|
||||
|
||||
#[error("utf8 error:{0}")]
|
||||
Utf8Error(#[from] FromUtf8Error),
|
||||
|
||||
#[error("TryFromInt error:{0}")]
|
||||
IntError(#[from] TryFromIntError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
@ -3,6 +3,7 @@
|
||||
#![forbid(missing_docs)]
|
||||
#![deny(unsafe_code)]
|
||||
#![deny(unused_imports)]
|
||||
#![deny(unused_must_use)]
|
||||
#![deny(clippy::all)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::panic)]
|
||||
|
@ -92,14 +92,13 @@ pub fn merge_upstream_commit(
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::merge_ff::test::write_commit_file;
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
branch_compare_upstream,
|
||||
remotes::{fetch_origin, push::push},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare,
|
||||
repo_init_bare, write_commit_file,
|
||||
},
|
||||
RepoState,
|
||||
};
|
||||
|
@ -49,41 +49,12 @@ pub fn branch_merge_upstream_fastforward(
|
||||
pub mod test {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
commit,
|
||||
remotes::{fetch_origin, push::push},
|
||||
stage_add_file,
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare,
|
||||
repo_init_bare, write_commit_file,
|
||||
},
|
||||
CommitId,
|
||||
};
|
||||
use git2::Repository;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
// write, stage and commit a file
|
||||
pub fn write_commit_file(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
) -> CommitId {
|
||||
File::create(
|
||||
repo.workdir().unwrap().join(file).to_str().unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.write_all(content.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
stage_add_file(
|
||||
repo.workdir().unwrap().to_str().unwrap(),
|
||||
Path::new(file),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_fastforward() {
|
||||
|
@ -39,14 +39,41 @@ pub struct DiffLine {
|
||||
pub content: String,
|
||||
///
|
||||
pub line_type: DiffLineType,
|
||||
///
|
||||
pub position: DiffLinePosition,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone, Copy, Default, Hash, Debug, PartialEq, Eq)]
|
||||
pub struct DiffLinePosition {
|
||||
///
|
||||
pub old_lineno: Option<u32>,
|
||||
///
|
||||
pub new_lineno: Option<u32>,
|
||||
}
|
||||
|
||||
impl PartialEq<&git2::DiffLine<'_>> for DiffLinePosition {
|
||||
fn eq(&self, other: &&git2::DiffLine) -> bool {
|
||||
other.new_lineno() == self.new_lineno
|
||||
&& other.old_lineno() == self.old_lineno
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git2::DiffLine<'_>> for DiffLinePosition {
|
||||
fn from(line: &git2::DiffLine<'_>) -> Self {
|
||||
Self {
|
||||
old_lineno: line.old_lineno(),
|
||||
new_lineno: line.new_lineno(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
|
||||
pub(crate) struct HunkHeader {
|
||||
old_start: u32,
|
||||
old_lines: u32,
|
||||
new_start: u32,
|
||||
new_lines: u32,
|
||||
pub old_start: u32,
|
||||
pub old_lines: u32,
|
||||
pub new_start: u32,
|
||||
pub new_lines: u32,
|
||||
}
|
||||
|
||||
impl From<DiffHunk<'_>> for HunkHeader {
|
||||
@ -89,10 +116,14 @@ pub(crate) fn get_diff_raw<'a>(
|
||||
p: &str,
|
||||
stage: bool,
|
||||
reverse: bool,
|
||||
context: Option<u32>,
|
||||
) -> Result<Diff<'a>> {
|
||||
// scope_time!("get_diff_raw");
|
||||
|
||||
let mut opt = DiffOptions::new();
|
||||
if let Some(context) = context {
|
||||
opt.context_lines(context);
|
||||
}
|
||||
opt.pathspec(p);
|
||||
opt.reverse(reverse);
|
||||
|
||||
@ -133,7 +164,7 @@ pub fn get_diff(
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let work_dir = work_dir(&repo)?;
|
||||
let diff = get_diff_raw(&repo, &p, stage, false)?;
|
||||
let diff = get_diff_raw(&repo, &p, stage, false, None)?;
|
||||
|
||||
raw_diff_to_file_diff(&diff, work_dir)
|
||||
}
|
||||
@ -209,6 +240,7 @@ fn raw_diff_to_file_diff<'a>(
|
||||
};
|
||||
|
||||
let diff_line = DiffLine {
|
||||
position: DiffLinePosition::from(&line),
|
||||
content: String::from_utf8_lossy(line.content())
|
||||
.to_string(),
|
||||
line_type,
|
||||
|
@ -19,7 +19,7 @@ pub fn stage_hunk(
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, &file_path, false, false)?;
|
||||
let diff = get_diff_raw(&repo, &file_path, false, false, None)?;
|
||||
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|hunk| {
|
||||
@ -46,7 +46,7 @@ pub fn reset_hunk(
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, &file_path, false, false)?;
|
||||
let diff = get_diff_raw(&repo, &file_path, false, false, None)?;
|
||||
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
if let Some(hunk_index) = hunk_index {
|
||||
@ -58,7 +58,8 @@ pub fn reset_hunk(
|
||||
res
|
||||
});
|
||||
|
||||
let diff = get_diff_raw(&repo, &file_path, false, true)?;
|
||||
let diff =
|
||||
get_diff_raw(&repo, &file_path, false, true, None)?;
|
||||
|
||||
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
|
||||
|
||||
@ -104,7 +105,7 @@ pub fn unstage_hunk(
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, &file_path, true, false)?;
|
||||
let diff = get_diff_raw(&repo, &file_path, true, false, None)?;
|
||||
let diff_count_positive = diff.deltas().len();
|
||||
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
@ -113,7 +114,7 @@ pub fn unstage_hunk(
|
||||
Ok,
|
||||
)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, &file_path, true, true)?;
|
||||
let diff = get_diff_raw(&repo, &file_path, true, true, None)?;
|
||||
|
||||
if diff.deltas().len() != diff_count_positive {
|
||||
return Err(Error::Generic(format!(
|
||||
|
@ -14,8 +14,10 @@ mod hooks;
|
||||
mod hunks;
|
||||
mod ignore;
|
||||
mod logwalker;
|
||||
mod patches;
|
||||
pub mod remotes;
|
||||
mod reset;
|
||||
mod staging;
|
||||
mod stash;
|
||||
mod state;
|
||||
pub mod status;
|
||||
@ -47,6 +49,7 @@ pub use remotes::{
|
||||
tags::PushTagsProgress,
|
||||
};
|
||||
pub use reset::{reset_stage, reset_workdir};
|
||||
pub use staging::discard_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};
|
||||
@ -58,12 +61,14 @@ pub use utils::{
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
commit, stage_add_file,
|
||||
staging::repo_write_file,
|
||||
status::{get_status, StatusType},
|
||||
CommitId, LogWalker,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use git2::Repository;
|
||||
use std::process::Command;
|
||||
use std::{path::Path, process::Command};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Calling `set_search_path` with an empty directory makes sure that there
|
||||
@ -88,6 +93,25 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
/// write, stage and commit a file
|
||||
pub fn write_commit_file(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
) -> CommitId {
|
||||
repo_write_file(repo, file, content).unwrap();
|
||||
|
||||
stage_add_file(
|
||||
repo.workdir().unwrap().to_str().unwrap(),
|
||||
Path::new(file),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
|
||||
sandbox_config_files();
|
||||
|
74
asyncgit/src/sync/patches.rs
Normal file
74
asyncgit/src/sync/patches.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use super::diff::{get_diff_raw, HunkHeader};
|
||||
use crate::error::{Error, Result};
|
||||
use git2::{Diff, DiffLine, Patch, Repository};
|
||||
|
||||
//
|
||||
pub(crate) struct HunkLines<'a> {
|
||||
pub hunk: HunkHeader,
|
||||
pub lines: Vec<DiffLine<'a>>,
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn get_file_diff_patch_and_hunklines<'a>(
|
||||
repo: &'a Repository,
|
||||
file: &str,
|
||||
is_staged: bool,
|
||||
reverse: bool,
|
||||
) -> Result<(Patch<'a>, Vec<HunkLines<'a>>)> {
|
||||
let diff =
|
||||
get_diff_raw(&repo, file, is_staged, reverse, Some(1))?;
|
||||
let patches = get_patches(&diff)?;
|
||||
if patches.len() > 1 {
|
||||
return Err(Error::Generic(String::from("patch error")));
|
||||
}
|
||||
|
||||
let patch = patches.into_iter().next().ok_or_else(|| {
|
||||
Error::Generic(String::from("no patch found"))
|
||||
})?;
|
||||
|
||||
let lines = patch_get_hunklines(&patch)?;
|
||||
|
||||
Ok((patch, lines))
|
||||
}
|
||||
|
||||
//
|
||||
fn patch_get_hunklines<'a>(
|
||||
patch: &Patch<'a>,
|
||||
) -> Result<Vec<HunkLines<'a>>> {
|
||||
let count_hunks = patch.num_hunks();
|
||||
let mut res = Vec::with_capacity(count_hunks);
|
||||
for hunk_idx in 0..count_hunks {
|
||||
let (hunk, _) = patch.hunk(hunk_idx)?;
|
||||
|
||||
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
|
||||
|
||||
let mut hunk = HunkLines {
|
||||
hunk: HunkHeader::from(hunk),
|
||||
lines: Vec::with_capacity(count_lines),
|
||||
};
|
||||
|
||||
for line_idx in 0..count_lines {
|
||||
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
|
||||
hunk.lines.push(line);
|
||||
}
|
||||
|
||||
res.push(hunk);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
//
|
||||
fn get_patches<'a>(diff: &Diff<'a>) -> Result<Vec<Patch<'a>>> {
|
||||
let count = diff.deltas().len();
|
||||
|
||||
let mut res = Vec::with_capacity(count);
|
||||
for idx in 0..count {
|
||||
let p = Patch::from_diff(&diff, idx)?;
|
||||
if let Some(p) = p {
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
//!
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{
|
||||
push::{remote_callbacks, AsyncProgress},
|
||||
utils,
|
||||
@ -13,6 +11,7 @@ use crate::{
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::{Direction, PushOptions};
|
||||
use scopetime::scope_time;
|
||||
use std::collections::HashSet;
|
||||
|
||||
///
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
@ -157,37 +156,8 @@ mod tests {
|
||||
self,
|
||||
remotes::{fetch_origin, push::push},
|
||||
tests::{repo_clone, repo_init_bare},
|
||||
CommitId,
|
||||
};
|
||||
use git2::Repository;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
// write, stage and commit a file
|
||||
fn write_commit_file(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
) -> CommitId {
|
||||
File::create(
|
||||
repo.workdir().unwrap().join(file).to_str().unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.write_all(content.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
sync::stage_add_file(
|
||||
repo.workdir().unwrap().to_str().unwrap(),
|
||||
Path::new(file),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
sync::commit(
|
||||
repo.workdir().unwrap().to_str().unwrap(),
|
||||
commit_name,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
use sync::tests::write_commit_file;
|
||||
|
||||
#[test]
|
||||
fn test_push_pull_tags() {
|
||||
|
519
asyncgit/src/sync/staging/mod.rs
Normal file
519
asyncgit/src/sync/staging/mod.rs
Normal file
@ -0,0 +1,519 @@
|
||||
use super::{
|
||||
diff::DiffLinePosition,
|
||||
patches::{get_file_diff_patch_and_hunklines, HunkLines},
|
||||
utils::{repo, work_dir},
|
||||
};
|
||||
use crate::error::{Error, Result};
|
||||
use git2::{DiffLine, Repository};
|
||||
use scopetime::scope_time;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryFrom,
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NewFromOldContent {
|
||||
lines: Vec<String>,
|
||||
old_index: usize,
|
||||
}
|
||||
|
||||
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') {
|
||||
line[0..line.len() - 1].to_string()
|
||||
} else {
|
||||
line
|
||||
};
|
||||
|
||||
self.lines.push(line);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn skip_old_line(&mut self) {
|
||||
self.old_index += 1;
|
||||
}
|
||||
|
||||
fn add_old_line(&mut self, old_lines: &[&str]) {
|
||||
self.lines.push(old_lines[self.old_index].to_string());
|
||||
self.old_index += 1;
|
||||
}
|
||||
|
||||
fn catchup_to_hunkstart(
|
||||
&mut self,
|
||||
hunk_start: usize,
|
||||
old_lines: &[&str],
|
||||
) {
|
||||
while hunk_start > self.old_index + 1 {
|
||||
self.add_old_line(old_lines);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self, old_lines: &[&str]) -> String {
|
||||
for line in old_lines.iter().skip(self.old_index) {
|
||||
self.lines.push(line.to_string());
|
||||
}
|
||||
let lines = self.lines.join("\n");
|
||||
if lines.ends_with('\n') {
|
||||
lines
|
||||
} else {
|
||||
let mut lines = lines;
|
||||
lines.push('\n');
|
||||
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(
|
||||
lines: &[DiffLinePosition],
|
||||
hunks: &[HunkLines],
|
||||
old_lines: Vec<&str>,
|
||||
is_staged: bool,
|
||||
reverse: bool,
|
||||
) -> Result<String> {
|
||||
let mut new_content = NewFromOldContent::default();
|
||||
let lines = lines.iter().collect::<HashSet<_>>();
|
||||
|
||||
let char_added = if reverse { '-' } else { '+' };
|
||||
let char_deleted = if reverse { '+' } else { '-' };
|
||||
|
||||
let mut first_hunk_encountered = false;
|
||||
for hunk in hunks {
|
||||
let hunk_start = if is_staged || reverse {
|
||||
usize::try_from(hunk.hunk.new_start)?
|
||||
} else {
|
||||
usize::try_from(hunk.hunk.old_start)?
|
||||
};
|
||||
|
||||
if !first_hunk_encountered {
|
||||
let any_slection_in_hunk =
|
||||
hunk.lines.iter().any(|line| {
|
||||
let line: DiffLinePosition = line.into();
|
||||
lines.contains(&line)
|
||||
});
|
||||
|
||||
first_hunk_encountered = any_slection_in_hunk;
|
||||
}
|
||||
|
||||
if first_hunk_encountered {
|
||||
// catchup until this hunk
|
||||
new_content.catchup_to_hunkstart(hunk_start, &old_lines);
|
||||
|
||||
for hunk_line in &hunk.lines {
|
||||
let hunk_line_pos: DiffLinePosition =
|
||||
hunk_line.into();
|
||||
let selected_line = lines.contains(&hunk_line_pos);
|
||||
|
||||
log::debug!(
|
||||
// println!(
|
||||
"{} line: {} [{:?} old, {:?} new] -> {}",
|
||||
if selected_line { "*" } else { " " },
|
||||
hunk_line.origin(),
|
||||
hunk_line.old_lineno(),
|
||||
hunk_line.new_lineno(),
|
||||
String::from_utf8_lossy(hunk_line.content())
|
||||
.trim()
|
||||
);
|
||||
|
||||
if hunk_line.origin() == '<' {
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_staged && !selected_line)
|
||||
|| (!is_staged && selected_line)
|
||||
{
|
||||
if hunk_line.origin() == char_added {
|
||||
new_content.add_from_hunk(hunk_line)?;
|
||||
if is_staged {
|
||||
new_content.skip_old_line();
|
||||
}
|
||||
} else if hunk_line.origin() == char_deleted {
|
||||
if !is_staged {
|
||||
new_content.skip_old_line();
|
||||
}
|
||||
} else {
|
||||
new_content.add_old_line(&old_lines);
|
||||
}
|
||||
} else {
|
||||
if hunk_line.origin() != char_added {
|
||||
new_content.add_from_hunk(hunk_line)?;
|
||||
}
|
||||
|
||||
if (is_staged
|
||||
&& hunk_line.origin() != char_deleted)
|
||||
|| (!is_staged
|
||||
&& hunk_line.origin() != char_added)
|
||||
{
|
||||
new_content.skip_old_line();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_content.finish(&old_lines))
|
||||
}
|
||||
|
||||
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();
|
||||
file.read_to_string(&mut res)?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -502,6 +502,10 @@ impl App {
|
||||
sync::reset_hunk(CWD, path, hash)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::ResetLines(path, lines) => {
|
||||
sync::discard_lines(CWD, &path, &lines)?;
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
}
|
||||
Action::DeleteBranch(branch_ref) => {
|
||||
if let Err(e) =
|
||||
sync::delete_branch(CWD, &branch_ref)
|
||||
|
@ -9,7 +9,11 @@ use crate::{
|
||||
ui::{self, calc_scroll_top, style::SharedTheme},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use asyncgit::{hash, sync, DiffLine, DiffLineType, FileDiff, CWD};
|
||||
use asyncgit::{
|
||||
hash,
|
||||
sync::{self, diff::DiffLinePosition},
|
||||
DiffLine, DiffLineType, FileDiff, CWD,
|
||||
};
|
||||
use bytesize::ByteSize;
|
||||
use crossterm::event::Event;
|
||||
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
|
||||
@ -509,6 +513,38 @@ impl DiffComponent {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_lines(&self) {
|
||||
if let Some(diff) = &self.diff {
|
||||
if self.selected_hunk.is_some() {
|
||||
let selected_lines: Vec<DiffLinePosition> = diff
|
||||
.hunks
|
||||
.iter()
|
||||
.flat_map(|hunk| hunk.lines.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, line)| {
|
||||
let is_add_or_delete = line.line_type
|
||||
== DiffLineType::Add
|
||||
|| line.line_type == DiffLineType::Delete;
|
||||
if self.selection.contains(i)
|
||||
&& is_add_or_delete
|
||||
{
|
||||
Some(line.position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.queue.as_ref().borrow_mut().push_back(
|
||||
InternalEvent::ConfirmAction(Action::ResetLines(
|
||||
self.current.path.clone(),
|
||||
selected_lines,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_untracked(&self) {
|
||||
self.queue.as_ref().borrow_mut().push_back(
|
||||
InternalEvent::ConfirmAction(Action::Reset(ResetItem {
|
||||
@ -634,11 +670,20 @@ impl Component for DiffComponent {
|
||||
self.selected_hunk.is_some(),
|
||||
self.focused && !self.is_stage(),
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::diff_lines_revert(
|
||||
&self.key_config,
|
||||
),
|
||||
//TODO: only if any modifications are selected
|
||||
true,
|
||||
self.focused && !self.is_stage(),
|
||||
));
|
||||
}
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||
if self.focused {
|
||||
if let Event::Key(e) = ev {
|
||||
@ -688,6 +733,16 @@ impl Component for DiffComponent {
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else if e == self.key_config.status_reset_lines
|
||||
&& !self.is_immutable
|
||||
&& !self.is_stage()
|
||||
{
|
||||
if let Some(diff) = &self.diff {
|
||||
if !diff.untracked {
|
||||
self.reset_lines();
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else if e == self.key_config.copy {
|
||||
self.copy_selection();
|
||||
Ok(true)
|
||||
|
@ -151,6 +151,10 @@ impl ResetComponent {
|
||||
strings::confirm_title_reset(&self.key_config),
|
||||
strings::confirm_msg_resethunk(&self.key_config),
|
||||
),
|
||||
Action::ResetLines(_, lines) => (
|
||||
strings::confirm_title_reset(&self.key_config),
|
||||
strings::confirm_msg_reset_lines(&self.key_config,lines.len()),
|
||||
),
|
||||
Action::DeleteBranch(branch_ref) => (
|
||||
strings::confirm_title_delete_branch(
|
||||
&self.key_config,
|
||||
|
@ -51,6 +51,7 @@ pub struct KeyConfig {
|
||||
pub edit_file: KeyEvent,
|
||||
pub status_stage_all: KeyEvent,
|
||||
pub status_reset_item: KeyEvent,
|
||||
pub status_reset_lines: KeyEvent,
|
||||
pub status_ignore_file: KeyEvent,
|
||||
pub stashing_save: KeyEvent,
|
||||
pub stashing_toggle_untracked: KeyEvent,
|
||||
@ -105,6 +106,7 @@ impl Default for KeyConfig {
|
||||
edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()},
|
||||
status_stage_all: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::empty()},
|
||||
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()},
|
||||
stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()},
|
||||
stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()},
|
||||
|
@ -1,5 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unused_imports)]
|
||||
#![deny(unused_must_use)]
|
||||
#![deny(clippy::cargo)]
|
||||
#![deny(clippy::pedantic)]
|
||||
#![deny(clippy::perf)]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::tabs::StashingOptions;
|
||||
use asyncgit::sync::{CommitId, CommitTags};
|
||||
use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags};
|
||||
use bitflags::bitflags;
|
||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||
|
||||
@ -27,6 +27,7 @@ pub struct ResetItem {
|
||||
pub enum Action {
|
||||
Reset(ResetItem),
|
||||
ResetHunk(String, u64),
|
||||
ResetLines(String, Vec<DiffLinePosition>),
|
||||
StashDrop(CommitId),
|
||||
DeleteBranch(String),
|
||||
ForcePush(String, bool),
|
||||
|
@ -107,6 +107,15 @@ pub fn confirm_msg_merge(
|
||||
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
|
||||
"confirm file reset?".to_string()
|
||||
}
|
||||
pub fn confirm_msg_reset_lines(
|
||||
_key_config: &SharedKeyConfig,
|
||||
lines: usize,
|
||||
) -> String {
|
||||
format!(
|
||||
"are you sure you want to discard {} selected lines?",
|
||||
lines
|
||||
)
|
||||
}
|
||||
pub fn confirm_msg_stashdrop(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
@ -388,13 +397,25 @@ pub mod commands {
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Revert hunk [{}]",
|
||||
"Reset hunk [{}]",
|
||||
key_config.get_hint(key_config.status_reset_item),
|
||||
),
|
||||
"reverts selected hunk",
|
||||
CMD_GROUP_DIFF,
|
||||
)
|
||||
}
|
||||
pub fn diff_lines_revert(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Reset lines [{}]",
|
||||
key_config.get_hint(key_config.status_reset_lines),
|
||||
),
|
||||
"resets selected lines",
|
||||
CMD_GROUP_DIFF,
|
||||
)
|
||||
}
|
||||
pub fn diff_hunk_remove(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
Loading…
Reference in New Issue
Block a user