apply.v

3.5 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//26// `filters` limits it to those paths or folders; an empty list means27// everything. `exclude` drops paths outright, which is how a combine leaves the28// clash files for the caller to decide.29pub fn three_way(current []snapshot.FileEntry, base []snapshot.FileEntry, target []snapshot.FileEntry, filters []string, exclude []string) Plan {30	base_map := snapshot.index_files(base)31	target_map := snapshot.index_files(target)32	current_map := snapshot.index_files(current)3334	mut paths := []string{}35	for path in base_map.keys() {36		if in_scope(path, filters) && !excluded(path, exclude) {37			paths << path38		}39	}40	for path in target_map.keys() {41		if in_scope(path, filters) && !excluded(path, exclude) && path !in base_map {42			paths << path43		}44	}45	paths.sort()4647	mut plan := Plan{48		files:   current.clone()49		applied: []string{}50	}51	for path in paths {52		from := base_map[path] or {53			snapshot.FileEntry{54				path: path55			}56		}57		to := target_map[path] or {58			snapshot.FileEntry{59				path: path60			}61		}62		if same_entry(from, to) {63			continue64		}65		here := current_map[path] or {66			snapshot.FileEntry{67				path: path68			}69		}70		if same_entry(here, from) {71			plan.files = set_entry(plan.files, path, to, path in target_map)72			plan.applied << path73			continue74		}75		if same_entry(here, to) {76			// Somebody already applied this change; nothing to do.77			continue78		}79		plan.conflict = path80		return plan81	}82	return plan83}8485pub fn in_scope(path string, filters []string) bool {86	if filters.len == 0 {87		return true88	}89	for raw in filters {90		filter := raw.trim_right('/')91		if path == filter || path.starts_with('${filter}/') {92			return true93		}94	}95	return false96}9798fn excluded(path string, exclude []string) bool {99	for name in exclude {100		if path == name {101			return true102		}103	}104	return false105}106107// same_entry treats two absent paths as equal, and compares presence, content108// and the executable bit otherwise.109fn same_entry(a snapshot.FileEntry, b snapshot.FileEntry) bool {110	if a.hash == '' && b.hash == '' {111		return true112	}113	return a.hash == b.hash && a.executable == b.executable114}115116// set_entry replaces one path in a sorted tree, adding or removing it, and117// keeps the list ordered by path.118fn set_entry(files []snapshot.FileEntry, path string, entry snapshot.FileEntry, keep bool) []snapshot.FileEntry {119	mut out := []snapshot.FileEntry{}120	mut placed := false121	for file in files {122		if file.path == path {123			if keep {124				out << entry125			}126			placed = true127			continue128		}129		if !placed && file.path > path {130			if keep {131				out << entry132			}133			placed = true134		}135		out << file136	}137	if !placed && keep {138		out << entry139	}140	return out141}