1use crate::git::repository::{Repository, RepositoryError};
2use std::path::{Path, PathBuf};
3
4pub struct GitPlumber {
6 repo_path: PathBuf,
7 repository: Option<Repository>,
8}
9
10impl GitPlumber {
11 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 #[must_use]
24 pub fn get_repo_path(&self) -> &Path {
25 &self.repo_path
26 }
27
28 #[must_use]
30 pub const fn get_repository(&self) -> Option<&Repository> {
31 self.repository.as_ref()
32 }
33
34 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 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 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 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 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 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 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 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 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 pub fn parse_pack_file(&self, path: &Path) -> Result<(), String> {
224 let pack_data = std::fs::read(path).map_err(|e| format!("Error reading file: {e}"))?;
226
227 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 pub fn parse_pack_file_rich(&self, path: &Path) -> Result<(), String> {
262 use crate::cli::formatters::CliPackFormatter;
263
264 let pack_data = std::fs::read(path).map_err(|e| format!("Error reading file: {e}"))?;
266
267 match crate::git::pack::Header::parse(&pack_data) {
269 Ok((mut remaining_data, header)) => {
270 let mut objects = Vec::new();
271
272 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 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 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 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 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 let Ok(pack_obj) = self.find_pack_object_by_partial_hash(hash) {
346 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 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 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 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 fn find_loose_object_by_partial_hash(
404 &self,
405 partial_hash: &str,
406 ) -> Result<crate::git::loose_object::LooseObject, String> {
407 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 match self.list_parsed_loose_objects(10000) {
419 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 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 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 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 for index in 0..header.object_count {
477 if let Ok((new_remaining_data, object)) = Object::parse(remaining_data) {
478 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 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, 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}