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, mut entries: Vec<Entry>, start: Option<Vec<u8>>, kept: bool,
381 ) -> Result<Vec<Step>> {
382 let resolved = self.resolved;
383 let provided = entries
386 .iter()
387 .map(|entry| match entry.want.and_then(|_| resolved.portion(entry)) {
388 Some(portion) => Ok(portion.shape.keys(resolved.payload(entry)?)),
389 None => Ok(BTreeSet::new()),
390 })
391 .collect::<Result<Vec<_>>>()?;
392 for (index, (entry, own)) in entries.iter_mut().zip(&provided).enumerate() {
393 let theirs: Vec<&Key> = provided
394 .iter()
395 .enumerate()
396 .filter(|(other, _)| *other != index)
397 .flat_map(|(_, keys)| keys)
398 .collect();
399 entry.keys.retain(|key| own.contains(key) || !theirs.iter().any(|k| overlap(key, k)));
400 }
401 let Some(path) = entries.first().map(|entry| entry.path.clone()) else {
402 return Ok(Vec::new());
403 };
404 let label = path.as_str();
405 let found = read_optional(&path.under(self.target.root()))?;
406 let starter = || {
407 let mut own = entries.iter().filter(|entry| entry.want.is_some());
408 own.find_map(|entry| resolved.portion(entry)?.starter.clone())
409 };
410 let recreated = found.is_none() && start.is_some();
411 let disk = found.unwrap_or_else(|| start.or_else(starter).unwrap_or_default());
412 let mut file = disk.clone();
413 let mut steps = Vec::with_capacity(entries.len());
414 let mut written: Vec<(usize, &Shape, Vec<u8>)> = Vec::new();
415 let mut removed: Vec<(usize, Shape, BTreeSet<Key>)> = Vec::new();
416 let mut clashes = Vec::new();
417 let claimed: Vec<Key> = entries
419 .iter()
420 .filter(|entry| entry.want.is_some())
421 .flat_map(|entry| entry.keys.iter().cloned())
422 .collect();
423 for entry in entries {
424 let Some(portion) = entry.want.and_then(|_| resolved.portion(&entry)) else {
425 let taken = self.take_out(&mut file, &entry, &claimed, label)?;
426 let action = if taken.is_some() { Action::Remove } else { Action::Untrack };
427 removed.extend(taken.map(|(shape, keys)| (steps.len(), shape, keys)));
428 steps.push(Step { entry, action, note: None });
429 continue;
430 };
431 let decision = if recreated && entry.action(self.mode) == Action::Keep {
432 Decision::of(Action::Write, Some(resolved.payload(&entry)?.to_vec()), None)
433 } else {
434 self.decide(&entry, portion, &disk)?
435 };
436 let Decision { action, content, note, clash } = decision;
437 clashes.extend(clash);
438 if let Some(content) = content {
439 file = portion.splice(&file, &content, &entry.keys, label)?;
440 written.push((steps.len(), &portion.shape, content));
441 }
442 steps.push(Step { entry, action, note });
443 }
444 if written.is_empty() && removed.is_empty() {
445 return Ok(steps);
446 }
447 let marked = steps.iter().any(|step| step.action == Action::Conflict);
449 let trouble = if marked { None } else { check(&file, &steps, &written, &removed, label)? };
450 let conflicted = marked || trouble.is_some();
451 if !conflicted {
452 if !removed.is_empty() && !kept && file.trim_ascii().is_empty() {
454 self.gone.insert(path);
455 } else {
456 self.merged.put(path, &file);
457 }
458 return Ok(steps);
459 }
460 for step in &mut steps {
461 if step.action.writes() {
462 step.action = Action::Conflict;
463 step.note = trouble.clone().or_else(|| {
464 Some("waits for the conflict in another part of the file".to_owned())
465 });
466 }
467 }
468 let sidecar = self.mark(&path, &file, &clashes)?;
469 self.merged.put(path, &sidecar);
470 Ok(steps)
471 }
472
473 fn decide(&self, entry: &Entry, portion: &'a Portion, disk: &[u8]) -> Result<Decision<'a>> {
475 let (shape, theirs) = (&portion.shape, self.resolved.payload(entry)?);
476 let label = entry.path.as_str();
477 Ok(match (entry.action(self.mode), entry.record, shape) {
478 (Action::Write, ..) => Decision::of(Action::Write, Some(theirs.to_vec()), None),
479 (Action::Merge, Some(record), Shape::Keys(_)) => {
480 let base = self.target.base(&record)?;
481 let base = base.as_deref().and_then(decode).unwrap_or_default();
482 let ours = shape.view(disk, &entry.keys, label)?;
483 let ours = ours.as_deref().and_then(decode).unwrap_or_default();
484 let theirs = decode(theirs).unwrap_or_default();
485 let merged = merge(&base, &ours, &theirs, &entry.keys);
486 let content = Some(encode(&merged.leaves));
487 if merged.conflicts.is_empty() {
488 return Ok(Decision::of(Action::Merge, content, None));
489 }
490 let note = format!("both changed {}", keys(&merged.conflicts));
491 let at = |side: &Leaves| {
492 let conflicts = &merged.conflicts;
493 side.iter().filter(|(k, _)| conflicts.contains(*k)).map(clone).collect()
494 };
495 let (base, theirs) = (at(&base), at(&theirs));
496 let clash = Clash { portion, keys: merged.conflicts, base, theirs };
497 Decision { action: Action::Conflict, content, note: Some(note), clash: Some(clash) }
498 },
499 (Action::Merge, Some(record), Shape::Block(_)) => {
500 let base = self.target.base(&record)?;
501 let ours = shape.view(disk, &entry.keys, label)?.unwrap_or_default();
502 let Merged { bytes, conflict } =
503 self.merge(&entry.path, base.as_deref(), &ours, theirs)?;
504 let action = if conflict.is_some() { Action::Conflict } else { Action::Merge };
505 Decision::of(action, Some(bytes), conflict)
506 },
507 (Action::Record, None, Shape::Keys(_)) => {
510 let ours = shape.view(disk, &entry.keys, label)?;
511 let ours = ours.as_deref().and_then(decode).unwrap_or_default();
512 let mut filled = decode(theirs).unwrap_or_default();
513 filled.extend(ours.iter().map(clone));
514 if filled == ours {
515 Decision::of(Action::Record, None, None)
516 } else {
517 Decision::of(Action::Merge, Some(encode(&filled)), None)
518 }
519 },
520 (action, ..) => Decision::of(action, None, None),
521 })
522 }
523
524 fn take_out(
528 &self, file: &mut Vec<u8>, entry: &Entry, claimed: &[Key], label: &str,
529 ) -> Result<Option<(Shape, BTreeSet<Key>)>> {
530 if entry.action(self.mode) != Action::Remove {
531 return Ok(None);
532 }
533 let shape = self.resolved.portion(entry).map(|portion| portion.shape.clone());
535 let Some(shape) = shape.or_else(|| released(self.resolved, self.target, entry)) else {
536 return Ok(None);
537 };
538 let mut keys = entry.keys.clone();
539 keys.retain(|key| !claimed.iter().any(|kept| overlap(key, kept)));
540 *file = shape.remove(file, &keys, label)?;
541 Ok(Some((shape, keys)))
542 }
543
544 fn mark(&self, path: &RelPath, file: &[u8], clashes: &[Clash<'_>]) -> Result<Vec<u8>> {
547 if clashes.is_empty() {
548 return Ok(file.to_vec());
549 }
550 let label = path.as_str();
551 let (mut base, mut theirs) = (file.to_vec(), file.to_vec());
552 for clash in clashes {
553 base = clash.portion.splice(&base, &encode(&clash.base), &clash.keys, label)?;
554 theirs = clash.portion.splice(&theirs, &encode(&clash.theirs), &clash.keys, label)?;
555 }
556 let root = self.target.root();
557 Ok(Driver::Builtin.merge(root, path, &base, file, &theirs)?.bytes)
558 }
559
560 fn resolution(&mut self, entries: Vec<Entry>) -> Result<Vec<Step>> {
565 let path = entries.iter().find(|entry| entry.want.is_some()).map(|e| e.path.clone());
566 if let Some(path) = path {
567 let bytes = fs_err::read(self.target.sidecar(&path))?;
568 if has_markers(&bytes) {
569 return Err(MergeError::Unmerged { path }.into());
570 }
571 for want in entries.iter().filter_map(|entry| entry.want) {
572 want.validate
573 .check(&bytes)
574 .map_err(|reason| MergeError::Invalid { path: path.clone(), reason })?;
575 }
576 self.merged.put(path, &bytes);
577 }
578 let mode = self.mode;
579 let step = |entry: Entry| {
580 let action = match (entry.want, &entry.part) {
581 (None, _) => Action::Untrack,
582 (Some(_), None) => Action::Merge,
583 (Some(_), Some(_)) => match entry.action(mode) {
584 action if action.writes() => Action::Merge,
585 action => action,
586 },
587 };
588 Step { entry, action, note: None }
589 };
590 Ok(entries.into_iter().map(step).collect())
591 }
592}
593
594fn check(
597 file: &[u8], steps: &[Step], written: &[(usize, &Shape, Vec<u8>)],
598 removed: &[(usize, Shape, BTreeSet<Key>)], label: &str,
599) -> Result<Option<String>> {
600 let mut formats: Vec<Format> =
601 steps.iter().filter_map(|s| s.entry.want).map(|w| w.validate).collect();
602 formats.dedup();
603 for format in formats {
604 if let Err(why) = format.check(file) {
605 return Ok(Some(format!("with it, the file is invalid: {why}")));
606 }
607 }
608 for (index, shape, content) in written {
609 let Some(step) = steps.get(*index) else {
610 continue;
611 };
612 let back = shape.view(file, &step.entry.keys, label)?.unwrap_or_default();
613 if !Fingerprint::of(&back).same_content(&Fingerprint::of(content)) {
614 return Ok(Some("it reads back other than devset wrote it".to_owned()));
615 }
616 }
617 for (_, shape, keys) in removed {
618 let left = shape.view(file, keys, label)?;
619 if left.is_some_and(|left| decode(&left).is_none_or(|leaves| !leaves.is_empty())) {
620 return Ok(Some("a part devset removed is still in it".to_owned()));
621 }
622 }
623 Ok(None)
624}
625
626fn clone((key, value): (&Key, &Value)) -> (Key, Value) {
628 (key.clone(), value.clone())
629}
630
631fn keys(keys: &BTreeSet<Key>) -> String {
633 const NAMED: usize = 3;
634 let named = keys.iter().take(NAMED).map(|key| format!("`{}`", display(key)));
635 let rest = keys.len().saturating_sub(NAMED);
636 list(named.chain((rest > 0).then(|| format!("{rest} more"))))
637}
638
639#[cfg(test)]
640mod tests {
641 use alloc::collections::BTreeSet;
642
643 use super::{Action, Mode};
644 use crate::digest::Fingerprint;
645 use crate::format::Format;
646 use crate::path::RelPath;
647 use crate::profile::Policy;
648 use crate::survey::{Entry, Want};
649
650 fn fp(name: &str) -> Fingerprint {
655 Fingerprint::of(match name {
656 "p" => b"profile\n",
657 "p~" => b"profile \r\n\n",
658 "o" => b"old\n",
659 _ => b"edited\n",
660 })
661 }
662
663 fn entry(policy: Policy, found: Option<&str>, record: Option<&str>) -> Entry {
664 Entry {
665 path: RelPath::new("f").unwrap(),
666 part: None,
667 want: Some(Want {
668 layer: 0,
669 policy,
670 validate: Format::None,
671 executable: false,
672 fingerprint: fp("p"),
673 }),
674 record: record.map(fp),
675 recorded: record.map(|_| policy),
676 found: found.map(fp),
677 conflict: false,
678 gate: None,
679 present: found.is_some(),
680 keys: BTreeSet::new(),
681 }
682 }
683
684 #[test]
685 fn decision_table() {
686 use Action::{Keep, Merge, Record, Write};
687 use Policy::{Merge as M, Once, Owned};
688 #[rustfmt::skip]
689 let table = [
690 (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),
700 (Once, None, Some("p"), Keep, Keep),
701 (Once, Some("x"), None, Record, Record),
702 (Once, Some("o"), Some("o"), Keep, Keep),
703 (Once, Some("x"), Some("p"), Keep, Keep),
704 (M, Some("x"), Some("o"), Merge, Merge),
705 (M, Some("x"), Some("p"), Keep, Keep),
706 (M, Some("o"), Some("o"), Write, Write),
707 ];
708 for (policy, found, record, apply, force) in table {
709 let entry = entry(policy, found, record);
710 let case = format!("{policy:?} found={found:?} record={record:?}");
711 assert_eq!(entry.action(Mode::Apply), apply, "apply: {case}");
712 assert_eq!(entry.action(Mode::Force), force, "force: {case}");
713 }
714 }
715
716 #[test]
717 fn a_dropped_file_goes_unless_edited_or_the_targets() {
718 use Action::{Remove, Untrack};
719 use Policy::{Merge as M, Once, Owned};
720 #[rustfmt::skip]
721 let table = [
722 (Owned, Some("p"), Some("p"), Remove), (Owned, Some("p~"), Some("p"), Remove), (M, Some("p"), Some("p"), Remove),
726 (Owned, Some("x"), Some("p"), Untrack), (Owned, None, Some("p"), Untrack), (Once, Some("p"), Some("p"), Untrack), ];
730 for (policy, found, record, action) in table {
731 let entry = Entry { want: None, ..entry(policy, found, record) };
732 let case = format!("{policy:?} found={found:?} record={record:?}");
733 assert_eq!(entry.action(Mode::Apply), action, "apply: {case}");
734 assert_eq!(entry.action(Mode::Force), action, "force: {case}");
735 }
736 }
737}