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
35impl GitRepo {
36 pub fn new(repo_path: &Path) -> Result<Self> {
46 let repo_path = Repository::discover(repo_path)
47 .ok()
48 .and_then(|repo| {
49 repo.workdir()
50 .map(Path::to_path_buf)
51 .or_else(|| repo.path().parent().map(Path::to_path_buf))
52 })
53 .unwrap_or_else(|| repo_path.to_path_buf());
54
55 Ok(Self {
56 repo_path,
57 temp_dir: None,
58 is_remote: false,
59 remote_url: None,
60 })
61 }
62
63 pub fn new_from_url(repository_url: Option<String>) -> Result<Self> {
73 if let Some(url) = repository_url {
74 Self::clone_remote_repository(&url)
75 } else {
76 let current_dir = env::current_dir()?;
77 Self::new(¤t_dir)
78 }
79 }
80
81 pub fn clone_remote_repository(url: &str) -> Result<Self> {
91 log_debug!("Cloning remote repository from URL: {}", url);
92
93 let _ = Url::parse(url).map_err(|e| anyhow!("Invalid repository URL: {}", e))?;
95
96 let temp_dir = TempDir::new()?;
98 let temp_path = temp_dir.path();
99
100 log_debug!("Created temporary directory for clone: {:?}", temp_path);
101
102 let repo = match Repository::clone(url, temp_path) {
104 Ok(repo) => repo,
105 Err(e) => return Err(anyhow!("Failed to clone repository: {}", e)),
106 };
107
108 log_debug!("Successfully cloned repository to {:?}", repo.path());
109
110 Ok(Self {
111 repo_path: temp_path.to_path_buf(),
112 temp_dir: Some(temp_dir),
113 is_remote: true,
114 remote_url: Some(url.to_string()),
115 })
116 }
117
118 pub fn open_repo(&self) -> Result<Repository, git2::Error> {
120 Repository::open(&self.repo_path)
121 }
122
123 #[must_use]
125 pub fn is_remote(&self) -> bool {
126 self.is_remote
127 }
128
129 #[must_use]
131 pub fn get_remote_url(&self) -> Option<&str> {
132 self.remote_url.as_deref()
133 }
134
135 #[must_use]
137 pub fn repo_path(&self) -> &PathBuf {
138 &self.repo_path
139 }
140
141 pub fn update_remote(&self) -> Result<()> {
143 if !self.is_remote {
144 return Err(anyhow!("Not a remote repository"));
145 }
146
147 log_debug!("Updating remote repository");
148 let repo = self.open_repo()?;
149
150 let remotes = repo.remotes()?;
152 let remote_name = remotes
153 .iter()
154 .flatten()
155 .next()
156 .ok_or_else(|| anyhow!("No remote found"))?;
157
158 let mut remote = repo.find_remote(remote_name)?;
159 let fetch_refspec_storage: Vec<String> = remote
160 .fetch_refspecs()?
161 .iter()
162 .flatten()
163 .map(std::string::ToString::to_string)
164 .collect();
165 let fetch_refspecs: Vec<&str> = fetch_refspec_storage
166 .iter()
167 .map(std::string::String::as_str)
168 .collect();
169
170 remote.fetch(&fetch_refspecs, None, None)?;
173
174 log_debug!("Successfully updated remote repository");
175 Ok(())
176 }
177
178 pub fn get_current_branch(&self) -> Result<String> {
184 let repo = self.open_repo()?;
185 let head = repo.head()?;
186 let branch_name = head.shorthand().unwrap_or("HEAD detached").to_string();
187 log_debug!("Current branch: {}", branch_name);
188 Ok(branch_name)
189 }
190
191 pub fn get_default_base_ref(&self) -> Result<String> {
196 let repo = self.open_repo()?;
197 let local_branches = collect_branch_names(&repo, git2::BranchType::Local)?;
198 let remote_branches = collect_branch_names(&repo, git2::BranchType::Remote)?;
199
200 if let Some(base) = resolve_remote_head_base(&repo, "origin", &local_branches) {
201 return Ok(base);
202 }
203
204 if let Ok(remotes) = repo.remotes() {
205 for remote_name in remotes.iter().flatten() {
206 if remote_name == "origin" {
207 continue;
208 }
209 if let Some(base) = resolve_remote_head_base(&repo, remote_name, &local_branches) {
210 return Ok(base);
211 }
212 }
213 }
214
215 for candidate in ["main", "master", "trunk", "develop", "dev", "default"] {
216 if local_branches.contains(candidate) {
217 return Ok(candidate.to_string());
218 }
219 }
220
221 for candidate in [
222 "origin/main",
223 "origin/master",
224 "origin/trunk",
225 "origin/develop",
226 "origin/dev",
227 "origin/default",
228 ] {
229 if remote_branches.contains(candidate) {
230 return Ok(candidate.to_string());
231 }
232 }
233
234 self.get_current_branch()
235 }
236
237 pub fn execute_hook(&self, hook_name: &str) -> Result<()> {
247 if self.is_remote {
248 log_debug!("Skipping hook execution for remote repository");
249 return Ok(());
250 }
251
252 let repo = self.open_repo()?;
253 let hook_path = repo.path().join("hooks").join(hook_name);
254
255 if hook_path.exists() {
256 log_debug!("Executing hook: {}", hook_name);
257 log_debug!("Hook path: {:?}", hook_path);
258
259 let repo_workdir = repo
261 .workdir()
262 .context("Repository has no working directory")?;
263 log_debug!("Repository working directory: {:?}", repo_workdir);
264
265 let mut command = Command::new(&hook_path);
267 command
268 .current_dir(repo_workdir) .env("GIT_DIR", repo.path()) .env("GIT_WORK_TREE", repo_workdir) .stdout(Stdio::piped())
272 .stderr(Stdio::piped());
273
274 log_debug!("Executing hook command: {:?}", command);
275
276 let mut child = command.spawn()?;
277
278 let stdout = child.stdout.take().context("Could not get stdout")?;
279 let stderr = child.stderr.take().context("Could not get stderr")?;
280
281 std::thread::spawn(move || {
282 if let Err(e) =
283 std::io::copy(&mut std::io::BufReader::new(stdout), &mut std::io::stdout())
284 {
285 tracing::debug!("Failed to copy hook stdout: {e}");
286 }
287 });
288 std::thread::spawn(move || {
289 if let Err(e) =
290 std::io::copy(&mut std::io::BufReader::new(stderr), &mut std::io::stderr())
291 {
292 tracing::debug!("Failed to copy hook stderr: {e}");
293 }
294 });
295
296 let status = child.wait()?;
297
298 if !status.success() {
299 return Err(anyhow!(
300 "Hook '{}' failed with exit code: {:?}",
301 hook_name,
302 status.code()
303 ));
304 }
305
306 log_debug!("Hook '{}' executed successfully", hook_name);
307 } else {
308 log_debug!("Hook '{}' not found at {:?}", hook_name, hook_path);
309 }
310
311 Ok(())
312 }
313
314 pub fn get_repo_root() -> Result<PathBuf> {
316 if !is_inside_work_tree()? {
318 return Err(anyhow!(
319 "Not in a Git repository. Please run this command from within a Git repository."
320 ));
321 }
322
323 let output = Command::new("git")
325 .args(["rev-parse", "--show-toplevel"])
326 .output()
327 .context("Failed to execute git command")?;
328
329 if !output.status.success() {
330 return Err(anyhow!(
331 "Failed to get repository root: {}",
332 String::from_utf8_lossy(&output.stderr)
333 ));
334 }
335
336 let root = String::from_utf8(output.stdout)
338 .context("Invalid UTF-8 output from git command")?
339 .trim()
340 .to_string();
341
342 Ok(PathBuf::from(root))
343 }
344
345 pub fn get_readme_at_commit(&self, commit_ish: &str) -> Result<Option<String>> {
355 let repo = self.open_repo()?;
356 let obj = repo.revparse_single(commit_ish)?;
357 let tree = obj.peel_to_tree()?;
358
359 Self::find_readme_in_tree(&repo, &tree)
360 .context("Failed to find and read README at specified commit")
361 }
362
363 fn find_readme_in_tree(repo: &Repository, tree: &Tree) -> Result<Option<String>> {
373 log_debug!("Searching for README file in the repository");
374
375 let readme_patterns = [
376 "README.md",
377 "README.markdown",
378 "README.txt",
379 "README",
380 "Readme.md",
381 "readme.md",
382 ];
383
384 for entry in tree {
385 let name = entry.name().unwrap_or("");
386 if readme_patterns
387 .iter()
388 .any(|&pattern| name.eq_ignore_ascii_case(pattern))
389 {
390 let object = entry.to_object(repo)?;
391 if let Some(blob) = object.as_blob()
392 && let Ok(content) = std::str::from_utf8(blob.content())
393 {
394 log_debug!("README file found: {}", name);
395 return Ok(Some(content.to_string()));
396 }
397 }
398 }
399
400 log_debug!("No README file found");
401 Ok(None)
402 }
403
404 pub fn extract_files_info(&self, include_unstaged: bool) -> Result<RepoFilesInfo> {
406 let repo = self.open_repo()?;
407
408 let branch = self.get_current_branch()?;
410 let recent_commits = self.get_recent_commits(5)?;
411
412 let mut staged_files = get_file_statuses(&repo)?;
414 if include_unstaged {
415 let unstaged_files = self.get_unstaged_files()?;
416 staged_files.extend(unstaged_files);
417 log_debug!("Combined {} files (staged + unstaged)", staged_files.len());
418 }
419
420 let file_paths: Vec<String> = staged_files.iter().map(|file| file.path.clone()).collect();
422
423 Ok(RepoFilesInfo {
424 branch,
425 recent_commits,
426 staged_files,
427 file_paths,
428 })
429 }
430
431 pub fn get_unstaged_files(&self) -> Result<Vec<StagedFile>> {
433 let repo = self.open_repo()?;
434 get_unstaged_file_statuses(&repo)
435 }
436
437 pub fn get_ref_diff_full(&self, from: &str, to: &str) -> Result<String> {
445 let repo = self.open_repo()?;
446
447 let from_commit = repo.revparse_single(from)?.peel_to_commit()?;
449 let to_commit = repo.revparse_single(to)?.peel_to_commit()?;
450
451 let from_tree = from_commit.tree()?;
452 let to_tree = to_commit.tree()?;
453
454 let diff = repo.diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)?;
456
457 let mut diff_string = String::new();
459 diff.print(git2::DiffFormat::Patch, |delta, _hunk, line| {
460 if matches!(line.origin(), '+' | '-' | ' ') {
462 diff_string.push(line.origin());
463 }
464 diff_string.push_str(&String::from_utf8_lossy(line.content()));
466
467 if line.origin() == 'F'
468 && !diff_string.contains("diff --git")
469 && let Some(new_file) = delta.new_file().path()
470 {
471 let header = format!("diff --git a/{0} b/{0}\n", new_file.display());
472 if !diff_string.ends_with(&header) {
473 diff_string.insert_str(
474 diff_string.rfind("---").unwrap_or(diff_string.len()),
475 &header,
476 );
477 }
478 }
479 true
480 })?;
481
482 Ok(diff_string)
483 }
484
485 pub fn get_staged_diff_full(&self) -> Result<String> {
493 let repo = self.open_repo()?;
494
495 let head = repo.head()?;
497 let head_tree = head.peel_to_tree()?;
498
499 let diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?;
501
502 let mut diff_string = String::new();
504 diff.print(git2::DiffFormat::Patch, |delta, _hunk, line| {
505 match line.origin() {
507 'H' => {
508 diff_string.push_str(&String::from_utf8_lossy(line.content()));
510 }
511 'F' => {
512 diff_string.push_str(&String::from_utf8_lossy(line.content()));
514 }
515 '+' | '-' | ' ' => {
516 diff_string.push(line.origin());
517 diff_string.push_str(&String::from_utf8_lossy(line.content()));
518 }
519 '>' | '<' | '=' => {
520 diff_string.push_str(&String::from_utf8_lossy(line.content()));
522 }
523 _ => {
524 diff_string.push_str(&String::from_utf8_lossy(line.content()));
526 }
527 }
528
529 if line.origin() == 'F'
531 && !diff_string.contains("diff --git")
532 && let Some(new_file) = delta.new_file().path()
533 {
534 let header = format!("diff --git a/{0} b/{0}\n", new_file.display());
535 if !diff_string.ends_with(&header) {
536 diff_string.insert_str(
537 diff_string.rfind("---").unwrap_or(diff_string.len()),
538 &header,
539 );
540 }
541 }
542 true
543 })?;
544
545 Ok(diff_string)
546 }
547
548 fn create_commit_context(
561 &self,
562 branch: String,
563 recent_commits: Vec<RecentCommit>,
564 staged_files: Vec<StagedFile>,
565 ) -> Result<CommitContext> {
566 let repo = self.open_repo()?;
568 let user_name = repo.config()?.get_string("user.name").unwrap_or_default();
569 let user_email = repo.config()?.get_string("user.email").unwrap_or_default();
570
571 Ok(CommitContext::new(
573 branch,
574 recent_commits,
575 staged_files,
576 user_name,
577 user_email,
578 ))
579 }
580
581 pub fn get_git_info(&self, _config: &Config) -> Result<CommitContext> {
591 let repo = self.open_repo()?;
593 log_debug!("Getting git info for repo path: {:?}", repo.path());
594
595 let branch = self.get_current_branch()?;
596 let recent_commits = self.get_recent_commits(5)?;
597 let staged_files = get_file_statuses(&repo)?;
598
599 self.create_commit_context(branch, recent_commits, staged_files)
601 }
602
603 pub fn get_git_info_with_unstaged(
614 &self,
615 _config: &Config,
616 include_unstaged: bool,
617 ) -> Result<CommitContext> {
618 log_debug!("Getting git info with unstaged flag: {}", include_unstaged);
619
620 let files_info = self.extract_files_info(include_unstaged)?;
622
623 self.create_commit_context(
625 files_info.branch,
626 files_info.recent_commits,
627 files_info.staged_files,
628 )
629 }
630
631 pub fn get_git_info_for_branch_diff(
643 &self,
644 _config: &Config,
645 base_branch: &str,
646 target_branch: &str,
647 ) -> Result<CommitContext> {
648 log_debug!(
649 "Getting git info for branch diff: {} -> {}",
650 base_branch,
651 target_branch
652 );
653 let repo = self.open_repo()?;
654
655 let (display_branch, recent_commits, _file_paths) =
657 commit::extract_branch_diff_info(&repo, base_branch, target_branch)?;
658
659 let branch_files = commit::get_branch_diff_files(&repo, base_branch, target_branch)?;
661
662 self.create_commit_context(display_branch, recent_commits, branch_files)
664 }
665
666 pub fn get_git_info_for_commit_range(
678 &self,
679 _config: &Config,
680 from: &str,
681 to: &str,
682 ) -> Result<CommitContext> {
683 log_debug!("Getting git info for commit range: {} -> {}", from, to);
684 let repo = self.open_repo()?;
685
686 let (display_range, recent_commits, _file_paths) =
688 commit::extract_commit_range_info(&repo, from, to)?;
689
690 let range_files = commit::get_commit_range_files(&repo, from, to)?;
692
693 self.create_commit_context(display_range, recent_commits, range_files)
695 }
696
697 pub fn get_commits_for_pr(&self, from: &str, to: &str) -> Result<Vec<String>> {
699 let repo = self.open_repo()?;
700 commit::get_commits_for_pr(&repo, from, to)
701 }
702
703 pub fn get_commits_in_range(&self, from: &str, to: &str) -> Result<Vec<RecentCommit>> {
705 let repo = self.open_repo()?;
706 let mut commits =
707 commit::get_commits_between_with_callback(
708 &repo,
709 from,
710 to,
711 |commit| Ok(commit.clone()),
712 )?;
713 commits.reverse();
714 Ok(commits)
715 }
716
717 pub fn get_commit_range_files(&self, from: &str, to: &str) -> Result<Vec<StagedFile>> {
719 let repo = self.open_repo()?;
720 commit::get_commit_range_files(&repo, from, to)
721 }
722
723 pub fn get_recent_commits(&self, count: usize) -> Result<Vec<RecentCommit>> {
733 let repo = self.open_repo()?;
734 log_debug!("Fetching {} recent commits", count);
735 let mut revwalk = repo.revwalk()?;
736 revwalk.push_head()?;
737
738 let commits = revwalk
739 .take(count)
740 .map(|oid| {
741 let oid = oid?;
742 let commit = repo.find_commit(oid)?;
743 let author = commit.author();
744 Ok(RecentCommit {
745 hash: oid.to_string(),
746 message: commit.message().unwrap_or_default().to_string(),
747 author: author.name().unwrap_or_default().to_string(),
748 timestamp: DateTime::<Utc>::from_timestamp(commit.time().seconds(), 0)
749 .map_or_else(
750 || commit.time().seconds().to_string(),
751 |timestamp| timestamp.to_rfc3339(),
752 ),
753 })
754 })
755 .collect::<Result<Vec<_>>>()?;
756
757 log_debug!("Retrieved {} recent commits", commits.len());
758 Ok(commits)
759 }
760
761 pub fn commit_and_verify(&self, message: &str) -> Result<CommitResult> {
771 if self.is_remote {
772 return Err(anyhow!(
773 "Cannot commit to a remote repository in read-only mode"
774 ));
775 }
776
777 let repo = self.open_repo()?;
778 match commit::commit(&repo, message, self.is_remote) {
779 Ok(result) => {
780 if let Err(e) = self.execute_hook("post-commit") {
781 log_debug!("Post-commit hook failed: {}", e);
782 }
783 Ok(result)
784 }
785 Err(e) => {
786 log_debug!("Commit failed: {}", e);
787 Err(e)
788 }
789 }
790 }
791
792 pub fn get_git_info_for_commit(
803 &self,
804 _config: &Config,
805 commit_id: &str,
806 ) -> Result<CommitContext> {
807 log_debug!("Getting git info for commit: {}", commit_id);
808 let repo = self.open_repo()?;
809
810 let branch = self.get_current_branch()?;
812
813 let commit_info = commit::extract_commit_info(&repo, commit_id, &branch)?;
815
816 let commit_files = commit::get_commit_files(&repo, commit_id)?;
818
819 self.create_commit_context(commit_info.branch, vec![commit_info.commit], commit_files)
821 }
822
823 pub fn get_commit_date(&self, commit_ish: &str) -> Result<String> {
825 let repo = self.open_repo()?;
826 commit::get_commit_date(&repo, commit_ish)
827 }
828
829 pub fn get_commits_between_with_callback<T, F>(
831 &self,
832 from: &str,
833 to: &str,
834 callback: F,
835 ) -> Result<Vec<T>>
836 where
837 F: FnMut(&RecentCommit) -> Result<T>,
838 {
839 let repo = self.open_repo()?;
840 commit::get_commits_between_with_callback(&repo, from, to, callback)
841 }
842
843 pub fn commit(&self, message: &str) -> Result<CommitResult> {
845 let repo = self.open_repo()?;
846 commit::commit(&repo, message, self.is_remote)
847 }
848
849 pub fn amend_commit(&self, message: &str) -> Result<CommitResult> {
851 let repo = self.open_repo()?;
852 commit::amend_commit(&repo, message, self.is_remote)
853 }
854
855 pub fn get_head_commit_message(&self) -> Result<String> {
857 let repo = self.open_repo()?;
858 commit::get_head_commit_message(&repo)
859 }
860
861 pub fn is_inside_work_tree() -> Result<bool> {
863 is_inside_work_tree()
864 }
865
866 pub fn get_commit_files(&self, commit_id: &str) -> Result<Vec<StagedFile>> {
868 let repo = self.open_repo()?;
869 commit::get_commit_files(&repo, commit_id)
870 }
871
872 pub fn get_file_paths_for_commit(&self, commit_id: &str) -> Result<Vec<String>> {
874 let repo = self.open_repo()?;
875 commit::get_file_paths_for_commit(&repo, commit_id)
876 }
877
878 pub fn stage_file(&self, path: &Path) -> Result<()> {
880 let repo = self.open_repo()?;
881 let mut index = repo.index()?;
882
883 let full_path = self.repo_path.join(path);
885 if full_path.exists() {
886 index.add_path(path)?;
887 } else {
888 index.remove_path(path)?;
890 }
891
892 index.write()?;
893 Ok(())
894 }
895
896 pub fn unstage_file(&self, path: &Path) -> Result<()> {
898 let repo = self.open_repo()?;
899
900 let head = repo.head()?;
902 let head_commit = head.peel_to_commit()?;
903 let head_tree = head_commit.tree()?;
904
905 let mut index = repo.index()?;
906
907 if let Ok(entry) = head_tree.get_path(path) {
909 let blob = repo.find_blob(entry.id())?;
911 #[allow(
912 clippy::cast_sign_loss,
913 clippy::cast_possible_truncation,
914 clippy::as_conversions
915 )]
916 let file_mode = entry.filemode() as u32;
917 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
918 let file_size = blob.content().len() as u32;
919 index.add_frombuffer(
920 &git2::IndexEntry {
921 ctime: git2::IndexTime::new(0, 0),
922 mtime: git2::IndexTime::new(0, 0),
923 dev: 0,
924 ino: 0,
925 mode: file_mode,
926 uid: 0,
927 gid: 0,
928 file_size,
929 id: entry.id(),
930 flags: 0,
931 flags_extended: 0,
932 path: path.to_string_lossy().as_bytes().to_vec(),
933 },
934 blob.content(),
935 )?;
936 } else {
937 index.remove_path(path)?;
939 }
940
941 index.write()?;
942 Ok(())
943 }
944
945 pub fn stage_all(&self) -> Result<()> {
947 let repo = self.open_repo()?;
948 let mut index = repo.index()?;
949 index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
950 index.write()?;
951 Ok(())
952 }
953
954 pub fn unstage_all(&self) -> Result<()> {
956 let repo = self.open_repo()?;
957 let head = repo.head()?;
958 let head_commit = head.peel_to_commit()?;
959 repo.reset(head_commit.as_object(), git2::ResetType::Mixed, None)?;
960 Ok(())
961 }
962
963 pub fn get_untracked_files(&self) -> Result<Vec<String>> {
965 let repo = self.open_repo()?;
966 get_untracked_files(&repo)
967 }
968
969 pub fn get_all_tracked_files(&self) -> Result<Vec<String>> {
971 let repo = self.open_repo()?;
972 get_all_tracked_files(&repo)
973 }
974
975 #[must_use]
979 pub fn get_ahead_behind(&self) -> (usize, usize) {
980 let Ok(repo) = self.open_repo() else {
981 return (0, 0);
982 };
983 get_ahead_behind(&repo)
984 }
985}
986
987fn collect_branch_names(
988 repo: &Repository,
989 branch_type: git2::BranchType,
990) -> Result<HashSet<String>> {
991 let mut names = HashSet::new();
992 for branch in repo.branches(Some(branch_type))?.flatten() {
993 if let Ok(Some(name)) = branch.0.name() {
994 names.insert(name.to_string());
995 }
996 }
997 Ok(names)
998}
999
1000fn resolve_remote_head_base(
1001 repo: &Repository,
1002 remote_name: &str,
1003 local_branches: &HashSet<String>,
1004) -> Option<String> {
1005 let reference_name = format!("refs/remotes/{remote_name}/HEAD");
1006 let Ok(reference) = repo.find_reference(&reference_name) else {
1007 return None;
1008 };
1009 let symbolic_target = reference.symbolic_target()?;
1010 let remote_ref = symbolic_target.strip_prefix("refs/remotes/")?;
1011
1012 if let Some((_, local_candidate)) = remote_ref.split_once('/')
1013 && local_branches.contains(local_candidate)
1014 {
1015 return Some(local_candidate.to_string());
1016 }
1017
1018 Some(remote_ref.to_string())
1019}
1020
1021impl Drop for GitRepo {
1022 fn drop(&mut self) {
1023 if self.is_remote {
1025 log_debug!("Cleaning up temporary repository at {:?}", self.repo_path);
1026 }
1027 }
1028}