apply.v

3.1 KB · raw

1//! The per-file three-way engine, shared by `pluck` and by combine's replay.2//!3//! One save's change is applied onto the current tree file by file. For each4//! path in scope the current content must match either the base (clean apply,5//! take the new content) or the target (already applied, leave it); anything6//! else refuses rather than guessing. There is no line-level merging: the unit7//! of resolution is the file, which is why a clash is always a hard conflict.89module apply1011import src.snapshot1213pub struct Plan {14pub mut:15	files    []snapshot.FileEntry // the tree that results from applying16	applied  []string             // paths the plan changes17	conflict string               // first path that does not apply cleanly18}1920pub fn conflicts(plan Plan) bool {21	return plan.conflict != ''22}2324// three_way applies the change from `base` to `target` onto `current`.25// `filters` limits it to those paths or folders; an empty list means everything.26pub fn three_way(current []snapshot.FileEntry, base []snapshot.FileEntry, target []snapshot.FileEntry, filters []string) Plan {27	base_map := snapshot.index_files(base)28	target_map := snapshot.index_files(target)29	current_map := snapshot.index_files(current)3031	mut paths := []string{}32	for path in base_map.keys() {33		if in_scope(path, filters) {34			paths << path35		}36	}37	for path in target_map.keys() {38		if in_scope(path, filters) && path !in base_map {39			paths << path40		}41	}42	paths.sort()4344	mut plan := Plan{45		files:   current.clone()46		applied: []string{}47	}48	for path in paths {49		from := base_map[path] or {50			snapshot.FileEntry{51				path: path52			}53		}54		to := target_map[path] or {55			snapshot.FileEntry{56				path: path57			}58		}59		if same_entry(from, to) {60			continue61		}62		here := current_map[path] or {63			snapshot.FileEntry{64				path: path65			}66		}67		if same_entry(here, from) {68			plan.files = set_entry(plan.files, path, to, path in target_map)69			plan.applied << path70			continue71		}72		if same_entry(here, to) {73			// Somebody already applied this change; nothing to do.74			continue75		}76		plan.conflict = path77		return plan78	}79	return plan80}8182pub fn in_scope(path string, filters []string) bool {83	if filters.len == 0 {84		return true85	}86	for raw in filters {87		filter := raw.trim_right('/')88		if path == filter || path.starts_with('${filter}/') {89			return true90		}91	}92	return false93}9495// same_entry treats two absent paths as equal, and compares presence, content96// and the executable bit otherwise.97fn same_entry(a snapshot.FileEntry, b snapshot.FileEntry) bool {98	if a.hash == '' && b.hash == '' {99		return true100	}101	return a.hash == b.hash && a.executable == b.executable102}103104// set_entry replaces one path in a sorted tree, adding or removing it, and105// keeps the list ordered by path.106fn set_entry(files []snapshot.FileEntry, path string, entry snapshot.FileEntry, keep bool) []snapshot.FileEntry {107	mut out := []snapshot.FileEntry{}108	mut placed := false109	for file in files {110		if file.path == path {111			if keep {112				out << entry113			}114			placed = true115			continue116		}117		if !placed && file.path > path {118			if keep {119				out << entry120			}121			placed = true122		}123		out << file124	}125	if !placed && keep {126		out << entry127	}128	return out129}