Write a single NewEdge operation to the graph. This handles the actual edge modification: resolving positions, removing old edges, adding new edges, and detecting zombie conflicts. Edges are written to the global GRAPH and INODE_GRAPH tables. # Arguments `txn` - Write transaction `workspace` - Workspace for tracking state `change_id` - Internal ID of current change `inode` - File inode positio
(
txn: &mut CachedWriteGraphTxn<'_, '_>,
workspace: &mut Workspace,
change_id: NodeId,
inode: &Position<Option<Hash>>,
edge: &NewEdge<Option<Hash>>,
change: &Change,
detect
| 142 | /// * `edge` - The edge to write |
| 143 | /// * `change` - Full change for dependency checking |
| 144 | fn write_new_edge( |
| 145 | txn: &mut CachedWriteGraphTxn<'_, '_>, |
| 146 | workspace: &mut Workspace, |
| 147 | change_id: NodeId, |
| 148 | inode: &Position<Option<Hash>>, |
| 149 | edge: &NewEdge<Option<Hash>>, |
| 150 | change: &Change, |
| 151 | detect_conflicts: bool, |
| 152 | ) -> Result<(), LocalApplyError> { |
| 153 | log::debug!( |
| 154 | "write_new_edge: flag={:?} from={:?} to={:?}", |
| 155 | edge.flag, |
| 156 | edge.from, |
| 157 | edge.to |
| 158 | ); |
| 159 | |
| 160 | // Resolve the introduced_by change for validation / side effects. |
| 161 | // The resolved id itself is unused here — the additive-only edge |
| 162 | // model writes the new edge directly without consulting it. |
| 163 | let _ = resolve_introduced_by(txn, &edge.introduced_by, change_id)?; |
| 164 | |
| 165 | // Find source span — predecessor context (ending at position). |
| 166 | let source_pos = resolve_position(txn, &edge.from, change_id)?; |
| 167 | let source = resolve_vertex(txn, source_pos, true)?; |
| 168 | |
| 169 | // Prefer the exact serialized target span when it already exists. |
| 170 | // Structural updates like FileMove name deletions refer to a concrete |
| 171 | // vertex span, and reducing them to start_pos loses that precision. |
| 172 | let exact_target = resolve_exact_vertex(txn, &edge.to, change_id)?; |
| 173 | let mut target = if txn |
| 174 | .has_vertex(exact_target) |
| 175 | .map_err(|e| LocalApplyError::Internal { |
| 176 | message: format!("Failed to check target vertex: {}", e), |
| 177 | })? { |
| 178 | exact_target |
| 179 | } else { |
| 180 | let target_pos = resolve_position(txn, &edge.to.start_pos(), change_id)?; |
| 181 | resolve_vertex(txn, target_pos, false)? |
| 182 | }; |
| 183 | |
| 184 | // Resolve inode for indexing |
| 185 | let resolved_inode = resolve_inode(txn, inode, change_id)?; |
| 186 | |
| 187 | // Parse the edge kind for semantic dispatch. |
| 188 | // `edge.flag` is still `EdgeFlags` (wire format from the change), so we |
| 189 | // parse it into a typed `EdgeKind` for cleaner branching below. |
| 190 | let kind = EdgeKind::from_flags(edge.flag); |
| 191 | |
| 192 | // Track folder files for conflict detection |
| 193 | if kind.is_some_and(|k| k.is_folder()) { |
| 194 | workspace.mark_rooted(target.start_pos()); |
| 195 | } |
| 196 | |
| 197 | // Handle potential span splitting for partial targets |
| 198 | let target_end_pos = resolve_position(txn, &edge.to.end_pos(), change_id)?; |
| 199 | if target.end > target_end_pos.pos { |
| 200 | // Target span extends beyond the edge target - adjust |
| 201 | target = GraphNode { |
no test coverage detected