Compute the sync plan for one view: what to store and whether to declare. `remote` is the remote's parsed manifest, or `None` if the view does not exist on the remote (the remote log is empty). The fast-forward rule: the remote log must be a prefix of the local log. The suffix (everything beyond the prefix) is what needs storing — its dependencies are either earlier in the log or already on the
(
local: &ViewManifest,
remote: Option<&ViewManifest>,
force: bool,
is_leaf: bool,
)
| 196 | /// genuinely ambiguous (local is behind vs. local shrank), so it is left to the |
| 197 | /// prefix/divergence rules, which err toward "pull first, or `--force`". |
| 198 | pub fn plan_view_sync( |
| 199 | local: &ViewManifest, |
| 200 | remote: Option<&ViewManifest>, |
| 201 | force: bool, |
| 202 | is_leaf: bool, |
| 203 | ) -> Result<ViewSyncPlan, ViewSyncConflict> { |
| 204 | let remote = match remote { |
| 205 | None => { |
| 206 | // View absent on remote: the whole log is the suffix. |
| 207 | return Ok(ViewSyncPlan { |
| 208 | suffix: local.changes.clone(), |
| 209 | declare: true, |
| 210 | forced: false, |
| 211 | shrink: false, |
| 212 | }); |
| 213 | } |
| 214 | Some(r) => r, |
| 215 | }; |
| 216 | |
| 217 | // Identity must match: the manifest declares scope and parent, and the |
| 218 | // server refuses to mutate an existing view's identity. |
| 219 | if remote.scope != local.scope { |
| 220 | return Err(ViewSyncConflict::IdentityMismatch { |
| 221 | field: "scope", |
| 222 | local: local.scope.to_string(), |
| 223 | remote: remote.scope.to_string(), |
| 224 | }); |
| 225 | } |
| 226 | if remote.parent != local.parent { |
| 227 | let show = |p: &Option<String>| p.clone().unwrap_or_else(|| "(none)".to_string()); |
| 228 | return Err(ViewSyncConflict::IdentityMismatch { |
| 229 | field: "parent", |
| 230 | local: show(&local.parent), |
| 231 | remote: show(&remote.parent), |
| 232 | }); |
| 233 | } |
| 234 | |
| 235 | // Ancestor already satisfied. When this view is NOT the one being pushed (an |
| 236 | // ancestor pulled in for a descendant) and the remote already holds every |
| 237 | // local change, there is nothing to do: the remote's set already satisfies |
| 238 | // the descendant, and pushing a child never implies rewriting a parent. Skip |
| 239 | // without error — so a child push is never blocked by an ancestor whose |
| 240 | // remote is merely larger (a local shrink) — and flag `shrink` when the |
| 241 | // remote is strictly larger so the caller can explain the skip. The leaf |
| 242 | // falls through to the prefix/divergence rules below, where `local ⊂ remote` |
| 243 | // is ambiguous and errs toward "pull or --force". |
| 244 | if !is_leaf { |
| 245 | let remote_set: HashSet<&Hash> = remote.changes.iter().collect(); |
| 246 | if local.changes.iter().all(|h| remote_set.contains(h)) { |
| 247 | return Ok(ViewSyncPlan { |
| 248 | suffix: Vec::new(), |
| 249 | declare: false, |
| 250 | forced: false, |
| 251 | shrink: local.changes.len() != remote.changes.len(), |
| 252 | }); |
| 253 | } |
| 254 | } |
| 255 |