Skip to main content

devset_core/
settings.rs

1//! Settings a profile defaults and a target overrides.
2
3use derive_more::Display;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::errors::{ProfileError, Result};
8use crate::merge::Driver;
9use crate::resolve::Layer;
10
11/// A `[merge]` table: defaults in `profile.toml`, decisions in `.devset/config.toml`.
12#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields, rename_all = "kebab-case")]
14pub struct MergeSpec {
15    /// What an update does when a file conflicts.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub on_conflict: Option<OnConflict>,
18    /// A merge program, with `%O %A %B %P` substituted.
19    ///
20    /// From a profile it is only a suggestion: devset never runs a program a profile names.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    #[schemars(with = "Option<String>")]
23    pub driver: Option<Driver>,
24}
25
26/// What an update does when a file conflicts.
27#[derive(
28    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Display,
29)]
30#[serde(rename_all = "kebab-case")]
31#[display(rename_all = "kebab-case")]
32pub enum OnConflict {
33    /// Write every other file and advance the lock; conflicts wait in `.devset/conflicts/`.
34    #[default]
35    ApplyOthers,
36    /// Write nothing but the conflicts until every one is resolved.
37    ApplyNone,
38}
39
40/// The settings in force: devset's defaults, then each layer's, then the target's.
41#[derive(Clone, Debug, Default)]
42pub struct Settings {
43    /// What an update does when a file conflicts.
44    pub on_conflict: OnConflict,
45    /// How files are merged.
46    pub driver: Driver,
47}
48
49/// A merge driver a layer suggests and the target has not configured.
50#[derive(Clone, Debug)]
51pub struct Suggestion {
52    /// The layer's profile name.
53    pub layer: String,
54    /// The driver it names.
55    pub driver: Driver,
56}
57
58impl Settings {
59    /// The settings `target` decides, falling back to what `layers` agree on.
60    pub(crate) fn compose(layers: &[Layer], target: &MergeSpec) -> Result<Self> {
61        let on_conflict = match target.on_conflict {
62            Some(value) => value,
63            None => {
64                agreed(layers, "merge.on-conflict", |merge| merge.on_conflict)?.unwrap_or_default()
65            },
66        };
67        let driver = target.driver.clone().unwrap_or_default();
68        Ok(Self { on_conflict, driver })
69    }
70
71    /// The drivers `layers` suggest, while `target` configures none.
72    pub(crate) fn suggestions(layers: &[Layer], target: &MergeSpec) -> Vec<Suggestion> {
73        if target.driver.is_some() {
74            return Vec::new();
75        }
76        layers
77            .iter()
78            .filter_map(|layer| {
79                let driver = layer.merge().driver.clone()?;
80                Some(Suggestion { layer: layer.meta().name.clone(), driver })
81            })
82            .collect()
83    }
84}
85
86/// The value every layer that sets `key` agrees on.
87fn agreed<T: Copy + PartialEq>(
88    layers: &[Layer], key: &'static str, get: fn(&MergeSpec) -> Option<T>,
89) -> Result<Option<T>> {
90    let mut agreed: Option<(T, &Layer)> = None;
91    for layer in layers {
92        let Some(value) = get(layer.merge()) else {
93            continue;
94        };
95        match agreed {
96            Some((first, by)) if first != value => {
97                let (first, second) = (by.meta().name.clone(), layer.meta().name.clone());
98                return Err(ProfileError::Setting { key: key.to_owned(), first, second }.into());
99            },
100            Some(_) => {},
101            None => agreed = Some((value, layer)),
102        }
103    }
104    Ok(agreed.map(|(value, _)| value))
105}