git_plumber/core/
mod.rs

1use crate::git::repository::{Repository, RepositoryError};
2use std::path::{Path, PathBuf};
3
4/// Main application struct that handles shared logic
5pub struct GitPlumber {
6    repo_path: PathBuf,
7    repository: Option<Repository>,
8}
9
10impl GitPlumber {
11    /// Create a new `GitPlumber` instance
12    pub fn new(repo_path: impl AsRef<Path>) -> Self {
13        let repo_path = repo_path.as_ref().to_path_buf();
14        let repository = Repository::new(&repo_path).ok();
15
16        Self {
17            repo_path,
18            repository,
19        }
20    }
21
22    /// Get the repository path
23    #[must_use]
24    pub fn get_repo_path(&self) -> &Path {
25        &self.repo_path
26    }
27
28    /// Get access to the repository if it exists
29    #[must_use]
30    pub const fn get_repository(&self) -> Option<&Repository> {
31        self.repository.as_ref()
32    }
33
34    /// List all pack files in the repository
35    ///
36    /// # Errors
37    ///
38    /// This function will return an error if:
39    /// - The path is not a valid git repository
40    /// - File system operations fail when reading the objects/pack directory
41    pub fn list_pack_files(&self) -> Result<Vec<PathBuf>, RepositoryError> {
42        self.repository.as_ref().map_or_else(
43            || {
44                Err(RepositoryError::NotGitRepository(format!(
45                    "{} is not a git repository",
46                    self.repo_path.display()
47                )))
48            },
49            Repository::list_pack_files,
50        )
51    }
52
53    /// List all pack-related files grouped by their base name
54    ///
55    /// # Errors
56    ///
57    /// This function will return an error if:
58    /// - The path is not a valid git repository
59    /// - File system operations fail when reading the objects/pack directory
60    pub fn list_pack_groups(
61        &self,
62    ) -> Result<std::collections::HashMap<String, crate::git::repository::PackGroup>, RepositoryError>
63    {
64        self.repository.as_ref().map_or_else(
65            || {
66                Err(RepositoryError::NotGitRepository(format!(
67                    "{} is not a git repository",
68                    self.repo_path.display()
69                )))
70            },
71            Repository::list_pack_groups,
72        )
73    }
74
75    /// List all head refs (local branches) in the repository
76    ///
77    /// # Errors
78    ///
79    /// This function will return an error if:
80    /// - The path is not a valid git repository
81    /// - File system operations fail when reading the refs/heads directory
82    pub fn list_head_refs(&self) -> Result<Vec<PathBuf>, RepositoryError> {
83        self.repository.as_ref().map_or_else(
84            || {
85                Err(RepositoryError::NotGitRepository(format!(
86                    "{} is not a git repository",
87                    self.repo_path.display()
88                )))
89            },
90            Repository::list_head_refs,
91        )
92    }
93
94    /// List all remote refs in the repository
95    ///
96    /// # Errors
97    ///
98    /// This function will return an error if:
99    /// - The path is not a valid git repository
100    /// - File system operations fail when reading the refs/remotes directory
101    pub fn list_remote_refs(&self) -> Result<Vec<(String, Vec<PathBuf>)>, RepositoryError> {
102        self.repository.as_ref().map_or_else(
103            || {
104                Err(RepositoryError::NotGitRepository(format!(
105                    "{} is not a git repository",
106                    self.repo_path.display()
107                )))
108            },
109            Repository::list_remote_refs,
110        )
111    }
112
113    /// List all tag refs in the repository
114    ///
115    /// # Errors
116    ///
117    /// This function will return an error if:
118    /// - The path is not a valid git repository
119    /// - File system operations fail when reading the refs/tags directory
120    pub fn list_tag_refs(&self) -> Result<Vec<PathBuf>, RepositoryError> {
121        self.repository.as_ref().map_or_else(
122            || {
123                Err(RepositoryError::NotGitRepository(format!(
124                    "{} is not a git repository",
125                    self.repo_path.display()
126                )))
127            },
128            Repository::list_tag_refs,
129        )
130    }
131
132    /// Check if the repository has stash refs
133    ///
134    /// # Errors
135    ///
136    /// This function will return an error if:
137    /// - The path is not a valid git repository
138    /// - File system operations fail when checking for stash refs
139    pub fn has_stash_ref(&self) -> Result<bool, RepositoryError> {
140        self.repository.as_ref().map_or_else(
141            || {
142                Err(RepositoryError::NotGitRepository(format!(
143                    "{} is not a git repository",
144                    self.repo_path.display()
145                )))
146            },
147            Repository::has_stash_ref,
148        )
149    }
150
151    /// List loose objects in the repository with a limit
152    ///
153    /// # Errors
154    ///
155    /// This function will return an error if:
156    /// - The path is not a valid git repository
157    /// - File system operations fail when reading loose object directories
158    pub fn list_loose_objects(&self, limit: usize) -> Result<Vec<PathBuf>, RepositoryError> {
159        self.repository.as_ref().map_or_else(
160            || {
161                Err(RepositoryError::NotGitRepository(format!(
162                    "{} is not a git repository",
163                    self.repo_path.display()
164                )))
165            },
166            |repo| repo.list_loose_objects(limit),
167        )
168    }
169
170    /// List parsed loose objects in the repository with a limit
171    ///
172    /// # Errors
173    ///
174    /// This function will return an error if:
175    /// - The path is not a valid git repository
176    /// - File system operations fail when reading loose object directories
177    /// - Loose objects cannot be parsed or decompressed
178    pub fn list_parsed_loose_objects(
179        &self,
180        limit: usize,
181    ) -> Result<Vec<crate::git::loose_object::LooseObject>, RepositoryError> {
182        self.repository.as_ref().map_or_else(
183            || {
184                Err(RepositoryError::NotGitRepository(format!(
185                    "{} is not a git repository",
186                    self.repo_path.display()
187                )))
188            },
189            |repo| repo.list_parsed_loose_objects(limit),
190        )
191    }
192
193    /// Get statistics about loose objects in the repository
194    ///
195    /// # Errors
196    ///
197    /// This function will return an error if:
198    /// - The path is not a valid git repository
199    /// - File system operations fail when reading loose object directories
200    /// - Loose objects cannot be parsed or analyzed
201    pub fn get_loose_object_stats(
202        &self,
203    ) -> Result<crate::git::repository::LooseObjectStats, RepositoryError> {
204        self.repository.as_ref().map_or_else(
205            || {
206                Err(RepositoryError::NotGitRepository(format!(
207                    "{} is not a git repository",
208                    self.repo_path.display()
209                )))
210            },
211            Repository::get_loose_object_stats,
212        )
213    }
214
215    /// Parse a pack file (basic analysis)
216    ///
217    /// # Errors
218    ///
219    /// This function will return an error if:
220    /// - The pack file cannot be read
221    /// - The pack file format is invalid
222    /// - Parsing operations fail
223    pub fn parse_pack_file(&self, path: &Path) -> Result<(), String> {
224        // Read the pack file
225        let pack_data = std::fs::read(path).map_err(|e| format!("Error reading file: {e}"))?;
226
227        // Parse the pack file
228        match crate::git::pack::Header::parse(&pack_data) {
229            Ok((objects_data, header)) => {
230                crate::cli::safe_println(&format!("Pack version: {}", header.version))?;
231                crate::cli::safe_println(&format!("Number of objects: {}", header.object_count))?;
232                let mut remaining_data = objects_data;
233                for i in 0..header.object_count {
234                    match crate::git::pack::Object::parse(remaining_data) {
235                        Ok((new_remaining_data, object)) => {
236                            crate::cli::safe_println(&format!("{object}"))?;
237                            remaining_data = new_remaining_data;
238                        }
239                        Err(e) => {
240                            return Err(format!("Error parsing object: {e}"));
241                        }
242                    }
243                    if i < header.object_count - 1 {
244                        crate::cli::safe_println("--------------------------------")?;
245                    }
246                }
247                Ok(())
248            }
249            Err(e) => Err(format!("Error parsing pack file: {e}")),
250        }
251    }
252
253    /// Parse a pack file with detailed formatting for display
254    ///
255    /// # Errors
256    ///
257    /// This function will return an error if:
258    /// - The pack file cannot be read
259    /// - The pack file format is invalid
260    /// - Parsing or display formatting operations fail
261    pub fn parse_pack_file_rich(&self, path: &Path) -> Result<(), String> {
262        use crate::cli::formatters::CliPackFormatter;
263
264        // Read the pack file
265        let pack_data = std::fs::read(path).map_err(|e| format!("Error reading file: {e}"))?;
266
267        // Parse the pack file header
268        match crate::git::pack::Header::parse(&pack_data) {
269            Ok((mut remaining_data, header)) => {
270                let mut objects = Vec::new();
271
272                // Parse all objects
273                for _i in 0..header.object_count {
274                    match crate::git::pack::Object::parse(remaining_data) {
275                        Ok((new_remaining_data, object)) => {
276                            objects.push(object);
277                            remaining_data = new_remaining_data;
278                        }
279                        Err(e) => {
280                            return Err(format!("Error parsing object: {e}"));
281                        }
282                    }
283                }
284
285                // Format and display the rich output
286                let formatted_output = CliPackFormatter::format_pack_file(&header, &objects);
287                crate::cli::safe_print(&formatted_output)?;
288
289                Ok(())
290            }
291            Err(e) => Err(format!("Error parsing pack file: {e}")),
292        }
293    }
294
295    /// View a file as a loose object with rich formatting
296    ///
297    /// # Errors
298    ///
299    /// This function will return an error if:
300    /// - The file cannot be read or parsed as a loose object
301    /// - The formatting operations fail
302    pub fn view_file_as_object(&self, path: &Path) -> Result<(), String> {
303        use crate::cli::formatters::CliLooseFormatter;
304
305        self.repository.as_ref().map_or_else(
306            || {
307                Err(format!(
308                    "Not a git repository: {}",
309                    self.repo_path.display()
310                ))
311            },
312            |repo| match repo.read_loose_object(path) {
313                Ok(loose_obj) => {
314                    let formatted_output = CliLooseFormatter::format_loose_object(&loose_obj);
315                    crate::cli::safe_print(&formatted_output)?;
316                    Ok(())
317                }
318                Err(e) => Err(format!("Error reading loose object: {e}")),
319            },
320        )
321    }
322
323    /// View an object by hash with rich formatting
324    ///
325    /// # Errors
326    ///
327    /// This function will return an error if:
328    /// - The object cannot be found by hash
329    /// - Multiple objects match a partial hash (disambiguation needed)
330    /// - The formatting operations fail
331    pub fn view_object_by_hash(&self, hash: &str) -> Result<(), String> {
332        use crate::cli::formatters::{CliLooseFormatter, CliPackFormatter};
333        use std::fmt::Write;
334
335        match self.repository.as_ref() {
336            Some(_repo) => {
337                // First try as loose object
338                if let Ok(loose_obj) = self.find_loose_object_by_partial_hash(hash) {
339                    let formatted_output = CliLooseFormatter::format_loose_object(&loose_obj);
340                    crate::cli::safe_print(&formatted_output)?;
341                    return Ok(());
342                }
343
344                // If not found in loose objects, search pack files
345                if let Ok(pack_obj) = self.find_pack_object_by_partial_hash(hash) {
346                    // Format pack object using existing formatter - create single object "pack file"
347                    if let Some(ref object_data) = pack_obj.object_data {
348                        let mut output = String::new();
349                        writeln!(&mut output, "\x1b[1mPACK OBJECT (found by hash)\x1b[0m").unwrap();
350                        writeln!(&mut output, "{}", "─".repeat(50)).unwrap();
351                        writeln!(&mut output).unwrap();
352
353                        // Create a PackObject from the found object and format it
354                        let formatted_pack_obj = crate::tui::model::PackObject {
355                            index: pack_obj.index,
356                            obj_type: pack_obj.obj_type.clone(),
357                            size: pack_obj.size,
358                            sha1: pack_obj.sha1.clone(),
359                            base_info: pack_obj.base_info.clone(),
360                            object_data: Some(object_data.clone()),
361                        };
362
363                        let mut widget =
364                            crate::tui::widget::pack_obj_details::PackObjectWidget::new(
365                                formatted_pack_obj,
366                            );
367                        let formatted_text = widget.text();
368
369                        // Convert ratatui Text to ANSI colored string (reuse formatter logic)
370                        let colored_text = CliPackFormatter::text_to_ansi_string(&formatted_text);
371                        output.push_str(&colored_text);
372
373                        crate::cli::safe_print(&output)?;
374                    } else {
375                        // Fallback to basic info if no object data
376                        crate::cli::safe_println("Pack Object (found by hash):")?;
377                        crate::cli::safe_println(&format!(
378                            "SHA1: {}",
379                            pack_obj.sha1.as_deref().unwrap_or("unknown")
380                        ))?;
381                        crate::cli::safe_println(&format!("Type: {}", pack_obj.obj_type))?;
382                        crate::cli::safe_println(&format!("Size: {} bytes", pack_obj.size))?;
383                    }
384                    return Ok(());
385                }
386
387                Err(format!("Object not found: {hash}"))
388            }
389            None => Err(format!(
390                "Not a git repository: {}",
391                self.repo_path.display()
392            )),
393        }
394    }
395
396    /// Find a loose object by partial hash (4-40 characters)
397    ///
398    /// # Errors
399    ///
400    /// This function will return an error if:
401    /// - No matching objects are found
402    /// - Multiple objects match the partial hash
403    fn find_loose_object_by_partial_hash(
404        &self,
405        partial_hash: &str,
406    ) -> Result<crate::git::loose_object::LooseObject, String> {
407        // For full hash (40 chars), use direct lookup
408        if partial_hash.len() == 40 {
409            return self
410                .repository
411                .as_ref()
412                .expect("Repository should be available for hash lookup")
413                .read_loose_object_by_hash(partial_hash)
414                .map_err(|e| format!("Object not found: {e}"));
415        }
416
417        // For partial hash, we need to search all loose objects
418        match self.list_parsed_loose_objects(10000) {
419            // Large limit for comprehensive search
420            Ok(objects) => {
421                let matches: Vec<_> = objects
422                    .into_iter()
423                    .filter(|obj| obj.object_id.starts_with(partial_hash))
424                    .collect();
425
426                match matches.len() {
427                    0 => Err(format!("No loose objects found matching: {partial_hash}")),
428                    1 => Ok(matches
429                        .into_iter()
430                        .next()
431                        .expect("Should have exactly one match")),
432                    _ => {
433                        let mut error_msg = format!("Multiple objects match '{partial_hash}':\n");
434                        for obj in matches {
435                            use std::fmt::Write;
436                            writeln!(&mut error_msg, "  {} ({})", obj.object_id, obj.object_type)
437                                .expect("Writing to string should not fail");
438                        }
439                        Err(error_msg)
440                    }
441                }
442            }
443            Err(e) => Err(format!("Error searching loose objects: {e}")),
444        }
445    }
446
447    /// Find a pack object by partial hash (4-40 characters)
448    ///
449    /// # Errors
450    ///
451    /// This function will return an error if:
452    /// - No matching objects are found
453    /// - Multiple objects match the partial hash
454    /// - Pack files cannot be read or parsed
455    fn find_pack_object_by_partial_hash(
456        &self,
457        partial_hash: &str,
458    ) -> Result<crate::tui::model::PackObject, String> {
459        use crate::git::pack::{Header, Object};
460        use sha1::Digest;
461
462        // Get all pack files
463        let pack_files = self
464            .list_pack_files()
465            .map_err(|e| format!("Error listing pack files: {e}"))?;
466
467        let mut matches = Vec::new();
468
469        // Search through each pack file
470        for pack_path in pack_files {
471            let pack_data =
472                std::fs::read(&pack_path).map_err(|e| format!("Error reading pack file: {e}"))?;
473
474            if let Ok((mut remaining_data, header)) = Header::parse(&pack_data) {
475                // Parse all objects in this pack file
476                for index in 0..header.object_count {
477                    if let Ok((new_remaining_data, object)) = Object::parse(remaining_data) {
478                        // Calculate SHA-1 hash for this object
479                        let obj_type = object.header.obj_type();
480                        let size = object.header.uncompressed_data_size();
481                        let mut hasher = sha1::Sha1::new();
482                        let header_str = format!("{obj_type} {size}\0");
483                        hasher.update(header_str.as_bytes());
484                        hasher.update(&object.uncompressed_data);
485                        let sha1 = format!("{:x}", hasher.finalize());
486
487                        // Check if this hash matches our partial hash
488                        if sha1.starts_with(partial_hash) {
489                            let pack_obj = crate::tui::model::PackObject {
490                                index: index as usize + 1,
491                                obj_type: obj_type.to_string(),
492                                size: u32::try_from(size).unwrap_or(u32::MAX),
493                                sha1: Some(sha1),
494                                base_info: None, // TODO: Add delta info if needed
495                                object_data: Some(object),
496                            };
497                            matches.push(pack_obj);
498                        }
499
500                        remaining_data = new_remaining_data;
501                    }
502                }
503            }
504        }
505
506        match matches.len() {
507            0 => Err(format!("No pack objects found matching: {partial_hash}")),
508            1 => Ok(matches
509                .into_iter()
510                .next()
511                .expect("Should have exactly one match")),
512            _ => {
513                let mut error_msg = format!("Multiple pack objects match '{partial_hash}':\n");
514                for obj in matches {
515                    use std::fmt::Write;
516                    writeln!(
517                        &mut error_msg,
518                        "  {} ({})",
519                        obj.sha1.as_deref().unwrap_or("unknown"),
520                        obj.obj_type
521                    )
522                    .expect("Writing to string should not fail");
523                }
524                Err(error_msg)
525            }
526        }
527    }
528}