to_record.rs
//! Selection of changes to include or exclude in the next record.
/// Selection can be picked at 3 levels (order matters, from more coarse to
/// more fine):
///
/// - overall
/// - per file
/// - per change in a file
///
/// To determine a pick state for anything from any of these levels, the
/// high-levels (first in the list above) take precedence over the more
/// fine-grained levels. This is so that fine-grained partial selection is
/// preserved even if a state at higher-level is changed to be all included
/// or excluded so that it's possible to return back to the partial
/// selection.
use libpijul::HashMap;
use crate::{diff, file};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pick {
Include,
Exclude,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PickSet {
#[default]
Include,
Exclude,
Partial,
}
#[derive(Clone, Debug, Default)]
pub struct State {
/// State of all files
pub overall: PickSet,
/// Map from file name to its changes state. This is preserved even when
/// `overall` selection changes.
pub files: HashMap<file::Path, PickSet>,
/// Map from file name to its individual change state. This is preserved
/// even when `overall` and `files` selection changes
pub changes: HashMap<file::Path, PartialFile>,
}
#[derive(Clone, Debug, Default)]
pub struct PartialFile {
/// Map from file name to its state
// NOTE: When it's possible to select individual lines/chars to record,
// then the value would be `PickSet` and the inner-most value of
// selected ranges would be `Pick`
pub changes: HashMap<diff::IdHash, Pick>,
}
/// Determine the default value for a file that is not explicitly set.
pub fn default_pick_set(state: &State) -> PickSet {
match default_pick(state) {
Pick::Include => PickSet::Include,
Pick::Exclude => PickSet::Exclude,
}
}
/// Determine the default value for a change that is not explicitly set.
pub fn default_pick(state: &State) -> Pick {
let State {
overall,
files,
changes: _,
} = state;
if matches!(overall, PickSet::Partial)
|| files
.values()
.any(|pick| matches!(pick, PickSet::Partial | PickSet::Exclude))
{
Pick::Exclude
} else {
Pick::Include
}
}
pub fn determine_file(state: &State, file: &file::Path) -> PickSet {
match state.overall {
PickSet::Include => PickSet::Include,
PickSet::Exclude => PickSet::Exclude,
PickSet::Partial => {
if let Some(pick) = state.files.get(file) {
*pick
} else if file_has_any_partial_change(file, &state.changes) {
PickSet::Partial
} else {
default_pick_set(state)
}
}
}
}
pub fn determine_change(
file: &file::Path,
diff_id: diff::IdHash,
state: &State,
) -> Pick {
match state.overall {
PickSet::Include => return Pick::Include,
PickSet::Exclude => return Pick::Exclude,
PickSet::Partial => {}
}
if let Some(pick) = state.files.get(file) {
match pick {
PickSet::Include => return Pick::Include,
PickSet::Exclude => return Pick::Exclude,
PickSet::Partial => {}
}
}
let explicit = state
.changes
.get(file)
.and_then(|file| file.changes.get(&diff_id))
.copied();
explicit.unwrap_or_else(|| default_pick(state))
}
pub fn file_has_any_partial_change(
file: &file::Path,
changes: &HashMap<file::Path, PartialFile>,
) -> bool {
let (mut has_include, mut has_exclude) = (false, false);
changes
.get(file)
.map(|file| {
// We need to have both exclude and include to be partial
file.changes.values().any(|pick| {
has_exclude |= matches!(pick, Pick::Exclude);
has_include |= matches!(pick, Pick::Include);
has_include && has_exclude
})
})
.unwrap_or_default()
}