1#![allow(clippy::missing_errors_doc)]
2
3use crate::config::Config;
4use crate::context::{CommitContext, RecentCommit, StagedFile};
5use crate::git::commit::{self, CommitResult};
6use crate::git::files::{
7 RepoFilesInfo, get_ahead_behind, get_all_tracked_files, get_file_statuses,
8 get_unstaged_file_statuses, get_untracked_files,
9};
10use crate::git::utils::is_inside_work_tree;
11use crate::log_debug;
12use anyhow::{Context as AnyhowContext, Result, anyhow};
13use chrono::{DateTime, Utc};
14use git2::{Repository, Tree};
15use std::collections::HashSet;
16use std::env;
17use std::path::{Path, PathBuf};
18use std::process::{Command, Stdio};
19use tempfile::TempDir;
20use url::Url;
21
22#[derive(Debug)]
24pub struct GitRepo {
25 repo_path: PathBuf,
26 #[allow(dead_code)] temp_dir: Option<TempDir>,
29 is_remote: bool,
31 remote_url: Option<String>,
33}
34
35fn execute_hook_command(
36 hook_name: &str,
37 hook_path: &Path,
38 git_dir: &Path,
39 repo_workdir: &Path,
40) -> Result<()> {
41 log_hook_start(hook_name, hook_path, repo_workdir);
42 wait_for_hook(hook_name, hook_command(hook_path, git_dir, repo_workdir))
43}
44
45fn log_hook_start(hook_name: &str, hook_path: &Path, repo_workdir: &Path) {
46 log_debug!("Executing hook: {}", hook_name);
47 log_debug!("Hook path: {:?}", hook_path);
48 log_debug!("Repository working directory: {:?}", repo_workdir);
49}
50
51fn hook_command(hook_path: &Path, git_dir: &Path, repo_workdir: &Path) -> Command {
52 let mut command = Command::new(hook_path);
53 command
54 .current_dir(repo_workdir)
55 .env("GIT_DIR", git_dir)
56 .env("GIT_WORK_TREE", repo_workdir)
57 .stdout(Stdio::piped())
58 .stderr(Stdio::piped());
59 log_debug!("Executing hook command: {:?}", command);
60 command
61}
62
63fn wait_for_hook(hook_name: &str, mut command: Command) -> Result<()> {
64 let mut child = command.spawn()?;
65 pipe_hook_output(&mut child)?;
66
67 let status = child.wait()?;
68 if !status.success() {
69 return Err(anyhow!(
70 "Hook '{}' failed with exit code: {:?}",
71 hook_name,
72 status.code()
73 ));
74 }
75
76 log_debug!("Hook '{}' executed successfully", hook_name);
77 Ok(())
78}
79
80fn pipe_hook_output(child: &mut std::process::Child) -> Result<()> {
81 let stdout = child.stdout.take().context("Could not get stdout")?;
82 let stderr = child.stderr.take().context("Could not get stderr")?;
83
84 std::thread::spawn(move || copy_hook_stream(stdout, std::io::stdout(), "stdout"));
85 std::thread::spawn(move || copy_hook_stream(stderr, std::io::stderr(), "stderr"));
86 Ok(())
87}
88
89fn copy_hook_stream<R, W>(stream: R, mut output: W, name: &str)
90where
91 R: std::io::Read,
92 W: std::io::Write,
93{
94 if let Err(e) = std::io::copy(&mut std::io::BufReader::new(stream), &mut output) {
95 tracing::debug!("Failed to copy hook {name}: {e}");
96 }
97}
98
99impl GitRepo {
100 pub fn new(repo_path: &Path) -> Result<Self> {
110 let repo_path = Repository::discover(repo_path)
111 .ok()
112 .and_then(|repo| {
113 repo.workdir()
114 .map(Path::to_path_buf)
115 .or_else(|| repo.path().parent().map(Path::to_path_buf))
116 })
117 .unwrap_or_else(|| repo_path.to_path_buf());
118
119 Ok(Self {
120 repo_path,
121 temp_dir: None,
122 is_remote: false,
123 remote_url: None,
124 })
125 }
126
127 pub fn new_from_url(repository_url: Option<String>) -> Result<Self> {
137 if let Some(url) = repository_url {
138 Self::clone_remote_repository(&url)
139 } else {
140 let current_dir = env::current_dir()?;
141 Self::new(¤t_dir)
142 }
143 }
144
145 pub fn clone_remote_repository(url: &str) -> Result<Self> {
155 log_debug!("Cloning remote repository from URL: {}", url);
156
157 let _ = Url::parse(url).map_err(|e| anyhow!("Invalid repository URL: {}", e))?;
159
160 let temp_dir = TempDir::new()?;
162 let temp_path = temp_dir.path();
163
164 log_debug!("Created temporary directory for clone: {:?}", temp_path);
165
166 let repo = match Repository::clone(url, temp_path) {
168 Ok(repo) => repo,
169 Err(e) => return Err(anyhow!("Failed to clone repository: {}", e)),
170 };
171
172 log_debug!("Successfully cloned repository to {:?}", repo.path());
173
174 Ok(Self {
175 repo_path: temp_path.to_path_buf(),
176 temp_dir: Some(temp_dir),
177 is_remote: true,
178 remote_url: Some(url.to_string()),
179 })
180 }
181
182 pub fn open_repo(&self) -> Result<Repository, git2::Error> {
184 Repository::open(&self.repo_path)
185 }
186
187 #[must_use]
189 pub fn is_remote(&self) -> bool {
190 self.is_remote
191 }
192
193 #[must_use]
195 pub fn get_remote_url(&self) -> Option<&str> {
196 self.remote_url.as_deref()
197 }
198
199 #[must_use]
201 pub fn repo_path(&self) -> &PathBuf {
202 &self.repo_path
203 }
204
205 pub fn update_remote(&self) -> Result<()> {
207 if !self.is_remote {
208 return Err(anyhow!("Not a remote repository"));
209 }
210
211 log_debug!("Updating remote repository");
212 let repo = self.open_repo()?;
213
214 let remotes = repo.remotes()?;
216 let remote_name = remotes
217 .iter()
218 .flatten()
219 .next()
220 .ok_or_else(|| anyhow!("No remote found"))?;
221
222 let mut remote = repo.find_remote(remote_name)?;
223 let fetch_refspec_storage: Vec<String> = remote
224 .fetch_refspecs()?
225 .iter()
226 .flatten()
227 .map(std::string::ToString::to_string)
228 .collect();
229 let fetch_refspecs: Vec<&str> = fetch_refspec_storage
230 .iter()
231 .map(std::string::String::as_str)
232 .collect();
233
234 remote.fetch(&fetch_refspecs, None, None)?;
237
238 log_debug!("Successfully updated remote repository");
239 Ok(())
240 }
241
242 pub fn get_current_branch(&self) -> Result<String> {
248 let repo = self.open_repo()?;
249 let head = repo.head()?;
250 let branch_name = head.shorthand().unwrap_or("HEAD detached").to_string();
251 log_debug!("Current branch: {}", branch_name);
252 Ok(branch_name)
253 }
254
255 pub fn get_default_base_ref(&self) -> Result<String> {
260 let repo = self.open_repo()?;
261 let local_branches = collect_branch_names(&repo, git2::BranchType::Local)?;
262 let remote_branches = collect_branch_names(&repo, git2::BranchType::Remote)?;
263
264 if let Some(base) = resolve_remote_head_base(&repo, "origin", &local_branches) {
265 return Ok(base);
266 }
267
268 if let Ok(remotes) = repo.remotes() {
269 for remote_name in remotes.iter().flatten() {
270 if remote_name == "origin" {
271 continue;
272 }
273 if let Some(base) = resolve_remote_head_base(&repo, remote_name, &local_branches) {
274 return Ok(base);
275 }
276 }
277 }
278
279 for candidate in ["main", "master", "trunk", "develop", "dev", "default"] {
280 if local_branches.contains(candidate) {
281 return Ok(candidate.to_string());
282 }
283 }
284
285 for candidate in [
286 "origin/main",
287 "origin/master",
288 "origin/trunk",
289 "origin/develop",
290 "origin/dev",
291 "origin/default",
292 ] {
293 if remote_branches.contains(candidate) {
294 return Ok(candidate.to_string());
295 }
296 }
297
298 self.get_current_branch()
299 }
300
301 pub fn execute_hook(&self, hook_name: &str) -> Result<()> {
311 if self.is_remote {
312 log_debug!("Skipping hook execution for remote repository");
313 return Ok(());
314 }
315
316 let repo = self.open_repo()?;
317 let hook_path = repo.path().join("hooks").join(hook_name);
318
319 if !hook_path.exists() {
320 log_debug!("Hook '{}' not found at {:?}", hook_name, hook_path);
321 return Ok(());
322 }
323
324 let repo_workdir = repo
325 .workdir()
326 .context("Repository has no working directory")?;
327 execute_hook_command(hook_name, &hook_path, repo.path(), repo_workdir)
328 }
329
330 pub fn get_repo_root() -> Result<PathBuf> {
332 if !is_inside_work_tree()? {
334 return Err(anyhow!(
335 "Not in a Git repository. Please run this command from within a Git repository."
336 ));
337 }
338
339 let output = Command::new("git")
341 .args(["rev-parse", "--show-toplevel"])
342 .output()
343 .context("Failed to execute git command")?;
344
345 if !output.status.success() {
346 return Err(anyhow!(
347 "Failed to get repository root: {}",
348 String::from_utf8_lossy(&output.stderr)
349 ));
350 }
351
352 let root = String::from_utf8(output.stdout)
354 .context("Invalid UTF-8 output from git command")?
355 .trim()
356 .to_string();
357
358 Ok(PathBuf::from(root))
359 }
360
361 pub fn get_readme_at_commit(&self, commit_ish: &str) -> Result<Option<String>> {
371 let repo = self.open_repo()?;
372 let obj = repo.revparse_single(commit_ish)?;
373 let tree = obj.peel_to_tree()?;
374
375 Self::find_readme_in_tree(&repo, &tree)
376 .context("Failed to find and read README at specified commit")
377 }
378
379 fn find_readme_in_tree(repo: &Repository, tree: &Tree) -> Result<Option<String>> {
389 log_debug!("Searching for README file in the repository");
390
391 let readme_patterns = [
392 "README.md",
393 "README.markdown",
394 "README.txt",
395 "README",
396 "Readme.md",
397 "readme.md",
398 ];
399
400 for entry in tree {
401 let name = entry.name().unwrap_or("");
402 if readme_patterns
403 .iter()
404 .any(|&pattern| name.eq_ignore_ascii_case(pattern))
405 {
406 let object = entry.to_object(repo)?;
407 if let Some(blob) = object.as_blob()
408 && let Ok(content) = std::str::from_utf8(blob.content())
409 {
410 log_debug!("README file found: {}", name);
411 return Ok(Some(content.to_string()));
412 }
413 }
414 }
415
416 log_debug!("No README file found");
417 Ok(None)
418 }
419
420 pub fn extract_files_info(&self, include_unstaged: bool) -> Result<RepoFilesInfo> {
422 let repo = self.open_repo()?;
423
424 let branch = self.get_current_branch()?;
426 let recent_commits = self.get_recent_commits(5)?;
427
428 let mut staged_files = get_file_statuses(&repo)?;
430 if include_unstaged {
431 let unstaged_files = self.get_unstaged_files()?;
432 staged_files.extend(unstaged_files);
433 log_debug!("Combined {} files (staged + unstaged)", staged_files.len());
434 }
435
436 let file_paths: Vec<String> = staged_files.iter().map(|file| file.path.clone()).collect();
438
439 Ok(RepoFilesInfo {
440 branch,
441 recent_commits,
442 staged_files,
443 file_paths,
444 })
445 }
446
447 pub fn get_unstaged_files(&self) -> Result<Vec<StagedFile>> {
449 let repo = self.open_repo()?;
450 get_unstaged_file_statuses(&repo)
451 }
452
453 pub fn get_ref_diff_full(&self, from: &str, to: &str) -> Result<String> {
461 let repo = self.open_repo()?;
462
463 let from_commit = repo.revparse_single(from)?.peel_to_commit()?;
465 let to_commit = repo.revparse_single(to)?.peel_to_commit()?;
466
467 let from_tree = from_commit.tree()?;
468 let to_tree = to_commit.tree()?;
469
470 let diff = repo.diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)?;
472
473 let mut diff_string = String::new();
475 diff.print(git2::DiffFormat::Patch, |delta, _hunk, line| {
476 if matches!(line.origin(), '+' | '-' | ' ') {
478 diff_string.push(line.origin());
479 }
480 diff_string.push_str(&String::from_utf8_lossy(line.content()));
482
483 if line.origin() == 'F'
484 && !diff_string.contains("diff --git")
485 && let Some(new_file) = delta.new_file().path()
486 {
487 let header = format!("diff --git a/{0} b/{0}\n", new_file.display());
488 if !diff_string.ends_with(&header) {
489 diff_string.insert_str(
490 diff_string.rfind("---").unwrap_or(diff_string.len()),
491 &header,
492 );
493 }
494 }
495 true
496 })?;
497
498 Ok(diff_string)
499 }
500
501 pub fn get_staged_diff_full(&self) -> Result<String> {
509 let repo = self.open_repo()?;
510
511 let head = repo.head()?;
513 let head_tree = head.peel_to_tree()?;
514
515 let diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?;
517
518 let mut diff_string = String::new();
520 diff.print(git2::DiffFormat::Patch, |delta, _hunk, line| {
521 match line.origin() {
523 'H' => {
524 diff_string.push_str(&String::from_utf8_lossy(line.content()));
526 }
527 'F' => {
528 diff_string.push_str(&String::from_utf8_lossy(line.content()));
530 }
531 '+' | '-' | ' ' => {
532 diff_string.push(line.origin());
533 diff_string.push_str(&String::from_utf8_lossy(line.content()));
534 }
535 '>' | '<' | '=' => {
536 diff_string.push_str(&String::from_utf8_lossy(line.content()));
538 }
539 _ => {
540 diff_string.push_str(&String::from_utf8_lossy(line.content()));
542 }
543 }
544
545 if line.origin() == 'F'
547 && !diff_string.contains("diff --git")
548 && let Some(new_file) = delta.new_file().path()
549 {
550 let header = format!("diff --git a/{0} b/{0}\n", new_file.display());
551 if !diff_string.ends_with(&header) {
552 diff_string.insert_str(
553 diff_string.rfind("---").unwrap_or(diff_string.len()),
554 &header,
555 );
556 }
557 }
558 true
559 })?;
560
561 Ok(diff_string)
562 }
563
564 fn create_commit_context(
577 &self,
578 branch: String,
579 recent_commits: Vec<RecentCommit>,
580 staged_files: Vec<StagedFile>,
581 ) -> Result<CommitContext> {
582 let repo = self.open_repo()?;
584 let user_name = repo.config()?.get_string("user.name").unwrap_or_default();
585 let user_email = repo.config()?.get_string("user.email").unwrap_or_default();
586
587 Ok(CommitContext::new(
589 branch,
590 recent_commits,
591 staged_files,
592 user_name,
593 user_email,
594 ))
595 }
596
597 pub fn get_git_info(&self, _config: &Config) -> Result<CommitContext> {
607 let repo = self.open_repo()?;
609 log_debug!("Getting git info for repo path: {:?}", repo.path());
610
611 let branch = self.get_current_branch()?;
612 let recent_commits = self.get_recent_commits(5)?;
613 let staged_files = get_file_statuses(&repo)?;
614
615 self.create_commit_context(branch, recent_commits, staged_files)
617 }
618
619 pub fn get_git_info_with_unstaged(
630 &self,
631 _config: &Config,
632 include_unstaged: bool,
633 ) -> Result<CommitContext> {
634 log_debug!("Getting git info with unstaged flag: {}", include_unstaged);
635
636 let files_info = self.extract_files_info(include_unstaged)?;
638
639 self.create_commit_context(
641 files_info.branch,
642 files_info.recent_commits,
643 files_info.staged_files,
644 )
645 }
646
647 pub fn get_git_info_for_branch_diff(
659 &self,
660 _config: &Config,
661 base_branch: &str,
662 target_branch: &str,
663 ) -> Result<CommitContext> {
664 log_debug!(
665 "Getting git info for branch diff: {} -> {}",
666 base_branch,
667 target_branch
668 );
669 let repo = self.open_repo()?;
670
671 let (display_branch, recent_commits, _file_paths) =
673 commit::extract_branch_diff_info(&repo, base_branch, target_branch)?;
674
675 let branch_files = commit::get_branch_diff_files(&repo, base_branch, target_branch)?;
677
678 self.create_commit_context(display_branch, recent_commits, branch_files)
680 }
681
682 pub fn get_git_info_for_commit_range(
694 &self,
695 _config: &Config,
696 from: &str,
697 to: &str,
698 ) -> Result<CommitContext> {
699 log_debug!("Getting git info for commit range: {} -> {}", from, to);
700 let repo = self.open_repo()?;
701
702 let (display_range, recent_commits, _file_paths) =
704 commit::extract_commit_range_info(&repo, from, to)?;
705
706 let range_files = commit::get_commit_range_files(&repo, from, to)?;
708
709 self.create_commit_context(display_range, recent_commits, range_files)
711 }
712
713 pub fn get_commits_for_pr(&self, from: &str, to: &str) -> Result<Vec<String>> {
715 let repo = self.open_repo()?;
716 commit::get_commits_for_pr(&repo, from, to)
717 }
718
719 pub fn get_commits_in_range(&self, from: &str, to: &str) -> Result<Vec<RecentCommit>> {
721 let repo = self.open_repo()?;
722 let mut commits =
723 commit::get_commits_between_with_callback(
724 &repo,
725 from,
726 to,
727 |commit| Ok(commit.clone()),
728 )?;
729 commits.reverse();
730 Ok(commits)
731 }
732
733 pub fn get_commit_range_files(&self, from: &str, to: &str) -> Result<Vec<StagedFile>> {
735 let repo = self.open_repo()?;
736 commit::get_commit_range_files(&repo, from, to)
737 }
738
739 pub fn get_recent_commits(&self, count: usize) -> Result<Vec<RecentCommit>> {
749 let repo = self.open_repo()?;
750 log_debug!("Fetching {} recent commits", count);
751 let mut revwalk = repo.revwalk()?;
752 revwalk.push_head()?;
753
754 let commits = revwalk
755 .take(count)
756 .map(|oid| {
757 let oid = oid?;
758 let commit = repo.find_commit(oid)?;
759 let author = commit.author();
760 Ok(RecentCommit {
761 hash: oid.to_string(),
762 message: commit.message().unwrap_or_default().to_string(),
763 author: author.name().unwrap_or_default().to_string(),
764 timestamp: DateTime::<Utc>::from_timestamp(commit.time().seconds(), 0)
765 .map_or_else(
766 || commit.time().seconds().to_string(),
767 |timestamp| timestamp.to_rfc3339(),
768 ),
769 })
770 })
771 .collect::<Result<Vec<_>>>()?;
772
773 log_debug!("Retrieved {} recent commits", commits.len());
774 Ok(commits)
775 }
776
777 pub fn commit_and_verify(&self, message: &str) -> Result<CommitResult> {
787 if self.is_remote {
788 return Err(anyhow!(
789 "Cannot commit to a remote repository in read-only mode"
790 ));
791 }
792
793 let repo = self.open_repo()?;
794 match commit::commit(&repo, message, self.is_remote) {
795 Ok(result) => {
796 if let Err(e) = self.execute_hook("post-commit") {
797 log_debug!("Post-commit hook failed: {}", e);
798 }
799 Ok(result)
800 }
801 Err(e) => {
802 log_debug!("Commit failed: {}", e);
803 Err(e)
804 }
805 }
806 }
807
808 pub fn get_git_info_for_commit(
819 &self,
820 _config: &Config,
821 commit_id: &str,
822 ) -> Result<CommitContext> {
823 log_debug!("Getting git info for commit: {}", commit_id);
824 let repo = self.open_repo()?;
825
826 let branch = self.get_current_branch()?;
828
829 let commit_info = commit::extract_commit_info(&repo, commit_id, &branch)?;
831
832 let commit_files = commit::get_commit_files(&repo, commit_id)?;
834
835 self.create_commit_context(commit_info.branch, vec![commit_info.commit], commit_files)
837 }
838
839 pub fn get_commit_date(&self, commit_ish: &str) -> Result<String> {
841 let repo = self.open_repo()?;
842 commit::get_commit_date(&repo, commit_ish)
843 }
844
845 pub fn get_commits_between_with_callback<T, F>(
847 &self,
848 from: &str,
849 to: &str,
850 callback: F,
851 ) -> Result<Vec<T>>
852 where
853 F: FnMut(&RecentCommit) -> Result<T>,
854 {
855 let repo = self.open_repo()?;
856 commit::get_commits_between_with_callback(&repo, from, to, callback)
857 }
858
859 pub fn commit(&self, message: &str) -> Result<CommitResult> {
861 let repo = self.open_repo()?;
862 commit::commit(&repo, message, self.is_remote)
863 }
864
865 pub fn amend_commit(&self, message: &str) -> Result<CommitResult> {
867 let repo = self.open_repo()?;
868 commit::amend_commit(&repo, message, self.is_remote)
869 }
870
871 pub fn get_head_commit_message(&self) -> Result<String> {
873 let repo = self.open_repo()?;
874 commit::get_head_commit_message(&repo)
875 }
876
877 pub fn is_inside_work_tree() -> Result<bool> {
879 is_inside_work_tree()
880 }
881
882 pub fn get_commit_files(&self, commit_id: &str) -> Result<Vec<StagedFile>> {
884 let repo = self.open_repo()?;
885 commit::get_commit_files(&repo, commit_id)
886 }
887
888 pub fn get_file_paths_for_commit(&self, commit_id: &str) -> Result<Vec<String>> {
890 let repo = self.open_repo()?;
891 commit::get_file_paths_for_commit(&repo, commit_id)
892 }
893
894 pub fn stage_file(&self, path: &Path) -> Result<()> {
896 let repo = self.open_repo()?;
897 let mut index = repo.index()?;
898
899 let full_path = self.repo_path.join(path);
901 if full_path.exists() {
902 index.add_path(path)?;
903 } else {
904 index.remove_path(path)?;
906 }
907
908 index.write()?;
909 Ok(())
910 }
911
912 pub fn unstage_file(&self, path: &Path) -> Result<()> {
914 let repo = self.open_repo()?;
915
916 let head = repo.head()?;
918 let head_commit = head.peel_to_commit()?;
919 let head_tree = head_commit.tree()?;
920
921 let mut index = repo.index()?;
922
923 if let Ok(entry) = head_tree.get_path(path) {
925 let blob = repo.find_blob(entry.id())?;
927 #[allow(
928 clippy::cast_sign_loss,
929 clippy::cast_possible_truncation,
930 clippy::as_conversions
931 )]
932 let file_mode = entry.filemode() as u32;
933 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
934 let file_size = blob.content().len() as u32;
935 index.add_frombuffer(
936 &git2::IndexEntry {
937 ctime: git2::IndexTime::new(0, 0),
938 mtime: git2::IndexTime::new(0, 0),
939 dev: 0,
940 ino: 0,
941 mode: file_mode,
942 uid: 0,
943 gid: 0,
944 file_size,
945 id: entry.id(),
946 flags: 0,
947 flags_extended: 0,
948 path: path.to_string_lossy().as_bytes().to_vec(),
949 },
950 blob.content(),
951 )?;
952 } else {
953 index.remove_path(path)?;
955 }
956
957 index.write()?;
958 Ok(())
959 }
960
961 pub fn stage_all(&self) -> Result<()> {
963 let repo = self.open_repo()?;
964 let mut index = repo.index()?;
965 index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
966 index.write()?;
967 Ok(())
968 }
969
970 pub fn unstage_all(&self) -> Result<()> {
972 let repo = self.open_repo()?;
973 let head = repo.head()?;
974 let head_commit = head.peel_to_commit()?;
975 repo.reset(head_commit.as_object(), git2::ResetType::Mixed, None)?;
976 Ok(())
977 }
978
979 pub fn get_untracked_files(&self) -> Result<Vec<String>> {
981 let repo = self.open_repo()?;
982 get_untracked_files(&repo)
983 }
984
985 pub fn get_all_tracked_files(&self) -> Result<Vec<String>> {
987 let repo = self.open_repo()?;
988 get_all_tracked_files(&repo)
989 }
990
991 #[must_use]
995 pub fn get_ahead_behind(&self) -> (usize, usize) {
996 let Ok(repo) = self.open_repo() else {
997 return (0, 0);
998 };
999 get_ahead_behind(&repo)
1000 }
1001}
1002
1003fn collect_branch_names(
1004 repo: &Repository,
1005 branch_type: git2::BranchType,
1006) -> Result<HashSet<String>> {
1007 let mut names = HashSet::new();
1008 for branch in repo.branches(Some(branch_type))?.flatten() {
1009 if let Ok(Some(name)) = branch.0.name() {
1010 names.insert(name.to_string());
1011 }
1012 }
1013 Ok(names)
1014}
1015
1016fn resolve_remote_head_base(
1017 repo: &Repository,
1018 remote_name: &str,
1019 local_branches: &HashSet<String>,
1020) -> Option<String> {
1021 let reference_name = format!("refs/remotes/{remote_name}/HEAD");
1022 let Ok(reference) = repo.find_reference(&reference_name) else {
1023 return None;
1024 };
1025 let symbolic_target = reference.symbolic_target()?;
1026 let remote_ref = symbolic_target.strip_prefix("refs/remotes/")?;
1027
1028 if let Some((_, local_candidate)) = remote_ref.split_once('/')
1029 && local_branches.contains(local_candidate)
1030 {
1031 return Some(local_candidate.to_string());
1032 }
1033
1034 Some(remote_ref.to_string())
1035}
1036
1037impl Drop for GitRepo {
1038 fn drop(&mut self) {
1039 if self.is_remote {
1041 log_debug!("Cleaning up temporary repository at {:?}", self.repo_path);
1042 }
1043 }
1044}