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,
)
| 178 | /// Identity mismatches (scope/parent) are never forced — they are |
| 179 | /// structural, and the server would reject them regardless. |
| 180 | pub fn plan_view_sync( |
| 181 | local: &ViewManifest, |
| 182 | remote: Option<&ViewManifest>, |
| 183 | force: bool, |
| 184 | ) -> Result<ViewSyncPlan, ViewSyncConflict> { |
| 185 | let remote = match remote { |
| 186 | None => { |
| 187 | // View absent on remote: the whole log is the suffix. |
| 188 | return Ok(ViewSyncPlan { |
| 189 | suffix: local.changes.clone(), |
| 190 | declare: true, |
| 191 | forced: false, |
| 192 | }); |
| 193 | } |
| 194 | Some(r) => r, |
| 195 | }; |
| 196 | |
| 197 | // Identity must match: the manifest declares scope and parent, and the |
| 198 | // server refuses to mutate an existing view's identity. |
| 199 | if remote.scope != local.scope { |
| 200 | return Err(ViewSyncConflict::IdentityMismatch { |
| 201 | field: "scope", |
| 202 | local: local.scope.to_string(), |
| 203 | remote: remote.scope.to_string(), |
| 204 | }); |
| 205 | } |
| 206 | if remote.parent != local.parent { |
| 207 | let show = |p: &Option<String>| p.clone().unwrap_or_else(|| "(none)".to_string()); |
| 208 | return Err(ViewSyncConflict::IdentityMismatch { |
| 209 | field: "parent", |
| 210 | local: show(&local.parent), |
| 211 | remote: show(&remote.parent), |
| 212 | }); |
| 213 | } |
| 214 | |
| 215 | // Prefix rule. |
| 216 | let diverged = if remote.changes.len() > local.changes.len() { |
| 217 | Some(ViewSyncConflict::Diverged { |
| 218 | first_mismatch: None, |
| 219 | local_len: local.changes.len(), |
| 220 | remote_len: remote.changes.len(), |
| 221 | }) |
| 222 | } else { |
| 223 | remote |
| 224 | .changes |
| 225 | .iter() |
| 226 | .zip(local.changes.iter()) |
| 227 | .position(|(r, l)| r != l) |
| 228 | .map(|i| ViewSyncConflict::Diverged { |
| 229 | first_mismatch: Some(i), |
| 230 | local_len: local.changes.len(), |
| 231 | remote_len: remote.changes.len(), |
| 232 | }) |
| 233 | }; |
| 234 | |
| 235 | if let Some(conflict) = diverged { |
| 236 | if !force { |
| 237 | return Err(conflict); |