1use std::{fmt, fs, io::ErrorKind, path, process::Command};
2
3use anyhow::*;
4use git2::StatusOptions;
5use git2::build::CheckoutBuilder;
6use std::result::Result::Ok;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum MergeResult {
10 UpToDate,
11 FastForward,
12 Merged,
13 Rebased,
14 Conflicts,
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub enum RemoteComparison {
19 UpToDate,
20 Ahead(usize),
21 Behind(usize),
22 Diverged(usize, usize),
23 NoRemote,
24}
25
26pub struct Repo {
27 pub git_repo: git2::Repository,
28 pub work_dir: path::PathBuf,
29 pub head: String,
30 pub subrepos: Vec<Repo>,
31}
32
33pub fn init_configured_submodules(
34 work_dir: &path::Path,
35 configured_paths: &[path::PathBuf],
36) -> Result<()> {
37 let git_repo = git2::Repository::open(work_dir)
38 .with_context(|| format!("Cannot open repo at `{}`", work_dir.display()))?;
39
40 let mut sorted_paths = configured_paths.to_vec();
43 sorted_paths.sort_by_key(|path| path.components().count());
44
45 for configured_path in &sorted_paths {
46 init_configured_submodule_path(&git_repo, work_dir, configured_path)
47 .with_context(|| {
48 format!(
49 "Cannot initialize configured submodule `{}`",
50 configured_path.display()
51 )
52 })?;
53 }
54
55 Ok(())
56}
57
58fn init_configured_submodule_path(
59 git_repo: &git2::Repository,
60 work_dir: &path::Path,
61 configured_path: &path::Path,
62) -> Result<()> {
63 if configured_path.as_os_str().is_empty() {
64 return Ok(());
65 }
66
67 let mut components = configured_path.components();
68 let first_component = match components.next() {
69 Some(component) => component.as_os_str(),
70 None => return Ok(()),
71 };
72
73 let first_component_str = first_component.to_str().with_context(|| {
74 format!(
75 "Configured submodule path `{}` is not valid UTF-8",
76 configured_path.display()
77 )
78 })?;
79
80 let mut submodule = match git_repo.find_submodule(first_component_str) {
81 Ok(submodule) => submodule,
82 Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(()),
83 Err(err) => return Err(err.into()),
84 };
85
86 submodule.init(false).with_context(|| {
87 format!(
88 "Cannot initialize submodule `{}` in `{}`",
89 first_component_str,
90 work_dir.display()
91 )
92 })?;
93
94 let submodule_work_dir = work_dir.join(first_component);
95 let is_initialized = submodule.open().is_ok();
96 if !is_initialized {
97 let module_git_dir = git_repo.path().join("modules").join(first_component);
98 if module_git_dir.exists() && !submodule_work_dir.exists() {
99 fs::create_dir_all(&submodule_work_dir).with_context(|| {
100 format!(
101 "Cannot create submodule worktree directory `{}`",
102 submodule_work_dir.display()
103 )
104 })?;
105 }
106
107 if let Err(initial_err) = submodule.update(false, None) {
108 submodule.update(true, None).with_context(|| {
109 format!(
110 "Cannot update submodule `{}` in `{}` (initial attempt with init=false failed: {})",
111 first_component_str,
112 work_dir.display(),
113 initial_err,
114 )
115 })?;
116 }
117 }
118
119 let remaining_path = components.as_path();
120 if remaining_path.as_os_str().is_empty() {
121 return Ok(());
122 }
123
124 let child_work_dir = submodule_work_dir;
125 let child_repo = git2::Repository::open(&child_work_dir).with_context(|| {
126 format!(
127 "Cannot open initialized submodule repo at `{}`",
128 child_work_dir.display()
129 )
130 })?;
131
132 init_configured_submodule_path(&child_repo, &child_work_dir, remaining_path)
133}
134
135impl Repo {
136 pub fn new(work_dir: &path::Path, head_name: Option<&str>) -> Result<Self> {
137 let git_repo = git2::Repository::open(work_dir)
138 .with_context(|| format!("Cannot open repo at `{}`", work_dir.display()))?;
139
140 let head = match head_name {
141 Some(name) => String::from(name),
142 None => {
143 let is_detached = git_repo.head_detached().with_context(|| {
144 format!(
145 "Cannot determine head state for repo at `{}`",
146 work_dir.display()
147 )
148 })?;
149 if is_detached {
150 String::from("<detached>")
151 } else {
152 String::from(git_repo.head().with_context(|| {
153 format!(
154 "Cannot find the head branch for repo at `{}`. Is it detached?",
155 work_dir.display()
156 )
157 })?.shorthand().with_context(|| {
158 format!(
159 "Cannot find a human readable representation of the head ref for repo at `{}`",
160 work_dir.display(),
161 )
162 })?)
163 }
164 },
165 };
166
167 let subrepos = git_repo
168 .submodules()
169 .with_context(|| {
170 format!(
171 "Cannot load submodules for repo at `{}`",
172 work_dir.display()
173 )
174 })?
175 .iter()
176 .map(|submodule| Repo::new(&work_dir.join(submodule.path()), None))
177 .collect::<Result<Vec<Repo>>>()?;
178
179 Ok(Repo {
180 git_repo,
181 work_dir: path::PathBuf::from(work_dir),
182 head,
183 subrepos,
184 })
185 }
186
187 pub fn get_subrepo_by_path(&self, subrepo_path: &path::PathBuf) -> Option<&Repo> {
188 self.subrepos
189 .iter()
190 .find(|subrepo| subrepo.work_dir == self.work_dir.join(subrepo_path))
191 }
192
193 pub fn sync(&self) -> Result<()> {
194 self.switch(&self.head)?;
195 Ok(())
196 }
197
198 pub fn uses_lfs(&self) -> Result<bool> {
199 let attributes_path = self.work_dir.join(".gitattributes");
200 if attributes_path.exists() {
201 let attributes =
202 fs::read_to_string(&attributes_path).with_context(|| {
203 format!("Cannot read `{}`", attributes_path.display())
204 })?;
205 if attributes.lines().any(|line| {
206 let trimmed = line.trim();
207 !trimmed.is_empty()
208 && !trimmed.starts_with('#')
209 && trimmed.contains("filter=lfs")
210 }) {
211 return Ok(true);
212 }
213 }
214
215 if self.work_dir.join(".lfsconfig").exists() {
216 return Ok(true);
217 }
218
219 if self.git_repo.path().join("lfs").exists() {
222 return Ok(true);
223 }
224
225 Ok(false)
226 }
227
228 pub fn lfs_pull_if_needed(&self) -> Result<()> {
229 if self.uses_lfs()? {
230 self.run_git_lfs(&["pull"])?;
231 }
232 Ok(())
233 }
234
235 pub fn lfs_push_if_needed(
236 &self,
237 remote_name: &str,
238 branch_name: &str,
239 ) -> Result<()> {
240 if self.uses_lfs()? {
241 self.run_git_lfs(&["push", remote_name, branch_name])?;
242 }
243 Ok(())
244 }
245
246 fn run_git_lfs(&self, args: &[&str]) -> Result<()> {
247 let output = Command::new("git-lfs")
248 .args(args)
249 .current_dir(&self.work_dir)
250 .output()
251 .map_err(|err| {
252 if err.kind() == ErrorKind::NotFound {
253 anyhow!(
254 "Git LFS support required for `{}` but `git-lfs` is not installed.\n\
255 Install it on Fedora with:\n\
256 sudo dnf install git-lfs\n\
257 git lfs install",
258 self.work_dir.display()
259 )
260 } else {
261 anyhow!(err).context(format!(
262 "Cannot execute git-lfs in `{}`",
263 self.work_dir.display()
264 ))
265 }
266 })?;
267
268 if !output.status.success() {
269 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
270 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
271 let details = if !stderr.is_empty() {
272 stderr
273 } else if !stdout.is_empty() {
274 stdout
275 } else {
276 "no output".to_string()
277 };
278 bail!(
279 "git-lfs {} failed in `{}`: {}",
280 args.join(" "),
281 self.work_dir.display(),
282 details
283 );
284 }
285
286 Ok(())
287 }
288
289 pub fn switch(&self, head: &str) -> Result<()> {
290 self.git_repo.set_head(&self.resolve_reference(head)?)?;
291 let checkout_result = self.git_repo.checkout_head(None);
292 checkout_result?;
293 Ok(())
294 }
295
296 pub fn switch_force(&self, head: &str) -> Result<()> {
297 self.git_repo.set_head(&self.resolve_reference(head)?)?;
298 let mut checkout = CheckoutBuilder::new();
299 checkout.force();
300 let checkout_result = self.git_repo.checkout_head(Some(&mut checkout));
301 checkout_result?;
302 Ok(())
303 }
304
305 pub fn refresh_worktree(&self) -> Result<()> {
306 let checkout_result = self.git_repo.checkout_head(None);
307 checkout_result?;
308 Ok(())
309 }
310
311 pub fn refresh_worktree_force(&self) -> Result<()> {
312 let mut checkout = CheckoutBuilder::new();
313 checkout.force();
314 let checkout_result = self.git_repo.checkout_head(Some(&mut checkout));
315 checkout_result?;
316 Ok(())
317 }
318
319 pub fn checkout_path_from_head(&self, path: &path::Path) -> Result<()> {
320 let mut checkout = CheckoutBuilder::new();
321 checkout.force().path(path);
322 self.git_repo.checkout_head(Some(&mut checkout))?;
323 Ok(())
324 }
325
326 fn switch_forced(&self, head: &str) -> Result<()> {
327 self.git_repo.set_head(&self.resolve_reference(head)?)?;
328 let mut checkout = CheckoutBuilder::new();
329 checkout.force();
330 self.git_repo.checkout_head(Some(&mut checkout))?;
331 Ok(())
332 }
333
334 pub fn fetch(&self) -> Result<()> {
335 if self.git_repo.head_detached().with_context(|| {
336 format!(
337 "Cannot determine head state for repo at `{}`",
338 self.work_dir.display()
339 )
340 })? {
341 return Ok(());
342 }
343
344 let head_ref = self.git_repo.head()?;
346 let branch_name = head_ref.shorthand().with_context(|| {
347 format!(
348 "Cannot get branch name for repo at `{}`",
349 self.work_dir.display()
350 )
351 })?;
352
353 let tracking = match self.tracking_branch(branch_name)? {
354 Some(tracking) => tracking,
355 None => {
356 return Ok(());
358 },
359 };
360
361 match self.git_repo.find_remote(&tracking.remote) {
363 Ok(mut remote) => {
364 let mut fetch_options = git2::FetchOptions::new();
365 fetch_options.remote_callbacks(self.remote_callbacks()?);
366
367 remote
368 .fetch::<&str>(&[], Some(&mut fetch_options), None)
369 .with_context(|| {
370 format!(
371 "Failed to fetch from remote '{}' for repo at `{}`\n\
372 \n\
373 Possible causes:\n\
374 - SSH agent not running or not accessible (check SSH_AUTH_SOCK)\n\
375 - SSH keys not properly configured in ~/.ssh/\n\
376 - Credential helper not configured (git config credential.helper)\n\
377 - Network/firewall issues\n\
378 \n\
379 Try running: git fetch --verbose\n\
380 Or check authentication with: git-wok test-auth",
381 tracking.remote,
382 self.work_dir.display()
383 )
384 })?;
385 },
386 Err(_) => {
387 return Ok(());
389 },
390 }
391
392 Ok(())
393 }
394
395 pub fn ensure_on_branch(&self, branch_name: &str) -> Result<()> {
396 if !self.is_worktree_clean()? {
397 bail!(
398 "Refusing to switch branches with uncommitted changes in `{}`",
399 self.work_dir.display()
400 );
401 }
402
403 if !self.git_repo.head_detached().with_context(|| {
404 format!(
405 "Cannot determine head state for repo at `{}`",
406 self.work_dir.display()
407 )
408 })? && let Ok(head) = self.git_repo.head()
409 && head.shorthand() == Some(branch_name)
410 {
411 return Ok(());
412 }
413
414 let local_ref = format!("refs/heads/{}", branch_name);
415 if self.git_repo.find_reference(&local_ref).is_ok() {
416 self.switch_forced(branch_name)?;
417 return Ok(());
418 }
419
420 let remote_name = self.get_remote_name_for_branch(branch_name)?;
421 if let Ok(mut remote) = self.git_repo.find_remote(&remote_name) {
422 let mut fetch_options = git2::FetchOptions::new();
423 fetch_options.remote_callbacks(self.remote_callbacks()?);
424 remote.fetch::<&str>(&[], Some(&mut fetch_options), None)?;
425 }
426
427 let remote_ref = format!("refs/remotes/{}/{}", remote_name, branch_name);
428 if let Ok(remote_oid) = self.git_repo.refname_to_id(&remote_ref) {
429 let remote_commit = self.git_repo.find_commit(remote_oid)?;
430 self.git_repo.branch(branch_name, &remote_commit, false)?;
431 let mut local_branch = self
432 .git_repo
433 .find_branch(branch_name, git2::BranchType::Local)?;
434 local_branch
435 .set_upstream(Some(&format!("{}/{}", remote_name, branch_name)))?;
436 self.switch(branch_name)?;
437 return Ok(());
438 }
439
440 let head = self.git_repo.head()?;
441 let current_commit = head.peel_to_commit()?;
442 self.git_repo.branch(branch_name, ¤t_commit, false)?;
443 self.switch(branch_name)?;
444 Ok(())
445 }
446
447 pub fn ensure_on_branch_existing_or_remote(
448 &self,
449 branch_name: &str,
450 create: bool,
451 ) -> Result<()> {
452 if !self.is_worktree_clean()? {
453 bail!(
454 "Refusing to switch branches with uncommitted changes in `{}`",
455 self.work_dir.display()
456 );
457 }
458
459 if !self.git_repo.head_detached().with_context(|| {
460 format!(
461 "Cannot determine head state for repo at `{}`",
462 self.work_dir.display()
463 )
464 })? && let Ok(head) = self.git_repo.head()
465 && head.shorthand() == Some(branch_name)
466 {
467 return Ok(());
468 }
469
470 let local_ref = format!("refs/heads/{}", branch_name);
471 if self.git_repo.find_reference(&local_ref).is_ok() {
472 self.switch(branch_name)?;
473 return Ok(());
474 }
475
476 let remote_name = self.get_remote_name_for_branch(branch_name)?;
477 if let Ok(mut remote) = self.git_repo.find_remote(&remote_name) {
478 let mut fetch_options = git2::FetchOptions::new();
479 fetch_options.remote_callbacks(self.remote_callbacks()?);
480 remote.fetch::<&str>(&[], Some(&mut fetch_options), None)?;
481 }
482
483 let remote_ref = format!("refs/remotes/{}/{}", remote_name, branch_name);
484 if let Ok(remote_oid) = self.git_repo.refname_to_id(&remote_ref) {
485 let remote_commit = self.git_repo.find_commit(remote_oid)?;
486 self.git_repo.branch(branch_name, &remote_commit, false)?;
487 let mut local_branch = self
488 .git_repo
489 .find_branch(branch_name, git2::BranchType::Local)?;
490 local_branch
491 .set_upstream(Some(&format!("{}/{}", remote_name, branch_name)))?;
492 self.switch_forced(branch_name)?;
493 return Ok(());
494 }
495
496 if create {
497 let head = self.git_repo.head()?;
498 let current_commit = head.peel_to_commit()?;
499 self.git_repo.branch(branch_name, ¤t_commit, false)?;
500 self.switch_forced(branch_name)?;
501 return Ok(());
502 }
503
504 bail!(
505 "Branch '{}' does not exist and --create not specified",
506 branch_name
507 );
508 }
509
510 fn rebase(
511 &self,
512 _branch_name: &str,
513 remote_commit: &git2::Commit,
514 ) -> Result<MergeResult> {
515 let _local_commit = self.git_repo.head()?.peel_to_commit()?;
516 let remote_oid = remote_commit.id();
517
518 let remote_annotated = self.git_repo.find_annotated_commit(remote_oid)?;
520
521 let signature = self.git_repo.signature()?;
523 let mut rebase = self.git_repo.rebase(
524 None, Some(&remote_annotated), None, None, )?;
529
530 let mut has_conflicts = false;
532 while let Some(op) = rebase.next() {
533 match op {
534 Ok(_rebase_op) => {
535 let index = self.git_repo.index()?;
537 if index.has_conflicts() {
538 has_conflicts = true;
539 break;
540 }
541
542 if rebase.commit(None, &signature, None).is_err() {
544 has_conflicts = true;
545 break;
546 }
547 },
548 Err(_) => {
549 has_conflicts = true;
550 break;
551 },
552 }
553 }
554
555 if has_conflicts {
556 return Ok(MergeResult::Conflicts);
558 }
559
560 rebase.finish(Some(&signature))?;
562
563 Ok(MergeResult::Rebased)
564 }
565
566 pub fn merge(&self, branch_name: &str) -> Result<MergeResult> {
567 self.fetch()?;
569
570 let tracking = match self.tracking_branch(branch_name)? {
572 Some(tracking) => tracking,
573 None => {
574 return Ok(MergeResult::UpToDate);
576 },
577 };
578
579 let remote_branch_oid = match self.git_repo.refname_to_id(&tracking.remote_ref)
581 {
582 Ok(oid) => oid,
583 Err(_) => {
584 return Ok(MergeResult::UpToDate);
586 },
587 };
588
589 let remote_commit = self.git_repo.find_commit(remote_branch_oid)?;
590 let local_commit = self.git_repo.head()?.peel_to_commit()?;
591
592 if local_commit.id() == remote_commit.id() {
594 return Ok(MergeResult::UpToDate);
595 }
596
597 if self
599 .git_repo
600 .graph_descendant_of(remote_commit.id(), local_commit.id())?
601 {
602 self.git_repo.reference(
604 &format!("refs/heads/{}", branch_name),
605 remote_commit.id(),
606 true,
607 &format!("Fast-forward '{}' to {}", branch_name, tracking.remote_ref),
608 )?;
609 self.git_repo
610 .set_head(&format!("refs/heads/{}", branch_name))?;
611 let mut checkout = CheckoutBuilder::new();
612 checkout.force();
613 self.git_repo.checkout_head(Some(&mut checkout))?;
614 self.lfs_pull_if_needed()?;
615 return Ok(MergeResult::FastForward);
616 }
617
618 let pull_strategy = self.get_pull_strategy(branch_name)?;
620
621 match pull_strategy {
622 PullStrategy::Rebase => {
623 let result = self.rebase(branch_name, &remote_commit)?;
625 if matches!(result, MergeResult::Rebased) {
626 self.lfs_pull_if_needed()?;
627 }
628 Ok(result)
629 },
630 PullStrategy::Merge => {
631 let result = self.do_merge(
633 branch_name,
634 &local_commit,
635 &remote_commit,
636 &tracking,
637 )?;
638 if matches!(result, MergeResult::Merged) {
639 self.lfs_pull_if_needed()?;
640 }
641 Ok(result)
642 },
643 }
644 }
645
646 fn do_merge(
647 &self,
648 branch_name: &str,
649 local_commit: &git2::Commit,
650 remote_commit: &git2::Commit,
651 tracking: &TrackingBranch,
652 ) -> Result<MergeResult> {
653 let mut merge_opts = git2::MergeOptions::new();
655 merge_opts.fail_on_conflict(false); let _merge_result = self.git_repo.merge_commits(
658 local_commit,
659 remote_commit,
660 Some(&merge_opts),
661 )?;
662
663 let mut index = self.git_repo.index()?;
665 let has_conflicts = index.has_conflicts();
666
667 if !has_conflicts {
668 let signature = self.git_repo.signature()?;
670 let tree_id = index.write_tree()?;
671 let tree = self.git_repo.find_tree(tree_id)?;
672
673 self.git_repo.commit(
674 Some(&format!("refs/heads/{}", branch_name)),
675 &signature,
676 &signature,
677 &format!("Merge remote-tracking branch '{}'", tracking.remote_ref),
678 &tree,
679 &[local_commit, remote_commit],
680 )?;
681
682 self.git_repo.cleanup_state()?;
683
684 Ok(MergeResult::Merged)
685 } else {
686 Ok(MergeResult::Conflicts)
688 }
689 }
690
691 pub fn get_remote_name_for_branch(&self, branch_name: &str) -> Result<String> {
692 if let Some(tracking) = self.tracking_branch(branch_name)? {
693 Ok(tracking.remote)
694 } else {
695 Ok("origin".to_string())
697 }
698 }
699
700 pub fn get_remote_comparison(
702 &self,
703 branch_name: &str,
704 ) -> Result<Option<RemoteComparison>> {
705 let tracking = match self.tracking_branch(branch_name)? {
707 Some(tracking) => tracking,
708 None => return Ok(None), };
710
711 let remote_oid = match self.git_repo.refname_to_id(&tracking.remote_ref) {
713 Ok(oid) => oid,
714 Err(_) => {
715 return Ok(Some(RemoteComparison::NoRemote));
717 },
718 };
719
720 let local_oid = self.git_repo.head()?.peel_to_commit()?.id();
722
723 if local_oid == remote_oid {
725 return Ok(Some(RemoteComparison::UpToDate));
726 }
727
728 let (ahead, behind) =
730 self.git_repo.graph_ahead_behind(local_oid, remote_oid)?;
731
732 if ahead > 0 && behind > 0 {
733 Ok(Some(RemoteComparison::Diverged(ahead, behind)))
734 } else if ahead > 0 {
735 Ok(Some(RemoteComparison::Ahead(ahead)))
736 } else if behind > 0 {
737 Ok(Some(RemoteComparison::Behind(behind)))
738 } else {
739 Ok(Some(RemoteComparison::UpToDate))
740 }
741 }
742
743 pub fn remote_callbacks(&self) -> Result<git2::RemoteCallbacks<'static>> {
744 self.remote_callbacks_impl(false)
745 }
746
747 pub fn remote_callbacks_verbose(&self) -> Result<git2::RemoteCallbacks<'static>> {
748 self.remote_callbacks_impl(true)
749 }
750
751 fn remote_callbacks_impl(
752 &self,
753 verbose: bool,
754 ) -> Result<git2::RemoteCallbacks<'static>> {
755 let config = self.git_repo.config()?;
756
757 let mut callbacks = git2::RemoteCallbacks::new();
758 callbacks.credentials(move |url, username_from_url, allowed| {
759 if verbose {
760 eprintln!("DEBUG: Credential callback invoked");
761 eprintln!(" URL: {}", url);
762 eprintln!(" Username from URL: {:?}", username_from_url);
763 eprintln!(" Allowed types: {:?}", allowed);
764 }
765
766 if allowed.contains(git2::CredentialType::SSH_KEY) {
768 if let Some(username) = username_from_url {
769 if std::env::var("SSH_AUTH_SOCK").is_ok() {
771 if verbose {
772 eprintln!(
773 " Attempting: SSH key from agent for user '{}'",
774 username
775 );
776 }
777 match git2::Cred::ssh_key_from_agent(username) {
778 Ok(cred) => {
779 if verbose {
780 eprintln!(" SUCCESS: SSH key from agent");
781 }
782 return Ok(cred);
783 },
784 Err(e) => {
785 if verbose {
786 eprintln!(" FAILED: SSH key from agent - {}", e);
787 }
788 },
789 }
790 } else if verbose {
791 eprintln!(
792 " SKIPPED: SSH key from agent (SSH_AUTH_SOCK not set)"
793 );
794 }
795 } else if verbose {
796 eprintln!(" SKIPPED: SSH key from agent (no username provided)");
797 }
798
799 if let Some(username) = username_from_url
801 && let Ok(home) = std::env::var("HOME")
802 {
803 let key_paths = vec![
804 format!("{}/.ssh/id_ed25519", home),
805 format!("{}/.ssh/id_rsa", home),
806 format!("{}/.ssh/id_ecdsa", home),
807 ];
808
809 for key_path in key_paths {
810 if path::Path::new(&key_path).exists() {
811 if verbose {
812 eprintln!(" Attempting: SSH key file at {}", key_path);
813 }
814 match git2::Cred::ssh_key(
815 username,
816 None, path::Path::new(&key_path),
818 None, ) {
820 Ok(cred) => {
821 if verbose {
822 eprintln!(" SUCCESS: SSH key file");
823 }
824 return Ok(cred);
825 },
826 Err(e) => {
827 if verbose {
828 eprintln!(" FAILED: SSH key file - {}", e);
829 }
830 },
831 }
832 }
833 }
834 }
835 }
836
837 if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT)
839 || allowed.contains(git2::CredentialType::SSH_KEY)
840 || allowed.contains(git2::CredentialType::DEFAULT)
841 {
842 if verbose {
843 eprintln!(" Attempting: Credential helper");
844 }
845 match git2::Cred::credential_helper(&config, url, username_from_url) {
846 Ok(cred) => {
847 if verbose {
848 eprintln!(" SUCCESS: Credential helper");
849 }
850 return Ok(cred);
851 },
852 Err(e) => {
853 if verbose {
854 eprintln!(" FAILED: Credential helper - {}", e);
855 }
856 },
857 }
858 }
859
860 if allowed.contains(git2::CredentialType::USERNAME) {
862 let username = username_from_url.unwrap_or("git");
863 if verbose {
864 eprintln!(" Attempting: Username only ('{}')", username);
865 }
866 match git2::Cred::username(username) {
867 Ok(cred) => {
868 if verbose {
869 eprintln!(" SUCCESS: Username");
870 }
871 return Ok(cred);
872 },
873 Err(e) => {
874 if verbose {
875 eprintln!(" FAILED: Username - {}", e);
876 }
877 },
878 }
879 }
880
881 if verbose {
883 eprintln!(" Attempting: Default credentials");
884 }
885 match git2::Cred::default() {
886 Ok(cred) => {
887 if verbose {
888 eprintln!(" SUCCESS: Default credentials");
889 }
890 Ok(cred)
891 },
892 Err(e) => {
893 if verbose {
894 eprintln!(" FAILED: All credential methods exhausted");
895 eprintln!(" Last error: {}", e);
896 }
897 Err(e)
898 },
899 }
900 });
901
902 Ok(callbacks)
903 }
904
905 fn resolve_reference(&self, short_name: &str) -> Result<String> {
906 Ok(self
907 .git_repo
908 .resolve_reference_from_short_name(short_name)?
909 .name()
910 .with_context(|| {
911 format!(
912 "Cannot resolve head reference for repo at `{}`",
913 self.work_dir.display()
914 )
915 })?
916 .to_owned())
917 }
918
919 pub fn tracking_branch(&self, branch_name: &str) -> Result<Option<TrackingBranch>> {
920 let config = self.git_repo.config()?;
921
922 let remote_key = format!("branch.{}.remote", branch_name);
923 let merge_key = format!("branch.{}.merge", branch_name);
924
925 let remote = match config.get_string(&remote_key) {
926 Ok(name) => name,
927 Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(None),
928 Err(err) => return Err(err.into()),
929 };
930
931 let merge_ref = match config.get_string(&merge_key) {
932 Ok(name) => name,
933 Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(None),
934 Err(err) => return Err(err.into()),
935 };
936
937 let branch_short = merge_ref
938 .strip_prefix("refs/heads/")
939 .unwrap_or(&merge_ref)
940 .to_owned();
941
942 let remote_ref = format!("refs/remotes/{}/{}", remote, branch_short);
943
944 Ok(Some(TrackingBranch { remote, remote_ref }))
945 }
946
947 fn get_pull_strategy(&self, branch_name: &str) -> Result<PullStrategy> {
948 let config = self.git_repo.config()?;
949
950 let branch_rebase_key = format!("branch.{}.rebase", branch_name);
952 if let Ok(value) = config.get_string(&branch_rebase_key) {
953 return Ok(parse_rebase_config(&value));
954 }
955
956 if let Ok(value) = config.get_string("pull.rebase") {
958 return Ok(parse_rebase_config(&value));
959 }
960
961 if let Ok(value) = config.get_bool("pull.rebase") {
963 return Ok(if value {
964 PullStrategy::Rebase
965 } else {
966 PullStrategy::Merge
967 });
968 }
969
970 Ok(PullStrategy::Merge)
972 }
973
974 fn is_worktree_clean(&self) -> Result<bool> {
975 let mut status_options = StatusOptions::new();
976 status_options.include_ignored(false);
977 status_options.include_untracked(true);
978 let statuses = self.git_repo.statuses(Some(&mut status_options))?;
979 Ok(statuses.is_empty())
980 }
981}
982
983impl fmt::Debug for Repo {
984 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
985 f.debug_struct("Repo")
986 .field("work_dir", &self.work_dir)
987 .field("head", &self.head)
988 .field("subrepos", &self.subrepos)
989 .finish()
990 }
991}
992
993pub struct TrackingBranch {
994 pub remote: String,
995 pub remote_ref: String,
996}
997
998#[derive(Debug, Clone, PartialEq)]
999enum PullStrategy {
1000 Merge,
1001 Rebase,
1002}
1003
1004fn parse_rebase_config(value: &str) -> PullStrategy {
1005 match value.to_lowercase().as_str() {
1006 "true" | "interactive" | "i" | "merges" | "m" => PullStrategy::Rebase,
1007 "false" => PullStrategy::Merge,
1008 _ => PullStrategy::Merge, }
1010}