git_indexer/client.rs
1//! Helix DB client for indexing git repositories.
2//!
3//! This module provides a client for pushing git repository information
4//! to a Helix DB graph database instance.
5
6use crate::error::{ConfigError, HelixError, Result};
7use crate::extraction::extract;
8use crate::models::{BranchInfo, CommitInfo, FileChange, GitInfo};
9use std::path::Path;
10
11/// Client for interacting with a Helix DB instance.
12///
13/// Use [`GitIndexerClient::builder()`] to create a new client.
14///
15/// # Example
16///
17/// ```no_run
18/// use git_indexer::GitIndexerClient;
19///
20/// # async fn example() -> git_indexer::Result<()> {
21/// let client = GitIndexerClient::builder()
22/// .endpoint("http://localhost:6969")
23/// .build()?;
24///
25/// let git_info = client.index_repository("/path/to/repo").await?;
26/// # Ok(())
27/// # }
28/// ```
29#[derive(Debug, Clone)]
30pub struct GitIndexerClient {
31 endpoint: String,
32 // Future: Add helix-db client when implementing actual API calls
33 // helix_client: HelixDB,
34}
35
36/// Builder for constructing a [`GitIndexerClient`].
37#[derive(Debug, Default)]
38pub struct GitIndexerClientBuilder {
39 endpoint: Option<String>,
40}
41
42impl GitIndexerClient {
43 /// Create a new builder for configuring a `GitIndexerClient`.
44 pub fn builder() -> GitIndexerClientBuilder {
45 GitIndexerClientBuilder::default()
46 }
47
48 /// Get the configured endpoint URL.
49 pub fn endpoint(&self) -> &str {
50 &self.endpoint
51 }
52
53 /// Index a git repository and push all data to Helix DB.
54 ///
55 /// This is the main entry point for indexing a repository. It will:
56 /// 1. Extract all git information (branches, commits, file changes)
57 /// 2. Create nodes for each branch, commit, and file change
58 /// 3. Create edges for parent-child commit relationships
59 ///
60 /// # Arguments
61 ///
62 /// * `repo_path` - Path to the git repository
63 ///
64 /// # Returns
65 ///
66 /// Returns the extracted `GitInfo` after successfully pushing to Helix DB.
67 pub async fn index_repository<P: AsRef<Path>>(&self, repo_path: P) -> Result<GitInfo> {
68 let git_info = extract(repo_path.as_ref())?;
69
70 // Create branch nodes
71 for branch in &git_info.branches {
72 self.create_branch_node(branch).await?;
73 }
74
75 // Create commit nodes and their relationships
76 for commit in &git_info.commits {
77 self.create_commit_node(commit).await?;
78
79 // Create file change nodes
80 for file_change in &commit.file_changes {
81 self.create_file_node(file_change, &commit.id).await?;
82 }
83
84 // Create parent edges
85 for parent_id in &commit.parent_ids {
86 self.create_parent_edge(&commit.id, parent_id).await?;
87 }
88 }
89
90 Ok(git_info)
91 }
92
93 /// Create a commit node in Helix DB.
94 ///
95 /// # Arguments
96 ///
97 /// * `commit` - The commit information to store
98 pub async fn create_commit_node(&self, commit: &CommitInfo) -> Result<()> {
99 // TODO: Implement actual Helix DB API call
100 // Example future implementation:
101 // ```
102 // use helix_db::HelixDB;
103 // let payload = serde_json::json!({
104 // "id": commit.id,
105 // "message": commit.message,
106 // "author": commit.author,
107 // "timestamp": commit.timestamp,
108 // });
109 // self.helix_client.query("CreateCommit", &payload).await?;
110 // ```
111
112 let _ = commit; // Suppress unused warning
113 Err(HelixError::NotImplemented("create_commit_node".to_string()).into())
114 }
115
116 /// Create a branch node in Helix DB.
117 ///
118 /// # Arguments
119 ///
120 /// * `branch` - The branch information to store
121 pub async fn create_branch_node(&self, branch: &BranchInfo) -> Result<()> {
122 // TODO: Implement actual Helix DB API call
123 // Example future implementation:
124 // ```
125 // let payload = serde_json::json!({
126 // "name": branch.name,
127 // "is_head": branch.is_head,
128 // "commit_id": branch.commit_id,
129 // "is_remote": branch.is_remote,
130 // });
131 // self.helix_client.query("CreateBranch", &payload).await?;
132 // ```
133
134 let _ = branch; // Suppress unused warning
135 Err(HelixError::NotImplemented("create_branch_node".to_string()).into())
136 }
137
138 /// Create a file change node in Helix DB.
139 ///
140 /// # Arguments
141 ///
142 /// * `file` - The file change information to store
143 /// * `commit_id` - The SHA of the commit this file change belongs to
144 pub async fn create_file_node(&self, file: &FileChange, commit_id: &str) -> Result<()> {
145 // TODO: Implement actual Helix DB API call
146 // Example future implementation:
147 // ```
148 // let payload = serde_json::json!({
149 // "path": file.path,
150 // "old_path": file.old_path,
151 // "change_type": file.change_type,
152 // "commit_id": commit_id,
153 // "hunks": file.hunks,
154 // });
155 // self.helix_client.query("CreateFileChange", &payload).await?;
156 // ```
157
158 let _ = (file, commit_id); // Suppress unused warning
159 Err(HelixError::NotImplemented("create_file_node".to_string()).into())
160 }
161
162 /// Create a parent-child edge between commits in Helix DB.
163 ///
164 /// # Arguments
165 ///
166 /// * `child_id` - SHA of the child commit
167 /// * `parent_id` - SHA of the parent commit
168 pub async fn create_parent_edge(&self, child_id: &str, parent_id: &str) -> Result<()> {
169 // TODO: Implement actual Helix DB API call
170 // Example future implementation:
171 // ```
172 // let payload = serde_json::json!({
173 // "child_id": child_id,
174 // "parent_id": parent_id,
175 // });
176 // self.helix_client.query("CreateParentEdge", &payload).await?;
177 // ```
178
179 let _ = (child_id, parent_id); // Suppress unused warning
180 Err(HelixError::NotImplemented("create_parent_edge".to_string()).into())
181 }
182}
183
184impl GitIndexerClientBuilder {
185 /// Set the Helix DB endpoint URL.
186 ///
187 /// # Arguments
188 ///
189 /// * `endpoint` - The URL of the Helix DB instance (e.g., "http://localhost:6969")
190 pub fn endpoint<S: Into<String>>(mut self, endpoint: S) -> Self {
191 self.endpoint = Some(endpoint.into());
192 self
193 }
194
195 /// Build the `GitIndexerClient`.
196 ///
197 /// # Errors
198 ///
199 /// Returns an error if the endpoint is not set.
200 pub fn build(self) -> Result<GitIndexerClient> {
201 let endpoint = self
202 .endpoint
203 .ok_or_else(|| ConfigError::MissingField("endpoint".to_string()))?;
204
205 // Validate endpoint URL format
206 if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
207 return Err(ConfigError::InvalidEndpoint(format!(
208 "Endpoint must start with http:// or https://, got: {}",
209 endpoint
210 ))
211 .into());
212 }
213
214 Ok(GitIndexerClient { endpoint })
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_builder_missing_endpoint() {
224 let result = GitIndexerClient::builder().build();
225 assert!(result.is_err());
226 }
227
228 #[test]
229 fn test_builder_invalid_endpoint() {
230 let result = GitIndexerClient::builder().endpoint("not-a-url").build();
231 assert!(result.is_err());
232 }
233
234 #[test]
235 fn test_builder_valid_endpoint() {
236 let client = GitIndexerClient::builder()
237 .endpoint("http://localhost:6969")
238 .build()
239 .unwrap();
240 assert_eq!(client.endpoint(), "http://localhost:6969");
241 }
242
243 #[test]
244 fn test_builder_https_endpoint() {
245 let client = GitIndexerClient::builder()
246 .endpoint("https://helix.example.com:6969")
247 .build()
248 .unwrap();
249 assert_eq!(client.endpoint(), "https://helix.example.com:6969");
250 }
251}