1use alloc::collections::{BTreeMap, BTreeSet};
4
5use serde_json::Value;
6
7use crate::digest::{Fingerprint, is_binary};
8use crate::errors::{MergeError, Result, list};
9use crate::format::Format;
10use crate::merge::{Driver, Merged, has_markers};
11use crate::name::ScaffoldId;
12use crate::part::{Key, Leaves, Shape, decode, display, encode, merge, overlap};
13use crate::path::RelPath;
14use crate::profile::{OnConflict, Policy};
15use crate::resolve::{Portion, Resolved};
16use crate::survey::{Entry, Survey, released};
17use crate::target::{Scaffold, Target, read_optional};
18use crate::tree::Tree;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum Mode {
23 Apply,
25 Force,
27 Continue,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Action {
34 Keep,
36 Write,
38 Record,
40 Untrack,
43 Remove,
46 Merge,
48 Conflict,
50}
51
52impl Action {
53 #[must_use]
55 pub const fn writes(self) -> bool {
56 matches!(self, Self::Write | Self::Merge | Self::Remove)
57 }
58}
59
60impl Entry {
61 #[must_use]
65 pub fn action(&self, mode: Mode) -> Action {
66 let Some(want) = self.want else {
67 return self.released();
68 };
69 let incoming = want.fingerprint;
70 let restore = mode == Mode::Force && want.policy == Policy::Owned;
71 match (self.found, self.record) {
72 (None, None) => Action::Write,
73 (None, Some(_)) => {
74 if restore {
75 Action::Write
76 } else {
77 Action::Keep
78 }
79 },
80 (Some(found), record) if found.same_content(&incoming) => {
82 if record == Some(incoming) {
83 Action::Keep
84 } else {
85 Action::Record
86 }
87 },
88 (Some(_), None) => {
90 if restore {
91 Action::Write
92 } else {
93 Action::Record
94 }
95 },
96 (Some(found), Some(record)) if found.same_content(&record) => {
98 if want.policy == Policy::Once { Action::Keep } else { Action::Write }
99 },
100 (Some(_), Some(record)) => {
101 if restore {
102 Action::Write
103 } else if want.policy == Policy::Merge && !record.same_content(&incoming) {
104 Action::Merge
105 } else {
106 Action::Keep
107 }
108 },
109 }
110 }
111}
112
113impl Entry {
114 fn released(&self) -> Action {
117 let unchanged = matches!(
118 (self.found, self.record),
119 (Some(found), Some(record)) if found.same_content(&record)
120 );
121 if unchanged && self.recorded != Some(Policy::Once) {
122 Action::Remove
123 } else {
124 Action::Untrack
125 }
126 }
127}
128
129#[derive(Clone, Debug)]
131pub struct Step {
132 pub entry: Entry,
134 pub action: Action,
136 pub note: Option<String>,
138}
139
140#[derive(Debug)]
142pub struct Plan {
143 pub(crate) resolved: Resolved,
145 pub(crate) scaffolds: BTreeMap<ScaffoldId, Scaffold>,
147 pub(crate) steps: Vec<Step>,
149 pub(crate) merged: Tree,
151 pub(crate) gone: BTreeSet<RelPath>,
153 pub(crate) held: bool,
155}
156
157impl Plan {
158 #[must_use]
160 pub const fn resolved(&self) -> &Resolved {
161 &self.resolved
162 }
163
164 #[must_use]
166 pub fn steps(&self) -> &[Step] {
167 &self.steps
168 }
169
170 #[must_use]
172 pub const fn held(&self) -> bool {
173 self.held
174 }
175}
176
177pub fn plan(survey: Survey, mode: Mode, target: &Target) -> Result<Plan> {
192 let Survey { resolved, entries, scaffolds, .. } = survey;
193 let mut unresolved: Vec<RelPath> =
194 entries.iter().filter(|e| e.conflict).map(|e| e.path.clone()).collect();
195 unresolved.dedup();
196 match (mode, unresolved.is_empty()) {
197 (Mode::Continue, true) => return Err(MergeError::NothingToContinue.into()),
198 (Mode::Apply | Mode::Force, false) => {
199 return Err(MergeError::Unresolved { paths: unresolved }.into());
200 },
201 _ => {},
202 }
203 let mut planner = Planner {
204 resolved: &resolved,
205 target,
206 mode,
207 merged: Tree::default(),
208 gone: BTreeSet::new(),
209 };
210 let mut steps = Vec::with_capacity(entries.len());
211 let mut entries = entries.into_iter().peekable();
212 while let Some(first) = entries.next() {
213 let path = first.path.clone();
214 let mut file = vec![first];
215 while let Some(next) = entries.next_if(|next| next.path == path) {
216 file.push(next);
217 }
218 steps.extend(planner.file(file)?);
219 }
220 let Planner { merged, gone, .. } = planner;
221 let conflicted = steps.iter().any(|step| step.action == Action::Conflict);
222 let held = conflicted && resolved.settings().on_conflict == OnConflict::ApplyNone;
223 Ok(Plan { resolved, scaffolds, steps, merged, gone, held })
224}
225
226struct Planner<'a> {
228 resolved: &'a Resolved,
230 target: &'a Target,
232 mode: Mode,
234 merged: Tree,
236 gone: BTreeSet<RelPath>,
238}
239
240struct Decision<'a> {
242 action: Action,
244 content: Option<Vec<u8>>,
246 note: Option<String>,
248 clash: Option<Clash<'a>>,
250}
251
252impl Decision<'_> {
253 const fn of(action: Action, content: Option<Vec<u8>>, note: Option<String>) -> Self {
255 Self { action, content, note, clash: None }
256 }
257}
258
259struct Clash<'a> {
261 portion: &'a Portion,
263 keys: BTreeSet<Key>,
265 base: Leaves,
267 theirs: Leaves,
269}
270
271impl<'a> Planner<'a> {
272 fn file(&mut self, entries: Vec<Entry>) -> Result<Vec<Step>> {
274 if entries.first().is_some_and(|entry| entry.conflict) {
275 return self.resolution(entries);
276 }
277 let provided = |entry: &Entry| entry.want.is_some();
278 let parted = entries.iter().any(|entry| entry.part.is_some() && provided(entry));
279 let owner = entries.iter().any(|entry| {
282 entry.part.is_none() && entry.want.is_some_and(|want| want.policy != Policy::Once)
283 });
284 let kept = entries.iter().any(|entry| entry.part.is_none() && provided(entry));
286 let mut steps = Vec::with_capacity(entries.len());
287 let mut parts = Vec::new();
288 let mut start = None;
289 for entry in entries {
290 match (&entry.part, entry.want) {
291 (None, Some(_)) if parted => {
292 let action = entry.action(self.mode);
293 if action == Action::Write {
294 start = Some(self.resolved.payload(&entry)?.to_vec());
295 }
296 steps.push(Step { entry, action, note: None });
297 },
298 (None, None) if parted => {
299 let action = entry.action(self.mode);
301 let action = if action == Action::Remove { Action::Untrack } else { action };
302 steps.push(Step { entry, action, note: None });
303 },
304 (None, _) => steps.push(self.whole(entry)?),
305 (Some(_), None) if owner => {
306 steps.push(Step { entry, action: Action::Untrack, note: None });
307 },
308 (Some(_), _) => parts.push(entry),
309 }
310 }
311 if !parts.is_empty() {
312 steps.extend(self.parts(parts, start, kept)?);
313 steps.sort_by(|a, b| a.entry.part.cmp(&b.entry.part));
314 }
315 if steps.iter().any(|step| step.action == Action::Conflict) {
317 for step in
318 steps.iter_mut().filter(|s| s.entry.part.is_none() && s.action == Action::Write)
319 {
320 step.action = Action::Conflict;
321 step.note = Some("waits for the conflict in another part of the file".to_owned());
322 }
323 }
324 Ok(steps)
325 }
326
327 fn whole(&mut self, entry: Entry) -> Result<Step> {
329 let (action, note) = match (entry.action(self.mode), entry.want, entry.record) {
330 (Action::Merge, Some(want), Some(record)) => {
331 let base = self.target.base(&record)?;
332 let ours = fs_err::read(entry.path.under(self.target.root()))?;
333 let theirs = self.resolved.payload(&entry)?;
334 let Merged { bytes, conflict } =
335 self.merge(&entry.path, base.as_deref(), &ours, theirs)?;
336 let conflict = conflict.or_else(|| {
337 want.validate
338 .check(&bytes)
339 .err()
340 .map(|why| format!("merged, but invalid: {why}"))
341 });
342 self.merged.put(entry.path.clone(), &bytes);
343 (if conflict.is_some() { Action::Conflict } else { Action::Merge }, conflict)
344 },
345 (Action::Remove, ..) => {
346 self.gone.insert(entry.path.clone());
347 (Action::Remove, None)
348 },
349 (action, ..) => (action, None),
350 };
351 Ok(Step { entry, action, note })
352 }
353
354 fn merge(
356 &self, path: &RelPath, base: Option<&[u8]>, ours: &[u8], theirs: &[u8],
357 ) -> Result<Merged> {
358 let driver = &self.resolved.settings().driver;
359 let root = self.target.root();
360 let Merged { bytes, conflict } =
361 driver.merge(root, path, base.unwrap_or_default(), ours, theirs)?;
362 let lost = base.is_none() && !is_binary(ours) && !is_binary(theirs);
365 let conflict = if lost {
366 Some("its recorded base is missing, so it merged without one".to_owned())
367 } else {
368 conflict
369 };
370 Ok(Merged { bytes, conflict })
371 }
372
373 fn parts(
380 &mut self, entries: Vec<Entry>, start: Option<Vec<u8>>, kept: bool,
381 ) -> Result<Vec<Step>> {
382 let resolved = self.resolved;
383 let Some(path) = entries.first().map(|entry| entry.path.clone()) else {
384 return Ok(Vec::new());
385 };
386 let label = path.as_str();
387 let found = read_optional(&path.under(self.target.root()))?;
388 let starter = || {
389 let mut own = entries.iter().filter(|entry| entry.want.is_some());
390 own.find_map(|entry| resolved.portion(entry)?.starter.clone())
391 };
392 let recreated = found.is_none() && start.is_some();
393 let disk = found.unwrap_or_else(|| start.or_else(starter).unwrap_or_default());
394 let mut file = disk.clone();
395 let mut steps = Vec::with_capacity(entries.len());
396 let mut written: Vec<(usize, &Shape, Vec<u8>)> = Vec::new();
397 let mut removed: Vec<(usize, Shape, BTreeSet<Key>)> = Vec::new();
398 let mut clashes = Vec::new();
399 let claimed: Vec<Key> = entries
401 .iter()
402 .filter(|entry| entry.want.is_some())
403 .flat_map(|entry| entry.keys.iter().cloned())
404 .collect();
405 for entry in entries {
406 let Some(portion) = entry.want.and_then(|_| resolved.portion(&entry)) else {
407 let taken = self.take_out(&mut file, &entry, &claimed, label)?;
408 let action = if taken.is_some() { Action::Remove } else { Action::Untrack };
409 removed.extend(taken.map(|(shape, keys)| (steps.len(), shape, keys)));
410 steps.push(Step { entry, action, note: None });
411 continue;
412 };
413 let decision = if recreated && entry.action(self.mode) == Action::Keep {
414 Decision::of(Action::Write, Some(resolved.payload(&entry)?.to_vec()), None)
415 } else {
416 self.decide(&entry, portion, &disk)?
417 };
418 let Decision { action, content, note, clash } = decision;
419 clashes.extend(clash);
420 if let Some(content) = content {
421 file = portion.splice(&file, &content, &entry.keys, label)?;
422 written.push((steps.len(), &portion.shape, content));
423 }
424 steps.push(Step { entry, action, note });
425 }
426 if written.is_empty() && removed.is_empty() {
427 return Ok(steps);
428 }
429 let marked = steps.iter().any(|step| step.action == Action::Conflict);
431 let trouble = if marked { None } else { check(&file, &steps, &written, &removed, label)? };
432 let conflicted = marked || trouble.is_some();
433 if !conflicted {
434 if !removed.is_empty() && !kept && file.trim_ascii().is_empty() {
436 self.gone.insert(path);
437 } else {
438 self.merged.put(path, &file);
439 }
440 return Ok(steps);
441 }
442 for step in &mut steps {
443 if step.action.writes() {
444 step.action = Action::Conflict;
445 step.note = trouble.clone().or_else(|| {
446 Some("waits for the conflict in another part of the file".to_owned())
447 });
448 }
449 }
450 let sidecar = self.mark(&path, &file, &clashes)?;
451 self.merged.put(path, &sidecar);
452 Ok(steps)
453 }
454
455 fn decide(&self, entry: &Entry, portion: &'a Portion, disk: &[u8]) -> Result<Decision<'a>> {
457 let (shape, theirs) = (&portion.shape, self.resolved.payload(entry)?);
458 let label = entry.path.as_str();
459 Ok(match (entry.action(self.mode), entry.record, shape) {
460 (Action::Write, ..) => Decision::of(Action::Write, Some(theirs.to_vec()), None),
461 (Action::Merge, Some(record), Shape::Keys(_)) => {
462 let base = self.target.base(&record)?;
463 let base = base.as_deref().and_then(decode).unwrap_or_default();
464 let ours = shape.view(disk, &entry.keys, label)?;
465 let ours = ours.as_deref().and_then(decode).unwrap_or_default();
466 let theirs = decode(theirs).unwrap_or_default();
467 let merged = merge(&base, &ours, &theirs, &entry.keys);
468 let content = Some(encode(&merged.leaves));
469 if merged.conflicts.is_empty() {
470 return Ok(Decision::of(Action::Merge, content, None));
471 }
472 let note = format!("both changed {}", keys(&merged.conflicts));
473 let at = |side: &Leaves| {
474 let conflicts = &merged.conflicts;
475 side.iter().filter(|(k, _)| conflicts.contains(*k)).map(clone).collect()
476 };
477 let (base, theirs) = (at(&base), at(&theirs));
478 let clash = Clash { portion, keys: merged.conflicts, base, theirs };
479 Decision { action: Action::Conflict, content, note: Some(note), clash: Some(clash) }
480 },
481 (Action::Merge, Some(record), Shape::Block(_)) => {
482 let base = self.target.base(&record)?;
483 let ours = shape.view(disk, &entry.keys, label)?.unwrap_or_default();
484 let Merged { bytes, conflict } =
485 self.merge(&entry.path, base.as_deref(), &ours, theirs)?;
486 let action = if conflict.is_some() { Action::Conflict } else { Action::Merge };
487 Decision::of(action, Some(bytes), conflict)
488 },
489 (Action::Record, None, Shape::Keys(_)) => {
492 let ours = shape.view(disk, &entry.keys, label)?;
493 let ours = ours.as_deref().and_then(decode).unwrap_or_default();
494 let mut filled = decode(theirs).unwrap_or_default();
495 filled.extend(ours.iter().map(clone));
496 if filled == ours {
497 Decision::of(Action::Record, None, None)
498 } else {
499 Decision::of(Action::Merge, Some(encode(&filled)), None)
500 }
501 },
502 (action, ..) => Decision::of(action, None, None),
503 })
504 }
505
506 fn take_out(
510 &self, file: &mut Vec<u8>, entry: &Entry, claimed: &[Key], label: &str,
511 ) -> Result<Option<(Shape, BTreeSet<Key>)>> {
512 if entry.action(self.mode) != Action::Remove {
513 return Ok(None);
514 }
515 let shape = self.resolved.portion(entry).map(|portion| portion.shape.clone());
517 let Some(shape) = shape.or_else(|| released(self.resolved, self.target, entry)) else {
518 return Ok(None);
519 };
520 let mut keys = entry.keys.clone();
521 keys.retain(|key| !claimed.iter().any(|kept| overlap(key, kept)));
522 *file = shape.remove(file, &keys, label)?;
523 Ok(Some((shape, keys)))
524 }
525
526 fn mark(&self, path: &RelPath, file: &[u8], clashes: &[Clash<'_>]) -> Result<Vec<u8>> {
529 if clashes.is_empty() {
530 return Ok(file.to_vec());
531 }
532 let label = path.as_str();
533 let (mut base, mut theirs) = (file.to_vec(), file.to_vec());
534 for clash in clashes {
535 base = clash.portion.splice(&base, &encode(&clash.base), &clash.keys, label)?;
536 theirs = clash.portion.splice(&theirs, &encode(&clash.theirs), &clash.keys, label)?;
537 }
538 let root = self.target.root();
539 Ok(Driver::Builtin.merge(root, path, &base, file, &theirs)?.bytes)
540 }
541
542 fn resolution(&mut self, entries: Vec<Entry>) -> Result<Vec<Step>> {
547 let path = entries.iter().find(|entry| entry.want.is_some()).map(|e| e.path.clone());
548 if let Some(path) = path {
549 let bytes = fs_err::read(self.target.sidecar(&path))?;
550 if has_markers(&bytes) {
551 return Err(MergeError::Unmerged { path }.into());
552 }
553 for want in entries.iter().filter_map(|entry| entry.want) {
554 want.validate
555 .check(&bytes)
556 .map_err(|reason| MergeError::Invalid { path: path.clone(), reason })?;
557 }
558 self.merged.put(path, &bytes);
559 }
560 let mode = self.mode;
561 let step = |entry: Entry| {
562 let action = match (entry.want, &entry.part) {
563 (None, _) => Action::Untrack,
564 (Some(_), None) => Action::Merge,
565 (Some(_), Some(_)) => match entry.action(mode) {
566 action if action.writes() => Action::Merge,
567 action => action,
568 },
569 };
570 Step { entry, action, note: None }
571 };
572 Ok(entries.into_iter().map(step).collect())
573 }
574}
575
576fn check(
579 file: &[u8], steps: &[Step], written: &[(usize, &Shape, Vec<u8>)],
580 removed: &[(usize, Shape, BTreeSet<Key>)], label: &str,
581) -> Result<Option<String>> {
582 let mut formats: Vec<Format> =
583 steps.iter().filter_map(|s| s.entry.want).map(|w| w.validate).collect();
584 formats.dedup();
585 for format in formats {
586 if let Err(why) = format.check(file) {
587 return Ok(Some(format!("with it, the file is invalid: {why}")));
588 }
589 }
590 for (index, shape, content) in written {
591 let Some(step) = steps.get(*index) else {
592 continue;
593 };
594 let back = shape.view(file, &step.entry.keys, label)?.unwrap_or_default();
595 if !Fingerprint::of(&back).same_content(&Fingerprint::of(content)) {
596 return Ok(Some("it reads back other than devset wrote it".to_owned()));
597 }
598 }
599 for (_, shape, keys) in removed {
600 let left = shape.view(file, keys, label)?;
601 if left.is_some_and(|left| decode(&left).is_none_or(|leaves| !leaves.is_empty())) {
602 return Ok(Some("a part devset removed is still in it".to_owned()));
603 }
604 }
605 Ok(None)
606}
607
608fn clone((key, value): (&Key, &Value)) -> (Key, Value) {
610 (key.clone(), value.clone())
611}
612
613fn keys(keys: &BTreeSet<Key>) -> String {
615 const NAMED: usize = 3;
616 let named = keys.iter().take(NAMED).map(|key| format!("`{}`", display(key)));
617 let rest = keys.len().saturating_sub(NAMED);
618 list(named.chain((rest > 0).then(|| format!("{rest} more"))))
619}
620
621#[cfg(test)]
622mod tests {
623 use alloc::collections::BTreeSet;
624
625 use super::{Action, Mode};
626 use crate::digest::Fingerprint;
627 use crate::format::Format;
628 use crate::path::RelPath;
629 use crate::profile::Policy;
630 use crate::survey::{Entry, Want};
631
632 fn fp(name: &str) -> Fingerprint {
637 Fingerprint::of(match name {
638 "p" => b"profile\n",
639 "p~" => b"profile \r\n\n",
640 "o" => b"old\n",
641 _ => b"edited\n",
642 })
643 }
644
645 fn entry(policy: Policy, found: Option<&str>, record: Option<&str>) -> Entry {
646 Entry {
647 path: RelPath::new("f").unwrap(),
648 part: None,
649 want: Some(Want {
650 layer: 0,
651 policy,
652 validate: Format::None,
653 executable: false,
654 fingerprint: fp("p"),
655 }),
656 record: record.map(fp),
657 recorded: record.map(|_| policy),
658 found: found.map(fp),
659 conflict: false,
660 gate: None,
661 present: found.is_some(),
662 keys: BTreeSet::new(),
663 }
664 }
665
666 #[test]
667 fn decision_table() {
668 use Action::{Keep, Merge, Record, Write};
669 use Policy::{Merge as M, Once, Owned};
670 #[rustfmt::skip]
671 let table = [
672 (Owned, None, None, Write, Write), (Owned, None, Some("p"), Keep, Write), (Owned, Some("p"), Some("p"), Keep, Keep), (Owned, Some("p~"), Some("p"), Keep, Keep), (Owned, Some("p~"), Some("o"), Record, Record), (Owned, Some("x"), None, Record, Write), (Owned, Some("o"), Some("o"), Write, Write), (Owned, Some("x"), Some("p"), Keep, Write), (Once, None, None, Write, Write),
682 (Once, None, Some("p"), Keep, Keep),
683 (Once, Some("x"), None, Record, Record),
684 (Once, Some("o"), Some("o"), Keep, Keep),
685 (Once, Some("x"), Some("p"), Keep, Keep),
686 (M, Some("x"), Some("o"), Merge, Merge),
687 (M, Some("x"), Some("p"), Keep, Keep),
688 (M, Some("o"), Some("o"), Write, Write),
689 ];
690 for (policy, found, record, apply, force) in table {
691 let entry = entry(policy, found, record);
692 let case = format!("{policy:?} found={found:?} record={record:?}");
693 assert_eq!(entry.action(Mode::Apply), apply, "apply: {case}");
694 assert_eq!(entry.action(Mode::Force), force, "force: {case}");
695 }
696 }
697
698 #[test]
699 fn a_dropped_file_goes_unless_edited_or_the_targets() {
700 use Action::{Remove, Untrack};
701 use Policy::{Merge as M, Once, Owned};
702 #[rustfmt::skip]
703 let table = [
704 (Owned, Some("p"), Some("p"), Remove), (Owned, Some("p~"), Some("p"), Remove), (M, Some("p"), Some("p"), Remove),
708 (Owned, Some("x"), Some("p"), Untrack), (Owned, None, Some("p"), Untrack), (Once, Some("p"), Some("p"), Untrack), ];
712 for (policy, found, record, action) in table {
713 let entry = Entry { want: None, ..entry(policy, found, record) };
714 let case = format!("{policy:?} found={found:?} record={record:?}");
715 assert_eq!(entry.action(Mode::Apply), action, "apply: {case}");
716 assert_eq!(entry.action(Mode::Force), action, "force: {case}");
717 }
718 }
719}