From 1801eff41ae516d63e19fea20ce830a5fb6d8311 Mon Sep 17 00:00:00 2001 From: kelmith Date: Fri, 10 Jul 2026 15:06:00 +0530 Subject: [PATCH] fix: make sure repo hash uses an exact match not CONTAINS --- .../lang/graphs/neo4j/operations/migration.rs | 41 +++++++++---------- standalone/tests/cross_repo_sync.rs | 36 +++++++++++++++- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/ast/src/lang/graphs/neo4j/operations/migration.rs b/ast/src/lang/graphs/neo4j/operations/migration.rs index 1003819b4..f4d0272d3 100644 --- a/ast/src/lang/graphs/neo4j/operations/migration.rs +++ b/ast/src/lang/graphs/neo4j/operations/migration.rs @@ -4,6 +4,7 @@ use crate::lang::graphs::{ queries::*, Edge, EdgeType, Neo4jGraph, NodeData, NodeKeys, NodeRef, NodeType, }; +use git_url_parse::GitUrl; use neo4rs::{query, BoltMap}; use shared::{Error, Result}; use tracing::{info, warn}; @@ -297,20 +298,24 @@ impl Neo4jGraph { } } +fn repo_full_name(repo_url: &str) -> String { + match GitUrl::parse(repo_url) { + Ok(gurl) => format!("{}/{}", gurl.owner.unwrap_or_default(), gurl.name), + Err(_) => repo_url + .trim_end_matches(".git") + .rsplit('/') + .next() + .unwrap_or(repo_url) + .to_string(), + } +} + pub fn get_repository_hash_query(repo_url: &str) -> (String, BoltMap) { let mut params = BoltMap::new(); - let repo_name = if repo_url.contains('/') { - let parts: Vec<&str> = repo_url.split('/').collect(); - let name = parts.last().unwrap_or(&repo_url); - name.trim_end_matches(".git") - } else { - repo_url - }; - - boltmap_insert_str(&mut params, "repo_name", repo_name); - let query = "MATCH (r:Repository) - WHERE r.name CONTAINS $repo_name + boltmap_insert_str(&mut params, "repo_name", &repo_full_name(repo_url)); + let query = "MATCH (r:Repository) + WHERE r.name = $repo_name RETURN r.hash as hash"; (query.to_string(), params) @@ -392,19 +397,11 @@ pub fn remove_nodes_by_files_chunked_query( pub fn update_repository_hash_query(repo_url: &str, new_hash: &str) -> (String, BoltMap) { let mut params = BoltMap::new(); - let name = if repo_url.contains('/') { - let parts: Vec<&str> = repo_url.split('/').collect(); - let n = parts.last().unwrap_or(&repo_url); - n.trim_end_matches(".git") - } else { - repo_url - }; - - boltmap_insert_str(&mut params, "repo_name", name); + boltmap_insert_str(&mut params, "repo_name", &repo_full_name(repo_url)); boltmap_insert_str(&mut params, "new_hash", new_hash); - let query = "MATCH (r:Repository) - WHERE r.name CONTAINS $repo_name + let query = "MATCH (r:Repository) + WHERE r.name = $repo_name SET r.hash = $new_hash"; (query.to_string(), params) diff --git a/standalone/tests/cross_repo_sync.rs b/standalone/tests/cross_repo_sync.rs index b5732c022..44756e51a 100644 --- a/standalone/tests/cross_repo_sync.rs +++ b/standalone/tests/cross_repo_sync.rs @@ -3,7 +3,7 @@ use ast::lang::graphs::graph_ops::GraphOps; use ast::lang::graphs::EdgeType; use ast::lang::linker::normalize_frontend_path; -use ast::lang::{Graph, NodeType}; +use ast::lang::{Graph, NodeData, NodeType}; use ast::repo::Repo; use axum::{extract::State, Json}; use standalone::{ingest, sync, AppState, ProcessBody}; @@ -235,6 +235,40 @@ async fn test_cross_repo_muted_preservation() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_sync_hash_scoped_to_exact_repo() { + let state = test_state(); + let g = fresh_graph().await; + + reset_clones(); + let _ = ingest(State(state.clone()), Json(body(BE, Some("before")))) + .await + .expect("backend ingest failed"); + + let sibling = NodeData { + name: "fayekelmith/graph-update-backend-extra".to_string(), + file: "fayekelmith/graph-update-backend-extra".to_string(), + hash: Some("sentinel".to_string()), + ..Default::default() + }; + g.graph + .add_node_async(NodeType::Repository, sibling) + .await + .expect("failed to seed sibling repository node"); + + sync_repo(&state, BE, "after").await; + + let hash = g + .graph + .get_repository_hash("https://github.com/fayekelmith/graph-update-backend-extra") + .await + .expect("sibling repository node lost after syncing another repo"); + assert_eq!( + hash, "sentinel", + "sibling repository hash must not change when syncing another repo" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_backward_sync_preserves_mute() { let state = test_state();