dk_protocol/
pre_submit.rs1use std::collections::HashMap;
2
3use tonic::{Response, Status};
4use tracing::info;
5
6use crate::server::ProtocolServer;
7use crate::{PreSubmitCheckRequest, PreSubmitCheckResponse, SemanticConflict};
8
9pub async fn handle_pre_submit_check(
17 server: &ProtocolServer,
18 req: PreSubmitCheckRequest,
19) -> Result<Response<PreSubmitCheckResponse>, Status> {
20 let session = server.validate_session(&req.session_id)?;
21 crate::require_live_session::require_live_session(server, &req.session_id).await?;
22
23 let sid = req
24 .session_id
25 .parse::<uuid::Uuid>()
26 .map_err(|_| Status::invalid_argument("Invalid session ID"))?;
27 server.session_mgr().touch_session(&sid);
28
29 let engine = server.engine();
30
31 let ws = engine
33 .workspace_manager()
34 .get_workspace(&sid)
35 .ok_or_else(|| Status::not_found("Workspace not found for session"))?;
36
37 let (_repo_id, git_repo) = engine
39 .get_repo(&session.codebase)
40 .await
41 .map_err(|e| Status::internal(format!("Repo error: {e}")))?;
42
43 let head_hash = git_repo
44 .head_hash()
45 .map_err(|e| Status::internal(format!("Failed to read HEAD: {e}")))?
46 .unwrap_or_else(|| "initial".to_string());
47
48 let overlay = ws.overlay_for_tree();
49 let files_modified = overlay.len() as u32;
50 let symbols_changed = ws.graph.change_count() as u32;
51
52 if head_hash == ws.base_commit || overlay.is_empty() {
54 info!(
55 session_id = %req.session_id,
56 files_modified,
57 symbols_changed,
58 "PRE_SUBMIT_CHECK: clean (fast-forward possible)"
59 );
60
61 return Ok(Response::new(PreSubmitCheckResponse {
62 has_conflicts: false,
63 potential_conflicts: Vec::new(),
64 files_modified,
65 symbols_changed,
66 }));
67 }
68
69 let parser = engine.parser();
75 let paths: Vec<&String> = overlay.iter().map(|(p, _)| p).collect();
76
77 let mut base_entries: HashMap<&str, Option<Vec<u8>>> = HashMap::with_capacity(paths.len());
78 for path in &paths {
79 base_entries.insert(path.as_str(), git_repo.read_tree_entry(&ws.base_commit, path).ok());
80 }
81
82 let mut head_entries: HashMap<&str, Option<Vec<u8>>> = HashMap::with_capacity(paths.len());
83 for path in &paths {
84 head_entries.insert(path.as_str(), git_repo.read_tree_entry(&head_hash, path).ok());
85 }
86
87 let mut conflicts = Vec::new();
88
89 for (path, maybe_content) in &overlay {
90 let base_content = base_entries.get(path.as_str()).and_then(|v| v.as_ref());
91 let head_content = head_entries.get(path.as_str()).and_then(|v| v.as_ref());
92
93 match maybe_content {
94 None => {
95 if let (Some(base), Some(head)) = (base_content, head_content) {
97 if base != head {
98 conflicts.push(SemanticConflict {
99 file_path: path.clone(),
100 symbol_name: "<entire file>".to_string(),
101 our_change: "deleted".to_string(),
102 their_change: "modified".to_string(),
103 });
104 }
105 }
106 }
107 Some(overlay_content) => {
108 match (base_content, head_content) {
109 (Some(base), Some(head)) => {
110 if base != head {
111 let analysis =
112 dk_engine::workspace::conflict::analyze_file_conflict(
113 path,
114 base,
115 head,
116 overlay_content,
117 parser,
118 );
119
120 if let dk_engine::workspace::conflict::MergeAnalysis::Conflict {
121 conflicts: file_conflicts,
122 } = analysis
123 {
124 for c in file_conflicts {
125 conflicts.push(SemanticConflict {
126 file_path: c.file_path,
127 symbol_name: c.symbol_name,
128 our_change: format!("{:?}", c.our_change),
129 their_change: format!("{:?}", c.their_change),
130 });
131 }
132 }
133 }
134 }
135 (None, Some(head_blob)) => {
136 if *overlay_content != *head_blob {
137 conflicts.push(SemanticConflict {
138 file_path: path.clone(),
139 symbol_name: "<entire file>".to_string(),
140 our_change: "added".to_string(),
141 their_change: "added".to_string(),
142 });
143 }
144 }
145 (Some(_), None) => {
146 conflicts.push(SemanticConflict {
147 file_path: path.clone(),
148 symbol_name: "<entire file>".to_string(),
149 our_change: "modified".to_string(),
150 their_change: "deleted".to_string(),
151 });
152 }
153 (None, None) => {
154 }
156 }
157 }
158 }
159 }
160
161 let has_conflicts = !conflicts.is_empty();
162
163 info!(
164 session_id = %req.session_id,
165 has_conflicts,
166 conflict_count = conflicts.len(),
167 files_modified,
168 symbols_changed,
169 "PRE_SUBMIT_CHECK: completed"
170 );
171
172 Ok(Response::new(PreSubmitCheckResponse {
173 has_conflicts,
174 potential_conflicts: conflicts,
175 files_modified,
176 symbols_changed,
177 }))
178}