support options for the way we calculate the status (#849)

This commit is contained in:
Stephan Dilly 2021-08-19 02:19:36 +02:00 committed by GitHub
parent 923323bd71
commit 7cc19f673a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 638 additions and 75 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
**options**
![options](assets/options.gif)
**drop multiple stashes**
![drop-multiple-stashes](assets/drop-multiple-stashes.gif)
@ -16,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
![name-validation](assets/branch-validation.gif)
## Added
- new options popup (show untracked files, diff settings) ([#849](https://github.com/extrawurst/gitui/issues/849))
- mark and drop multiple stashes ([#854](https://github.com/extrawurst/gitui/issues/854))
- check branch name validity while typing ([#559](https://github.com/extrawurst/gitui/issues/559))
- support deleting remote branch [[@zcorniere](https://github.com/zcorniere)] ([#622](https://github.com/extrawurst/gitui/issues/622))

1
assets/options.drawio Normal file
View file

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-08-17T21:58:53.216Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" etag="DR-vNI6rA-1d9_EpYLQC" version="14.9.7" type="device"><diagram id="cIq82w5ce00BbVejkL92" name="Page-1">5Zldc6IwFIZ/DZfrEJCvS0Xb7exHZ9bd6WxvdlIIkGlMmBir3V+/iQQVg+12Bqm7eqHwkkB43nMCJ1puPF9fc1gWX1iKiOXY6dpyJ5bjABCG8kcpz5Xi+0El5BynutFOmOHfSIu2Vpc4RYtGQ8EYEbhsigmjFCWioUHO2arZLGOkedUS5sgQZgkkpnqHU1FUaugEO/0jwnlRXxn4UXVkDuvG+k4WBUzZak9yp5Ybc8ZEtTVfx4goeDWXqt/VkaPbgXFExd90KLNx+X10/wl4v+6/5fx28jMVH/RZniBZ6hvWgxXPNQHOljRF6iS25Y5XBRZoVsJEHV1Jz6VWiDmRe0Bu6tMhLtD66DjB9u5l2CA2R4I/yyZ1h0B30REDvGDgVcpq54AbaazFHn0n1CLUrufbs+/AyA3N5g2cnPPj5Hh+g5Njm5S2wbdPCYTeiSi5BqVZIUPesX9QwWHyKOkcUpP3L4c1hgTnVCoEZWpXgcEyCUdanuM0VT3GTcgZJiRmhPHNudxs8+mIrn9A17UNutE2MBtBeKoYHBp0JyiDSyKOQd2Lt4Xg7BHVrCij6ACflmofEokJ8ZecaAvwpjsduDC03RrxCz74bVPBqVzwDBfuFInFhoR8DhWQ5vKB1b0lr6VGL4Y4fjBwDhwJTUcA6Dc1fMOUm5wyjv7nzBgOgteN6DU1AsOFmNENccf+jKnKCh/OFQ76sCi3HC4iS1zbNCfs05zQMGd4UdnRZkCv2REZBmDFqljSRymTKj8uJx+clvepPu2oC8w9P+wT8D/jhGhxoNeEAGbxORNQLM8zDTL5KJvpIQ07cKTtTQq8e40BzEJ3gmVNdamO+O/viFlU35YCM3r+aaL2CXxAZMx4ivjBNbtwzIsOZzUQvb9jZoloWPWmxaIDdzoB11zdcD2TWuva0fBk0MwSLt6U0moAkKby/SiXm0xORfZyt5pky2BW7032DZVf9ePDtrzxNRYJoxmWvWJ1aLMQpba+oWTJF/gJWd7kWAp1uxzVZm8nJgaDenl962PLGiDwt9PavpXuyaxsqQMJWxwtxs9u7Q8EzewYAq9tBbrXOcUs32YrGeFFFezW1LGiqTUC1vTKGodWeKWUUWyF3k7pK9xPMD+1OxBEfTpg1m9f4VOFfzD4p+DWL94vh3c3cOXu7i+uzbG9Pwrd6R8=</diagram></mxfile>

BIN
assets/options.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View file

@ -1,7 +1,7 @@
use crate::{
error::Result,
hash,
sync::{self, CommitId},
sync::{self, diff::DiffOptions, CommitId},
AsyncGitNotification, FileDiff, CWD,
};
use crossbeam_channel::Sender;
@ -14,7 +14,7 @@ use std::{
};
///
#[derive(Hash, Clone, PartialEq)]
#[derive(Debug, Hash, Clone, PartialEq)]
pub enum DiffType {
/// diff in a given commit
Commit(CommitId),
@ -25,12 +25,14 @@ pub enum DiffType {
}
///
#[derive(Hash, Clone, PartialEq)]
#[derive(Debug, Hash, Clone, PartialEq)]
pub struct DiffParams {
/// path to the file to diff
pub path: String,
/// what kind of diff
pub diff_type: DiffType,
/// diff options
pub options: DiffOptions,
}
struct Request<R, A>(R, Option<A>);
@ -87,7 +89,7 @@ impl AsyncDiff {
&mut self,
params: DiffParams,
) -> Result<Option<FileDiff>> {
log::trace!("request");
log::trace!("request {:?}", params);
let hash = hash(&params);
@ -148,12 +150,18 @@ impl AsyncDiff {
hash: u64,
) -> Result<bool> {
let res = match params.diff_type {
DiffType::Stage => {
sync::diff::get_diff(CWD, &params.path, true)?
}
DiffType::WorkDir => {
sync::diff::get_diff(CWD, &params.path, false)?
}
DiffType::Stage => sync::diff::get_diff(
CWD,
&params.path,
true,
Some(params.options),
)?,
DiffType::WorkDir => sync::diff::get_diff(
CWD,
&params.path,
false,
Some(params.options),
)?,
DiffType::Commit(id) => sync::diff::get_diff_commit(
CWD,
id,

View file

@ -1,7 +1,7 @@
use crate::{
error::Result,
hash,
sync::{self, status::StatusType},
sync::{self, status::StatusType, ShowUntrackedFilesConfig},
AsyncGitNotification, StatusItem, CWD,
};
use crossbeam_channel::Sender;
@ -31,14 +31,19 @@ pub struct Status {
pub struct StatusParams {
tick: u128,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
}
impl StatusParams {
///
pub fn new(status_type: StatusType) -> Self {
pub fn new(
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
) -> Self {
Self {
tick: current_tick(),
status_type,
config,
}
}
}
@ -109,12 +114,14 @@ impl AsyncStatus {
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let status_type = params.status_type;
let config = params.config;
self.pending.fetch_add(1, Ordering::Relaxed);
rayon_core::spawn(move || {
let ok = Self::fetch_helper(
status_type,
config,
hash_request,
&arc_current,
&arc_last,
@ -135,11 +142,12 @@ impl AsyncStatus {
fn fetch_helper(
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
hash_request: u64,
arc_current: &Arc<Mutex<Request<u64, Status>>>,
arc_last: &Arc<Mutex<Status>>,
) -> Result<()> {
let res = Self::get_status(status_type)?;
let res = Self::get_status(status_type, config)?;
log::trace!(
"status fetched: {} (type: {:?})",
hash_request,
@ -161,9 +169,16 @@ impl AsyncStatus {
Ok(())
}
fn get_status(status_type: StatusType) -> Result<Status> {
fn get_status(
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
) -> Result<Status> {
Ok(Status {
items: sync::status::get_status(CWD, status_type)?,
items: sync::status::get_status(
CWD,
status_type,
config,
)?,
})
}
}

View file

@ -5,6 +5,7 @@ use scopetime::scope_time;
// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles
/// represents the `status.showUntrackedFiles` git config state
#[derive(Hash, Copy, Clone, PartialEq)]
pub enum ShowUntrackedFilesConfig {
///
No,
@ -14,19 +15,25 @@ pub enum ShowUntrackedFilesConfig {
All,
}
impl Default for ShowUntrackedFilesConfig {
fn default() -> Self {
Self::No
}
}
impl ShowUntrackedFilesConfig {
///
pub const fn include_none(&self) -> bool {
pub const fn include_none(self) -> bool {
matches!(self, Self::No)
}
///
pub const fn include_untracked(&self) -> bool {
pub const fn include_untracked(self) -> bool {
matches!(self, Self::Normal | Self::All)
}
///
pub const fn recurse_untracked_dirs(&self) -> bool {
pub const fn recurse_untracked_dirs(self) -> bool {
matches!(self, Self::All)
}
}

View file

@ -8,8 +8,7 @@ use super::{
use crate::{error::Error, error::Result, hash};
use easy_cast::Conv;
use git2::{
Delta, Diff, DiffDelta, DiffFormat, DiffHunk, DiffOptions, Patch,
Repository,
Delta, Diff, DiffDelta, DiffFormat, DiffHunk, Patch, Repository,
};
use scopetime::scope_time;
use std::{cell::RefCell, fs, path::Path, rc::Rc};
@ -125,18 +124,41 @@ pub struct FileDiff {
pub size_delta: i64,
}
/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>
#[derive(Debug, Hash, Clone, Copy, PartialEq)]
pub struct DiffOptions {
/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>
pub ignore_whitespace: bool,
/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>
pub context: u32,
/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>
pub interhunk_lines: u32,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
ignore_whitespace: false,
context: 3,
interhunk_lines: 0,
}
}
}
pub(crate) fn get_diff_raw<'a>(
repo: &'a Repository,
p: &str,
stage: bool,
reverse: bool,
context: Option<u32>,
options: Option<DiffOptions>,
) -> Result<Diff<'a>> {
// scope_time!("get_diff_raw");
let mut opt = DiffOptions::new();
if let Some(context) = context {
opt.context_lines(context);
let mut opt = git2::DiffOptions::new();
if let Some(options) = options {
opt.context_lines(options.context);
opt.ignore_whitespace(options.ignore_whitespace);
opt.interhunk_lines(options.interhunk_lines);
}
opt.pathspec(p);
opt.reverse(reverse);
@ -173,12 +195,13 @@ pub fn get_diff(
repo_path: &str,
p: &str,
stage: bool,
options: Option<DiffOptions>,
) -> Result<FileDiff> {
scope_time!("get_diff");
let repo = utils::repo(repo_path)?;
let work_dir = work_dir(&repo)?;
let diff = get_diff_raw(&repo, p, stage, false, None)?;
let diff = get_diff_raw(&repo, p, stage, false, options)?;
raw_diff_to_file_diff(&diff, work_dir)
}
@ -386,7 +409,8 @@ mod tests {
assert_eq!(get_statuses(repo_path), (1, 0));
let diff = get_diff(repo_path, "foo/bar.txt", false).unwrap();
let diff =
get_diff(repo_path, "foo/bar.txt", false, None).unwrap();
assert_eq!(diff.hunks.len(), 1);
assert_eq!(diff.hunks[0].lines[1].content, "test\n");
@ -412,9 +436,13 @@ mod tests {
assert_eq!(get_statuses(repo_path), (0, 1));
let diff =
get_diff(repo_path, file_path.to_str().unwrap(), true)
.unwrap();
let diff = get_diff(
repo_path,
file_path.to_str().unwrap(),
true,
None,
)
.unwrap();
assert_eq!(diff.hunks.len(), 1);
}
@ -462,8 +490,8 @@ mod tests {
.unwrap();
}
let res =
get_status(repo_path, StatusType::WorkingDir).unwrap();
let res = get_status(repo_path, StatusType::WorkingDir, None)
.unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].path, "bar.txt");
@ -480,7 +508,8 @@ mod tests {
assert_eq!(get_statuses(repo_path), (1, 1));
let res = get_diff(repo_path, "bar.txt", false).unwrap();
let res =
get_diff(repo_path, "bar.txt", false, None).unwrap();
assert_eq!(res.hunks.len(), 2)
}
@ -503,6 +532,7 @@ mod tests {
sub_path.to_str().unwrap(),
file_path.to_str().unwrap(),
false,
None,
)
.unwrap();
@ -525,9 +555,13 @@ mod tests {
File::create(&root.join(file_path))?
.write_all(b"\x00\x02")?;
let diff =
get_diff(repo_path, file_path.to_str().unwrap(), false)
.unwrap();
let diff = get_diff(
repo_path,
file_path.to_str().unwrap(),
false,
None,
)
.unwrap();
dbg!(&diff);
assert_eq!(diff.sizes, (1, 2));
@ -546,9 +580,13 @@ mod tests {
File::create(&root.join(file_path))?
.write_all(b"\x00\xc7")?;
let diff =
get_diff(repo_path, file_path.to_str().unwrap(), false)
.unwrap();
let diff = get_diff(
repo_path,
file_path.to_str().unwrap(),
false,
None,
)
.unwrap();
dbg!(&diff);
assert_eq!(diff.sizes, (0, 2));

View file

@ -173,6 +173,7 @@ mod tests {
sub_path.to_str().unwrap(),
file_path.to_str().unwrap(),
false,
None,
)?;
assert!(reset_hunk(

View file

@ -255,10 +255,12 @@ mod tests {
/// helper returning amount of files with changes in the (wd,stage)
pub fn get_statuses(repo_path: &str) -> (usize, usize) {
(
get_status(repo_path, StatusType::WorkingDir)
get_status(repo_path, StatusType::WorkingDir, None)
.unwrap()
.len(),
get_status(repo_path, StatusType::Stage, None)
.unwrap()
.len(),
get_status(repo_path, StatusType::Stage).unwrap().len(),
)
}

View file

@ -1,4 +1,4 @@
use super::diff::{get_diff_raw, HunkHeader};
use super::diff::{get_diff_raw, DiffOptions, HunkHeader};
use crate::error::{Error, Result};
use git2::{Diff, DiffLine, Patch, Repository};
@ -15,7 +15,16 @@ pub(crate) fn get_file_diff_patch_and_hunklines<'a>(
is_staged: bool,
reverse: bool,
) -> Result<(Patch<'a>, Vec<HunkLines<'a>>)> {
let diff = get_diff_raw(repo, file, is_staged, reverse, Some(1))?;
let diff = get_diff_raw(
repo,
file,
is_staged,
reverse,
Some(DiffOptions {
context: 1,
..DiffOptions::default()
}),
)?;
let patches = get_patches(&diff)?;
if patches.len() > 1 {
return Err(Error::Generic(String::from("patch error")));

View file

@ -88,8 +88,8 @@ mod tests {
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
let res =
get_status(repo_path, StatusType::WorkingDir).unwrap();
let res = get_status(repo_path, StatusType::WorkingDir, None)
.unwrap();
assert_eq!(res.len(), 0);
let file_path = root.join("bar.txt");

View file

@ -97,7 +97,7 @@ mod test {
)
.unwrap();
let diff = get_diff(path, "test.txt", true).unwrap();
let diff = get_diff(path, "test.txt", true, None).unwrap();
assert_eq!(diff.lines, 3);
assert_eq!(
@ -139,7 +139,7 @@ c = 4";
)
.unwrap();
let diff = get_diff(path, "test.txt", true).unwrap();
let diff = get_diff(path, "test.txt", true, None).unwrap();
assert_eq!(diff.lines, 5);
assert_eq!(
@ -172,7 +172,8 @@ c = 4";
assert_eq!(get_statuses(path), (0, 1));
let diff_before = get_diff(path, "test.txt", true).unwrap();
let diff_before =
get_diff(path, "test.txt", true, None).unwrap();
assert_eq!(diff_before.lines, 5);
@ -189,7 +190,7 @@ c = 4";
assert_eq!(get_statuses(path), (1, 1));
let diff = get_diff(path, "test.txt", true).unwrap();
let diff = get_diff(path, "test.txt", true, None).unwrap();
assert_eq!(diff.lines, 4);
}

View file

@ -9,6 +9,8 @@ use git2::{Delta, Status, StatusOptions, StatusShow};
use scopetime::scope_time;
use std::path::Path;
use super::ShowUntrackedFilesConfig;
///
#[derive(Copy, Clone, Hash, PartialEq, Debug)]
pub enum StatusItemType {
@ -96,12 +98,17 @@ impl From<StatusType> for StatusShow {
pub fn get_status(
repo_path: &str,
status_type: StatusType,
show_untracked: Option<ShowUntrackedFilesConfig>,
) -> Result<Vec<StatusItem>> {
scope_time!("get_status");
let repo = utils::repo(repo_path)?;
let show_untracked = untracked_files_config_repo(&repo)?;
let show_untracked = if let Some(config) = show_untracked {
config
} else {
untracked_files_config_repo(&repo)?
};
let mut options = StatusOptions::default();
options

View file

@ -278,7 +278,7 @@ mod tests {
let repo_path = root.as_os_str().to_str().unwrap();
let status_count = |s: StatusType| -> usize {
get_status(repo_path, s).unwrap().len()
get_status(repo_path, s, None).unwrap().len()
};
fs::create_dir_all(&root.join("a/d"))?;
@ -329,7 +329,8 @@ mod tests {
assert_eq!(get_statuses(repo_path), (0, 1));
// And that file is test.txt
let diff = get_diff(repo_path, "test.txt", true).unwrap();
let diff =
get_diff(repo_path, "test.txt", true, None).unwrap();
assert_eq!(
diff.hunks[0].lines[0].content,
String::from("@@ -1 +1 @@\n")
@ -371,7 +372,7 @@ mod tests {
let repo_path = root.as_os_str().to_str().unwrap();
let status_count = |s: StatusType| -> usize {
get_status(repo_path, s).unwrap().len()
get_status(repo_path, s, None).unwrap().len()
};
let full_path = &root.join(file_path);
@ -405,7 +406,7 @@ mod tests {
let repo_path = root.as_os_str().to_str().unwrap();
let status_count = |s: StatusType| -> usize {
get_status(repo_path, s).unwrap().len()
get_status(repo_path, s, None).unwrap().len()
};
let sub = &root.join("sub");

View file

@ -2,14 +2,15 @@ use crate::{
accessors,
cmdbar::CommandBar,
components::{
event_pump, BlameFileComponent, BranchListComponent,
CommandBlocking, CommandInfo, CommitComponent, Component,
ConfirmComponent, CreateBranchComponent, DrawableComponent,
event_pump, AppOption, BlameFileComponent,
BranchListComponent, CommandBlocking, CommandInfo,
CommitComponent, Component, ConfirmComponent,
CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, HelpComponent,
InspectCommitComponent, MsgComponent, PullComponent,
PushComponent, PushTagsComponent, RenameBranchComponent,
RevisionFilesPopup, StashMsgComponent, TagCommitComponent,
TagListComponent,
InspectCommitComponent, MsgComponent, OptionsPopupComponent,
PullComponent, PushComponent, PushTagsComponent,
RenameBranchComponent, RevisionFilesPopup, SharedOptions,
StashMsgComponent, TagCommitComponent, TagListComponent,
},
input::{Input, InputEvent, InputState},
keys::{KeyConfig, SharedKeyConfig},
@ -56,6 +57,7 @@ pub struct App {
create_branch_popup: CreateBranchComponent,
rename_branch_popup: RenameBranchComponent,
select_branch_popup: BranchListComponent,
options_popup: OptionsPopupComponent,
tags_popup: TagListComponent,
cmdbar: RefCell<CommandBar>,
tab: usize,
@ -88,6 +90,7 @@ impl App {
let queue = Queue::new();
let theme = Rc::new(theme);
let key_config = Rc::new(key_config);
let options = SharedOptions::default();
Self {
input,
@ -173,6 +176,12 @@ impl App {
theme.clone(),
key_config.clone(),
),
options_popup: OptionsPopupComponent::new(
&queue,
theme.clone(),
key_config.clone(),
options.clone(),
),
do_quit: false,
cmdbar: RefCell::new(CommandBar::new(
theme.clone(),
@ -195,6 +204,7 @@ impl App {
sender,
theme.clone(),
key_config.clone(),
options,
),
stashing_tab: Stashing::new(
sender,
@ -291,6 +301,9 @@ impl App {
} else if k == self.key_config.cmd_bar_toggle {
self.cmdbar.borrow_mut().toggle_more();
NeedsUpdate::empty()
} else if k == self.key_config.open_options {
self.options_popup.show()?;
NeedsUpdate::ALL
} else {
NeedsUpdate::empty()
};
@ -426,6 +439,7 @@ impl App {
select_branch_popup,
revision_files_popup,
tags_popup,
options_popup,
help,
revlog,
status_tab,
@ -453,6 +467,7 @@ impl App {
push_popup,
push_tags_popup,
pull_popup,
options_popup,
reset,
msg
]
@ -665,6 +680,20 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OptionSwitched(o) => {
match o {
AppOption::StatusShowUntracked => {
self.status_tab.update()?;
}
AppOption::DiffContextLines
| AppOption::DiffIgnoreWhitespaces
| AppOption::DiffInterhunkLines => {
self.status_tab.update_diff()?;
}
}
flags.insert(NeedsUpdate::ALL);
}
};
Ok(flags)
@ -784,6 +813,14 @@ impl App {
)
.order(order::NAV),
);
res.push(
CommandInfo::new(
strings::commands::options_popup(&self.key_config),
true,
!self.any_popup_visible(),
)
.order(order::NAV),
);
res.push(
CommandInfo::new(

View file

@ -12,7 +12,7 @@ use crate::{
};
use anyhow::Result;
use asyncgit::{
sync::{CommitId, CommitTags},
sync::{diff::DiffOptions, CommitId, CommitTags},
AsyncDiff, AsyncGitNotification, DiffParams, DiffType,
};
use crossbeam_channel::Sender;
@ -245,6 +245,7 @@ impl InspectCommitComponent {
let diff_params = DiffParams {
path: f.path.clone(),
diff_type: DiffType::Commit(id),
options: DiffOptions::default(),
};
if let Some((params, last)) =

View file

@ -13,6 +13,7 @@ mod filetree;
mod help;
mod inspect_commit;
mod msg;
mod options_popup;
mod pull;
mod push;
mod push_tags;
@ -41,6 +42,9 @@ pub use externaleditor::ExternalEditorComponent;
pub use help::HelpComponent;
pub use inspect_commit::InspectCommitComponent;
pub use msg::MsgComponent;
pub use options_popup::{
AppOption, OptionsPopupComponent, SharedOptions,
};
pub use pull::PullComponent;
pub use push::PushComponent;
pub use push_tags::PushTagsComponent;

View file

@ -0,0 +1,381 @@
#![allow(dead_code)]
use std::{cell::RefCell, rc::Rc};
use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState,
};
use crate::{
components::utils::string_width_align,
keys::SharedKeyConfig,
queue::{InternalEvent, Queue},
strings::{self},
ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::sync::{diff::DiffOptions, ShowUntrackedFilesConfig};
use crossterm::event::Event;
use tui::{
backend::Backend,
layout::{Alignment, Rect},
style::{Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
#[derive(Clone, Copy, PartialEq)]
pub enum AppOption {
StatusShowUntracked,
DiffIgnoreWhitespaces,
DiffContextLines,
DiffInterhunkLines,
}
#[derive(Default, Copy, Clone)]
pub struct Options {
pub status_show_untracked: Option<ShowUntrackedFilesConfig>,
pub diff: DiffOptions,
}
pub type SharedOptions = Rc<RefCell<Options>>;
pub struct OptionsPopupComponent {
selection: AppOption,
queue: Queue,
visible: bool,
key_config: SharedKeyConfig,
options: SharedOptions,
theme: SharedTheme,
}
impl OptionsPopupComponent {
///
pub fn new(
queue: &Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
options: SharedOptions,
) -> Self {
Self {
selection: AppOption::StatusShowUntracked,
queue: queue.clone(),
visible: false,
key_config,
options,
theme,
}
}
fn get_text(&self, width: u16) -> Vec<Spans> {
let mut txt: Vec<Spans> = Vec::with_capacity(10);
self.add_status(&mut txt, width);
txt
}
fn add_status(&self, txt: &mut Vec<Spans>, width: u16) {
Self::add_header(txt, "Status");
self.add_entry(
txt,
width,
"Show untracked",
match self.options.borrow().status_show_untracked {
None => "Gitconfig",
Some(ShowUntrackedFilesConfig::No) => "No",
Some(ShowUntrackedFilesConfig::Normal) => "Normal",
Some(ShowUntrackedFilesConfig::All) => "All",
},
self.is_select(AppOption::StatusShowUntracked),
);
Self::add_header(txt, "");
Self::add_header(txt, "Diff");
self.add_entry(
txt,
width,
"Ignore whitespaces",
&self.options.borrow().diff.ignore_whitespace.to_string(),
self.is_select(AppOption::DiffIgnoreWhitespaces),
);
self.add_entry(
txt,
width,
"Context lines",
&self.options.borrow().diff.context.to_string(),
self.is_select(AppOption::DiffContextLines),
);
self.add_entry(
txt,
width,
"Inter hunk lines",
&self.options.borrow().diff.interhunk_lines.to_string(),
self.is_select(AppOption::DiffInterhunkLines),
);
}
fn is_select(&self, kind: AppOption) -> bool {
self.selection == kind
}
fn add_header(txt: &mut Vec<Spans>, header: &'static str) {
txt.push(Spans::from(vec![Span::styled(
header,
//TODO:
Style::default().add_modifier(Modifier::UNDERLINED),
)]));
}
fn add_entry(
&self,
txt: &mut Vec<Spans>,
width: u16,
entry: &'static str,
value: &str,
selected: bool,
) {
let half = usize::from(width / 2);
txt.push(Spans::from(vec![
Span::styled(
string_width_align(entry, half),
self.theme.text(true, false),
),
Span::styled(
format!("{:^w$}", value, w = half),
self.theme.text(true, selected),
),
]));
}
fn move_selection(&mut self, up: bool) {
if up {
self.selection = match self.selection {
AppOption::StatusShowUntracked => {
AppOption::DiffInterhunkLines
}
AppOption::DiffIgnoreWhitespaces => {
AppOption::StatusShowUntracked
}
AppOption::DiffContextLines => {
AppOption::DiffIgnoreWhitespaces
}
AppOption::DiffInterhunkLines => {
AppOption::DiffContextLines
}
};
} else {
self.selection = match self.selection {
AppOption::StatusShowUntracked => {
AppOption::DiffIgnoreWhitespaces
}
AppOption::DiffIgnoreWhitespaces => {
AppOption::DiffContextLines
}
AppOption::DiffContextLines => {
AppOption::DiffInterhunkLines
}
AppOption::DiffInterhunkLines => {
AppOption::StatusShowUntracked
}
};
}
}
fn switch_option(&mut self, right: bool) {
if right {
match self.selection {
AppOption::StatusShowUntracked => {
let untracked =
self.options.borrow().status_show_untracked;
let untracked = match untracked {
None => {
Some(ShowUntrackedFilesConfig::Normal)
}
Some(ShowUntrackedFilesConfig::Normal) => {
Some(ShowUntrackedFilesConfig::All)
}
Some(ShowUntrackedFilesConfig::All) => {
Some(ShowUntrackedFilesConfig::No)
}
Some(ShowUntrackedFilesConfig::No) => None,
};
self.options.borrow_mut().status_show_untracked =
untracked;
}
AppOption::DiffIgnoreWhitespaces => {
let old =
self.options.borrow().diff.ignore_whitespace;
self.options
.borrow_mut()
.diff
.ignore_whitespace = !old;
}
AppOption::DiffContextLines => {
let old = self.options.borrow().diff.context;
self.options.borrow_mut().diff.context =
old.saturating_add(1);
}
AppOption::DiffInterhunkLines => {
let old =
self.options.borrow().diff.interhunk_lines;
self.options.borrow_mut().diff.interhunk_lines =
old.saturating_add(1);
}
};
} else {
match self.selection {
AppOption::StatusShowUntracked => {
let untracked =
self.options.borrow().status_show_untracked;
let untracked = match untracked {
None => Some(ShowUntrackedFilesConfig::No),
Some(ShowUntrackedFilesConfig::No) => {
Some(ShowUntrackedFilesConfig::All)
}
Some(ShowUntrackedFilesConfig::All) => {
Some(ShowUntrackedFilesConfig::Normal)
}
Some(ShowUntrackedFilesConfig::Normal) => {
None
}
};
self.options.borrow_mut().status_show_untracked =
untracked;
}
AppOption::DiffIgnoreWhitespaces => {
let old =
self.options.borrow().diff.ignore_whitespace;
self.options
.borrow_mut()
.diff
.ignore_whitespace = !old;
}
AppOption::DiffContextLines => {
let old = self.options.borrow().diff.context;
self.options.borrow_mut().diff.context =
old.saturating_sub(1);
}
AppOption::DiffInterhunkLines => {
let old =
self.options.borrow().diff.interhunk_lines;
self.options.borrow_mut().diff.interhunk_lines =
old.saturating_sub(1);
}
};
}
self.queue
.push(InternalEvent::OptionSwitched(self.selection));
}
}
impl DrawableComponent for OptionsPopupComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
if self.is_visible() {
const SIZE: (u16, u16) = (50, 10);
let area =
ui::centered_rect_absolute(SIZE.0, SIZE.1, area);
let width = area.width;
f.render_widget(Clear, area);
f.render_widget(
Paragraph::new(self.get_text(width))
.block(
Block::default()
.borders(Borders::ALL)
.title(Span::styled(
"Options",
self.theme.title(true),
))
.border_style(self.theme.block(true)),
)
.alignment(Alignment::Left),
area,
);
}
Ok(())
}
}
impl Component for OptionsPopupComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.is_visible() || force_all {
out.push(
CommandInfo::new(
strings::commands::close_popup(&self.key_config),
true,
true,
)
.order(1),
);
out.push(
CommandInfo::new(
strings::commands::navigate_tree(
&self.key_config,
),
true,
true,
)
.order(1),
);
}
visibility_blocking(self)
}
fn event(
&mut self,
event: crossterm::event::Event,
) -> Result<EventState> {
if self.is_visible() {
if let Event::Key(key) = &event {
if *key == self.key_config.exit_popup {
self.hide();
} else if *key == self.key_config.move_up {
self.move_selection(true);
} else if *key == self.key_config.move_down {
self.move_selection(false);
} else if *key == self.key_config.move_right {
self.switch_option(true);
} else if *key == self.key_config.move_left {
self.switch_option(false);
}
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View file

@ -39,6 +39,7 @@ pub struct KeyConfig {
pub open_commit: KeyEvent,
pub open_commit_editor: KeyEvent,
pub open_help: KeyEvent,
pub open_options: KeyEvent,
pub move_left: KeyEvent,
pub move_right: KeyEvent,
pub tree_collapse_recursive: KeyEvent,
@ -108,6 +109,7 @@ impl Default for KeyConfig {
open_commit: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()},
open_commit_editor: KeyEvent { code: KeyCode::Char('e'), modifiers:KeyModifiers::CONTROL},
open_help: KeyEvent { code: KeyCode::Char('h'), modifiers: KeyModifiers::empty()},
open_options: KeyEvent { code: KeyCode::Char('o'), modifiers: KeyModifiers::empty()},
move_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()},
move_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()},
tree_collapse_recursive: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::SHIFT},

View file

@ -1,4 +1,4 @@
use crate::tabs::StashingOptions;
use crate::{components::AppOption, tabs::StashingOptions};
use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags};
use bitflags::bitflags;
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
@ -83,6 +83,8 @@ pub enum InternalEvent {
PushTags,
///
OpenFileTree(CommitId),
///
OptionSwitched(AppOption),
}
/// single threaded simple queue for components to communicate with each other

View file

@ -373,6 +373,18 @@ pub mod commands {
CMD_GROUP_GENERAL,
)
}
pub fn options_popup(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Options [{}]",
key_config.get_hint(key_config.open_options),
),
"open options popup",
CMD_GROUP_GENERAL,
)
}
pub fn help_open(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(

View file

@ -74,7 +74,8 @@ impl Stashing {
pub fn update(&mut self) -> Result<()> {
if self.is_visible() {
self.git_status
.fetch(&StatusParams::new(StatusType::Both))?;
//TODO: support options
.fetch(&StatusParams::new(StatusType::Both, None))?;
}
Ok(())

View file

@ -4,7 +4,7 @@ use crate::{
command_pump, event_pump, visibility_blocking,
ChangesComponent, CommandBlocking, CommandInfo, Component,
DiffComponent, DrawableComponent, EventState,
FileTreeItemKind,
FileTreeItemKind, SharedOptions,
},
keys::SharedKeyConfig,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
@ -70,6 +70,7 @@ pub struct Status {
git_branch_name: cached::BranchName,
queue: Queue,
git_action_executed: bool,
options: SharedOptions,
key_config: SharedKeyConfig,
}
@ -134,6 +135,7 @@ impl Status {
sender: &Sender<AsyncGitNotification>,
theme: SharedTheme,
key_config: SharedKeyConfig,
options: SharedOptions,
) -> Self {
Self {
queue: queue.clone(),
@ -169,6 +171,7 @@ impl Status {
git_branch_state: None,
git_branch_name: cached::BranchName::new(CWD),
key_config,
options,
}
}
@ -317,11 +320,17 @@ impl Status {
self.git_branch_name.lookup().map(Some).unwrap_or(None);
if self.is_visible() {
let config = self.options.borrow().status_show_untracked;
self.git_diff.refresh()?;
self.git_status_workdir
.fetch(&StatusParams::new(StatusType::WorkingDir))?;
self.git_status_stage
.fetch(&StatusParams::new(StatusType::Stage))?;
self.git_status_workdir.fetch(&StatusParams::new(
StatusType::WorkingDir,
config,
))?;
self.git_status_stage.fetch(&StatusParams::new(
StatusType::Stage,
config,
))?;
self.branch_compare();
}
@ -394,6 +403,7 @@ impl Status {
let diff_params = DiffParams {
path: path.clone(),
diff_type,
options: self.options.borrow().diff,
};
if self.diff.current() == (path.clone(), is_stage) {
@ -401,18 +411,20 @@ impl Status {
// maybe the diff changed (outside file change)
if let Some((params, last)) = self.git_diff.last()? {
if params == diff_params {
// all params match, so we might need to update
self.diff.update(path, is_stage, last);
} else {
// params changed, we need to request the right diff
self.request_diff(
diff_params,
path,
is_stage,
)?;
}
}
} else {
// we dont show the right diff right now, so we need to request
if let Some(diff) =
self.git_diff.request(diff_params)?
{
self.diff.update(path, is_stage, diff);
} else {
self.diff.clear(true);
}
self.request_diff(diff_params, path, is_stage)?;
}
} else {
self.diff.clear(false);
@ -421,6 +433,21 @@ impl Status {
Ok(())
}
fn request_diff(
&mut self,
diff_params: DiffParams,
path: String,
is_stage: bool,
) -> Result<(), anyhow::Error> {
if let Some(diff) = self.git_diff.request(diff_params)? {
self.diff.update(path, is_stage, diff);
} else {
self.diff.clear(true);
}
Ok(())
}
/// called after confirmation
pub fn reset(&mut self, item: &ResetItem) -> bool {
if let Err(e) = sync::reset_workdir(CWD, item.path.as_str()) {

View file

@ -25,6 +25,7 @@
focus_below: ( code: Char('j'), modifiers: ( bits: 0,),),
open_help: ( code: F(1), modifiers: ( bits: 0,),),
open_options: ( code: Char('o'), modifiers: ( bits: 0,),),
exit: ( code: Char('c'), modifiers: ( bits: 2,),),
quit: ( code: Char('q'), modifiers: ( bits: 0,),),