git_repos/
repo.rs

1use std::{path::Path, str::FromStr};
2
3use git2::Repository;
4use serde::{Deserialize, Serialize};
5
6use super::{output::*, path, worktree};
7use crate::{
8    common::error::{Error, Result},
9    err,
10};
11
12const WORKTREE_CONFIG_FILE_NAME: &str = "grm.toml";
13const GIT_CONFIG_BARE_KEY: &str = "core.bare";
14const GIT_CONFIG_PUSH_DEFAULT: &str = "push.default";
15
16#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
17pub enum RemoteType {
18    SSH,
19    HTTPS,
20    File,
21}
22
23impl FromStr for RemoteType {
24    type Err = Error;
25
26    fn from_str(url: &str) -> std::result::Result<Self, Self::Err> {
27        if url.starts_with("git@") && url.ends_with(".git") {
28            return Ok(RemoteType::SSH);
29        }
30
31        if url.starts_with("ssh://") {
32            return Ok(RemoteType::SSH);
33        }
34
35        if url.starts_with("https://") {
36            return Ok(RemoteType::HTTPS);
37        }
38
39        if url.starts_with("file://") {
40            return Ok(RemoteType::File);
41        }
42
43        if url.starts_with("http://") {
44            return err!("Remotes HTTP protocol not supported");
45        }
46
47        if url.starts_with("git://") {
48            return err!("Remotes git protocol not supported");
49        }
50
51        err!("Remotes protocol not supported: {url}")
52    }
53}
54
55pub enum WorktreeRemoveFailureReason {
56    Changes(String),
57    Error(String),
58    NotMerged(String),
59}
60
61pub enum WorktreeConversionFailureReason {
62    Changes,
63    Ignored,
64    Error(String),
65}
66
67pub enum GitPushDefaultSetting {
68    Upstream,
69}
70
71#[derive(Debug, PartialEq, Eq)]
72pub enum RepoErrorKind {
73    NotFound,
74    Unknown(String),
75}
76
77#[derive(Debug)]
78pub struct RepoError {
79    pub kind: RepoErrorKind,
80}
81
82impl RepoError {
83    fn new(kind: RepoErrorKind) -> RepoError {
84        RepoError { kind }
85    }
86}
87
88#[derive(Debug, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct TrackingConfig {
91    pub default: bool,
92    pub default_remote: String,
93    pub default_remote_prefix: Option<String>,
94}
95
96#[derive(Debug, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct WorktreeRootConfig {
99    pub persistent_branches: Option<Vec<String>>,
100
101    pub track: Option<TrackingConfig>,
102}
103
104pub fn read_worktree_root_config(
105    worktree_root: &Path,
106) -> Result<Option<WorktreeRootConfig>, String> {
107    let path = worktree_root.join(WORKTREE_CONFIG_FILE_NAME);
108    let content = match std::fs::read_to_string(&path) {
109        Ok(s) => s,
110        Err(e) => {
111            match e.kind() {
112                std::io::ErrorKind::NotFound => return Ok(None),
113                _ => {
114                    return Err(format!(
115                        "Error reading configuration file \"{}\": {}",
116                        path.display(),
117                        e
118                    ))
119                },
120            }
121        },
122    };
123
124    let config: WorktreeRootConfig = match toml::from_str(&content) {
125        Ok(c) => c,
126        Err(e) => {
127            return Err(format!("Error parsing configuration file \"{}\": {}", path.display(), e))
128        },
129    };
130
131    Ok(Some(config))
132}
133
134impl std::error::Error for RepoError {}
135
136impl std::fmt::Display for RepoError {
137    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
138        write!(f, "{:?}", self.kind)
139    }
140}
141
142#[derive(Debug)]
143pub struct Remote {
144    pub name: String,
145    pub url: String,
146    pub remote_type: RemoteType,
147}
148
149#[derive(Debug)]
150pub struct Repo {
151    pub name: String,
152    pub namespace: Option<String>,
153    pub worktree_setup: bool,
154    pub remotes: Option<Vec<Remote>>,
155}
156
157impl Repo {
158    pub fn fullname(&self) -> String {
159        match &self.namespace {
160            Some(namespace) => format!("{}/{}", namespace, self.name),
161            None => self.name.clone(),
162        }
163    }
164
165    pub fn remove_namespace(&mut self) {
166        self.namespace = None
167    }
168}
169
170pub struct RepoChanges {
171    pub files_new: usize,
172    pub files_modified: usize,
173    pub files_deleted: usize,
174}
175
176pub enum SubmoduleStatus {
177    Clean,
178    Uninitialized,
179    Changed,
180    OutOfDate,
181}
182
183pub enum RemoteTrackingStatus {
184    UpToDate,
185    Ahead(usize),
186    Behind(usize),
187    Diverged(usize, usize),
188}
189
190pub struct RepoStatus {
191    pub operation: Option<git2::RepositoryState>,
192
193    pub empty: bool,
194
195    pub remotes: Vec<String>,
196
197    pub head: Option<String>,
198
199    pub changes: Option<RepoChanges>,
200
201    pub worktrees: usize,
202
203    pub submodules: Option<Vec<(String, SubmoduleStatus)>>,
204
205    pub branches: Vec<(String, Option<(String, RemoteTrackingStatus)>)>,
206}
207
208pub struct Worktree {
209    name: String,
210}
211
212impl Worktree {
213    pub fn new(name: &str) -> Self {
214        Self { name: name.to_string() }
215    }
216
217    pub fn name(&self) -> &str {
218        &self.name
219    }
220
221    pub fn forward_branch(&self, rebase: bool, stash: bool) -> Result<Option<String>, String> {
222        let repo = RepoHandle::open(Path::new(&self.name), false)
223            .map_err(|error| format!("Error opening worktree: {}", error))?;
224
225        if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() {
226            let status = repo.status(false)?;
227            let mut stashed_changes = false;
228
229            if !status.clean() {
230                if stash {
231                    repo.stash()?;
232                    stashed_changes = true;
233                } else {
234                    return Ok(Some(String::from("Worktree contains changes")));
235                }
236            }
237
238            let unstash = || -> Result<(), String> {
239                if stashed_changes {
240                    repo.stash_pop()?;
241                }
242                Ok(())
243            };
244
245            let remote_annotated_commit = repo
246                .0
247                .find_annotated_commit(remote_branch.commit()?.id().0)
248                .map_err(convert_libgit2_error)?;
249
250            if rebase {
251                let mut rebase = repo
252                    .0
253                    .rebase(
254                        None, // use HEAD
255                        Some(&remote_annotated_commit),
256                        None, // figure out the base yourself, libgit2!
257                        Some(&mut git2::RebaseOptions::new()),
258                    )
259                    .map_err(convert_libgit2_error)?;
260
261                while let Some(operation) = rebase.next() {
262                    let operation = operation.map_err(convert_libgit2_error)?;
263
264                    // This is required to preserve the commiter of the rebased
265                    // commits, which is the expected behavior.
266                    let rebased_commit =
267                        repo.0.find_commit(operation.id()).map_err(convert_libgit2_error)?;
268                    let committer = rebased_commit.committer();
269
270                    // This is effectively adding all files to the index explicitly.
271                    // Normal files are already staged, but changed submodules are not.
272                    let mut index = repo.0.index().map_err(convert_libgit2_error)?;
273                    index
274                        .add_all(["."].iter(), git2::IndexAddOption::CHECK_PATHSPEC, None)
275                        .map_err(convert_libgit2_error)?;
276
277                    if let Err(error) = rebase.commit(None, &committer, None) {
278                        if error.code() == git2::ErrorCode::Applied {
279                            continue;
280                        }
281                        rebase.abort().map_err(convert_libgit2_error)?;
282                        unstash()?;
283                        return Err(convert_libgit2_error(error));
284                    }
285                }
286
287                rebase.finish(None).map_err(convert_libgit2_error)?;
288            } else {
289                let (analysis, _preference) = repo
290                    .0
291                    .merge_analysis(&[&remote_annotated_commit])
292                    .map_err(convert_libgit2_error)?;
293
294                if analysis.is_up_to_date() {
295                    unstash()?;
296                    return Ok(None);
297                }
298                if !analysis.is_fast_forward() {
299                    unstash()?;
300                    return Ok(Some(String::from("Worktree cannot be fast forwarded")));
301                }
302
303                repo.0
304                    .reset(
305                        remote_branch.commit()?.0.as_object(),
306                        git2::ResetType::Hard,
307                        Some(git2::build::CheckoutBuilder::new().safe()),
308                    )
309                    .map_err(convert_libgit2_error)?;
310            }
311            unstash()?;
312        } else {
313            return Ok(Some(String::from("No remote branch to rebase onto")));
314        };
315
316        Ok(None)
317    }
318
319    pub fn rebase_onto_default(
320        &self,
321        config: &Option<WorktreeRootConfig>,
322        stash: bool,
323    ) -> Result<Option<String>, String> {
324        let repo = RepoHandle::open(Path::new(&self.name), false)
325            .map_err(|error| format!("Error opening worktree: {}", error))?;
326
327        let guess_default_branch = || {
328            repo.default_branch()
329                .map_err(|_| "Could not determine default branch")?
330                .name()
331                .map_err(|error| format!("Failed getting default branch name: {}", error))
332        };
333
334        let default_branch_name = match &config {
335            None => guess_default_branch()?,
336            Some(config) => {
337                match &config.persistent_branches {
338                    None => guess_default_branch()?,
339                    Some(persistent_branches) => {
340                        if persistent_branches.is_empty() {
341                            guess_default_branch()?
342                        } else {
343                            persistent_branches[0].clone()
344                        }
345                    },
346                }
347            },
348        };
349
350        let status = repo.status(false)?;
351        let mut stashed_changes = false;
352
353        if !status.clean() {
354            if stash {
355                repo.stash()?;
356                stashed_changes = true;
357            } else {
358                return Ok(Some(String::from("Worktree contains changes")));
359            }
360        }
361
362        let unstash = || -> Result<(), String> {
363            if stashed_changes {
364                repo.stash_pop()?;
365            }
366            Ok(())
367        };
368
369        let base_branch = repo.find_local_branch(&default_branch_name)?;
370        let base_annotated_commit = repo
371            .0
372            .find_annotated_commit(base_branch.commit()?.id().0)
373            .map_err(convert_libgit2_error)?;
374
375        let mut rebase = repo
376            .0
377            .rebase(
378                None, // use HEAD
379                Some(&base_annotated_commit),
380                None, // figure out the base yourself, libgit2!
381                Some(&mut git2::RebaseOptions::new()),
382            )
383            .map_err(convert_libgit2_error)?;
384
385        while let Some(operation) = rebase.next() {
386            let operation = operation.map_err(convert_libgit2_error)?;
387
388            // This is required to preserve the commiter of the rebased
389            // commits, which is the expected behavior.
390            let rebased_commit =
391                repo.0.find_commit(operation.id()).map_err(convert_libgit2_error)?;
392            let committer = rebased_commit.committer();
393
394            // This is effectively adding all files to the index explicitly.
395            // Normal files are already staged, but changed submodules are not.
396            let mut index = repo.0.index().map_err(convert_libgit2_error)?;
397            index
398                .add_all(["."].iter(), git2::IndexAddOption::CHECK_PATHSPEC, None)
399                .map_err(convert_libgit2_error)?;
400
401            if let Err(error) = rebase.commit(None, &committer, None) {
402                if error.code() == git2::ErrorCode::Applied {
403                    continue;
404                }
405                rebase.abort().map_err(convert_libgit2_error)?;
406                unstash()?;
407                return Err(convert_libgit2_error(error));
408            }
409        }
410
411        rebase.finish(None).map_err(convert_libgit2_error)?;
412        unstash()?;
413        Ok(None)
414    }
415}
416
417impl RepoStatus {
418    fn clean(&self) -> bool {
419        match &self.changes {
420            None => true,
421            Some(changes) => {
422                changes.files_new == 0 && changes.files_deleted == 0 && changes.files_modified == 0
423            },
424        }
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use nill::{Nil, NIL};
431
432    use super::*;
433
434    #[test]
435    fn remote_type() -> Result<Nil> {
436        assert_eq!(RemoteType::from_str("ssh://git@unknown.com")?, RemoteType::SSH);
437        assert_eq!(RemoteType::from_str("git@unknown.git")?, RemoteType::SSH);
438
439        assert_eq!(RemoteType::from_str("https://unknown.com")?, RemoteType::HTTPS);
440        assert_eq!(RemoteType::from_str("https://unknown.com/unknown.git")?, RemoteType::HTTPS);
441
442        assert_eq!(RemoteType::from_str("file:///unknown")?, RemoteType::File);
443
444        Ok(NIL)
445    }
446
447    #[test]
448    fn repo_check_fullname() {
449        let with_namespace = Repo {
450            name: "name".to_string(),
451            namespace: Some("namespace".to_string()),
452            worktree_setup: false,
453            remotes: None,
454        };
455
456        let without_namespace = Repo {
457            name: "name".to_string(),
458            namespace: None,
459            worktree_setup: false,
460            remotes: None,
461        };
462
463        assert_eq!(with_namespace.fullname(), "namespace/name");
464        assert_eq!(without_namespace.fullname(), "name");
465    }
466}
467
468pub struct RepoHandle(git2::Repository);
469pub struct Branch<'a>(git2::Branch<'a>);
470
471fn convert_libgit2_error(error: git2::Error) -> String {
472    error.message().to_string()
473}
474
475impl RepoHandle {
476    pub fn open(path: &Path, is_worktree: bool) -> Result<Self, RepoError> {
477        let open_func = match is_worktree {
478            true => Repository::open_bare,
479            false => Repository::open,
480        };
481        let path = match is_worktree {
482            true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY),
483            false => path.to_path_buf(),
484        };
485        match open_func(path) {
486            Ok(r) => Ok(Self(r)),
487            Err(e) => {
488                match e.code() {
489                    git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
490                    _ => Err(RepoError::new(RepoErrorKind::Unknown(convert_libgit2_error(e)))),
491                }
492            },
493        }
494    }
495
496    pub fn stash(&self) -> Result<(), String> {
497        let head_branch = self.head_branch()?;
498        let head = head_branch.commit()?;
499        let author = head.author();
500
501        // This is honestly quite horrible. The problem is that all stash operations expect a
502        // mutable reference (as they, well, mutate the repo after all). But we are heavily using
503        // immutable references a lot with this struct. I'm really not sure how to best solve this.
504        // Right now, we just open the repo AGAIN. It is safe, as we are only accessing the stash
505        // with the second reference, so there are no cross effects. But it just smells. Also,
506        // using `unwrap()` here as we are already sure that the repo is openable(?).
507        let mut repo = RepoHandle::open(self.0.path(), false).unwrap();
508        repo.0
509            .stash_save2(&author, None, Some(git2::StashFlags::INCLUDE_UNTRACKED))
510            .map_err(convert_libgit2_error)?;
511        Ok(())
512    }
513
514    pub fn stash_pop(&self) -> Result<(), String> {
515        let mut repo = RepoHandle::open(self.0.path(), false).unwrap();
516        repo.0
517            .stash_pop(0, Some(git2::StashApplyOptions::new().reinstantiate_index()))
518            .map_err(convert_libgit2_error)?;
519        Ok(())
520    }
521
522    pub fn rename_remote(&self, remote: &RemoteHandle, new_name: &str) -> Result<(), String> {
523        let failed_refspecs =
524            self.0.remote_rename(&remote.name(), new_name).map_err(convert_libgit2_error)?;
525
526        if !failed_refspecs.is_empty() {
527            return Err(String::from("Some non-default refspecs could not be renamed"));
528        }
529
530        Ok(())
531    }
532
533    pub fn graph_ahead_behind(
534        &self,
535        local_branch: &Branch,
536        remote_branch: &Branch,
537    ) -> Result<(usize, usize), String> {
538        self.0
539            .graph_ahead_behind(local_branch.commit()?.id().0, remote_branch.commit()?.id().0)
540            .map_err(convert_libgit2_error)
541    }
542
543    pub fn head_branch(&self) -> Result<Branch, String> {
544        let head = self.0.head().map_err(convert_libgit2_error)?;
545        if !head.is_branch() {
546            return Err(String::from("No branch checked out"));
547        }
548        // unwrap() is safe here, as we can be certain that a branch with that
549        // name exists
550        let branch = self
551            .find_local_branch(head.shorthand().expect("Branch name is not valid utf-8"))
552            .unwrap();
553        Ok(branch)
554    }
555
556    pub fn remote_set_url(&self, name: &str, url: &str) -> Result<(), String> {
557        self.0.remote_set_url(name, url).map_err(convert_libgit2_error)
558    }
559
560    pub fn remote_delete(&self, name: &str) -> Result<(), String> {
561        self.0.remote_delete(name).map_err(convert_libgit2_error)
562    }
563
564    pub fn is_empty(&self) -> Result<bool, String> {
565        self.0.is_empty().map_err(convert_libgit2_error)
566    }
567
568    pub fn is_bare(&self) -> bool {
569        self.0.is_bare()
570    }
571
572    pub fn new_worktree(
573        &self,
574        name: &str,
575        directory: &Path,
576        target_branch: &Branch,
577    ) -> Result<(), String> {
578        self.0
579            .worktree(
580                name,
581                directory,
582                Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.as_reference()))),
583            )
584            .map_err(convert_libgit2_error)?;
585        Ok(())
586    }
587
588    pub fn remotes(&self) -> Result<Vec<String>, String> {
589        Ok(self
590            .0
591            .remotes()
592            .map_err(convert_libgit2_error)?
593            .iter()
594            .map(|name| name.expect("Remote name is invalid utf-8"))
595            .map(|name| name.to_owned())
596            .collect())
597    }
598
599    pub fn new_remote(&self, name: &str, url: &str) -> Result<(), String> {
600        self.0.remote(name, url).map_err(convert_libgit2_error)?;
601        Ok(())
602    }
603
604    pub fn fetchall(&self) -> Result<(), String> {
605        for remote in self.remotes()? {
606            self.fetch(&remote)?;
607        }
608        Ok(())
609    }
610
611    pub fn local_branches(&self) -> Result<Vec<Branch>, String> {
612        self.0
613            .branches(Some(git2::BranchType::Local))
614            .map_err(convert_libgit2_error)?
615            .map(|branch| Ok(Branch(branch.map_err(convert_libgit2_error)?.0)))
616            .collect::<Result<Vec<Branch>, String>>()
617    }
618
619    pub fn remote_branches(&self) -> Result<Vec<Branch>, String> {
620        self.0
621            .branches(Some(git2::BranchType::Remote))
622            .map_err(convert_libgit2_error)?
623            .map(|branch| Ok(Branch(branch.map_err(convert_libgit2_error)?.0)))
624            .collect::<Result<Vec<Branch>, String>>()
625    }
626
627    pub fn fetch(&self, remote_name: &str) -> Result<(), String> {
628        let mut remote = self.0.find_remote(remote_name).map_err(convert_libgit2_error)?;
629
630        let mut fetch_options = git2::FetchOptions::new();
631        fetch_options.remote_callbacks(get_remote_callbacks());
632
633        for refspec in &remote.fetch_refspecs().map_err(convert_libgit2_error)? {
634            remote
635                .fetch(
636                    &[refspec.ok_or("Remote name is invalid utf-8")?],
637                    Some(&mut fetch_options),
638                    None,
639                )
640                .map_err(convert_libgit2_error)?;
641        }
642        Ok(())
643    }
644
645    pub fn init(path: &Path, is_worktree: bool) -> Result<Self, String> {
646        let repo = match is_worktree {
647            false => Repository::init(path).map_err(convert_libgit2_error)?,
648            true => {
649                Repository::init_bare(path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY))
650                    .map_err(convert_libgit2_error)?
651            },
652        };
653
654        let repo = RepoHandle(repo);
655
656        if is_worktree {
657            repo.set_config_push(GitPushDefaultSetting::Upstream)?;
658        }
659
660        Ok(repo)
661    }
662
663    pub fn config(&self) -> Result<git2::Config, String> {
664        self.0.config().map_err(convert_libgit2_error)
665    }
666
667    pub fn find_worktree(&self, name: &str) -> Result<(), String> {
668        self.0.find_worktree(name).map_err(convert_libgit2_error)?;
669        Ok(())
670    }
671
672    pub fn prune_worktree(&self, name: &str) -> Result<(), String> {
673        let worktree = self.0.find_worktree(name).map_err(convert_libgit2_error)?;
674        worktree.prune(None).map_err(convert_libgit2_error)?;
675        Ok(())
676    }
677
678    pub fn find_remote_branch(
679        &self,
680        remote_name: &str,
681        branch_name: &str,
682    ) -> Result<Branch, String> {
683        Ok(Branch(
684            self.0
685                .find_branch(&format!("{}/{}", remote_name, branch_name), git2::BranchType::Remote)
686                .map_err(convert_libgit2_error)?,
687        ))
688    }
689
690    pub fn find_local_branch(&self, name: &str) -> Result<Branch, String> {
691        Ok(Branch(
692            self.0.find_branch(name, git2::BranchType::Local).map_err(convert_libgit2_error)?,
693        ))
694    }
695
696    pub fn create_branch(&self, name: &str, target: &Commit) -> Result<Branch, String> {
697        Ok(Branch(self.0.branch(name, &target.0, false).map_err(convert_libgit2_error)?))
698    }
699
700    pub fn make_bare(&self, value: bool) -> Result<(), String> {
701        let mut config = self.config()?;
702
703        config
704            .set_bool(GIT_CONFIG_BARE_KEY, value)
705            .map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_BARE_KEY, error))
706    }
707
708    pub fn convert_to_worktree(
709        &self,
710        root_dir: &Path,
711    ) -> Result<(), WorktreeConversionFailureReason> {
712        if self.status(false).map_err(WorktreeConversionFailureReason::Error)?.changes.is_some() {
713            return Err(WorktreeConversionFailureReason::Changes);
714        }
715
716        if self.has_untracked_files(false).map_err(WorktreeConversionFailureReason::Error)? {
717            return Err(WorktreeConversionFailureReason::Ignored);
718        }
719
720        std::fs::rename(".git", worktree::GIT_MAIN_WORKTREE_DIRECTORY).map_err(|error| {
721            WorktreeConversionFailureReason::Error(format!(
722                "Error moving .git directory: {}",
723                error
724            ))
725        })?;
726
727        for entry in match std::fs::read_dir(root_dir) {
728            Ok(iterator) => iterator,
729            Err(error) => {
730                return Err(WorktreeConversionFailureReason::Error(format!(
731                    "Opening directory failed: {}",
732                    error
733                )));
734            },
735        } {
736            match entry {
737                Ok(entry) => {
738                    let path = entry.path();
739                    // unwrap is safe here, the path will ALWAYS have a file component
740                    if path.file_name().unwrap() == worktree::GIT_MAIN_WORKTREE_DIRECTORY {
741                        continue;
742                    }
743                    if path.is_file() || path.is_symlink() {
744                        if let Err(error) = std::fs::remove_file(&path) {
745                            return Err(WorktreeConversionFailureReason::Error(format!(
746                                "Failed removing {}",
747                                error
748                            )));
749                        }
750                    } else if let Err(error) = std::fs::remove_dir_all(&path) {
751                        return Err(WorktreeConversionFailureReason::Error(format!(
752                            "Failed removing {}",
753                            error
754                        )));
755                    }
756                },
757                Err(error) => {
758                    return Err(WorktreeConversionFailureReason::Error(format!(
759                        "Error getting directory entry: {}",
760                        error
761                    )));
762                },
763            }
764        }
765
766        let worktree_repo = RepoHandle::open(root_dir, true).map_err(|error| {
767            WorktreeConversionFailureReason::Error(format!(
768                "Opening newly converted repository failed: {}",
769                error
770            ))
771        })?;
772
773        worktree_repo
774            .make_bare(true)
775            .map_err(|error| WorktreeConversionFailureReason::Error(format!("Error: {}", error)))?;
776
777        worktree_repo
778            .set_config_push(GitPushDefaultSetting::Upstream)
779            .map_err(|error| WorktreeConversionFailureReason::Error(format!("Error: {}", error)))?;
780
781        Ok(())
782    }
783
784    pub fn set_config_push(&self, value: GitPushDefaultSetting) -> Result<(), String> {
785        let mut config = self.config()?;
786
787        config
788            .set_str(GIT_CONFIG_PUSH_DEFAULT, match value {
789                GitPushDefaultSetting::Upstream => "upstream",
790            })
791            .map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_PUSH_DEFAULT, error))
792    }
793
794    pub fn has_untracked_files(&self, is_worktree: bool) -> Result<bool, String> {
795        match is_worktree {
796            true => Err(String::from("Cannot get changes as this is a bare worktree repository")),
797            false => {
798                let statuses = self
799                    .0
800                    .statuses(Some(git2::StatusOptions::new().include_ignored(true)))
801                    .map_err(convert_libgit2_error)?;
802
803                match statuses.is_empty() {
804                    true => Ok(false),
805                    false => {
806                        for status in statuses.iter() {
807                            let status_bits = status.status();
808                            if status_bits.intersects(git2::Status::IGNORED) {
809                                return Ok(true);
810                            }
811                        }
812                        Ok(false)
813                    },
814                }
815            },
816        }
817    }
818
819    pub fn status(&self, is_worktree: bool) -> Result<RepoStatus, String> {
820        let operation = match self.0.state() {
821            git2::RepositoryState::Clean => None,
822            state => Some(state),
823        };
824
825        let empty = self.is_empty()?;
826
827        let remotes = self
828            .0
829            .remotes()
830            .map_err(convert_libgit2_error)?
831            .iter()
832            .map(|repo_name| repo_name.expect("Worktree name is invalid utf-8."))
833            .map(|repo_name| repo_name.to_owned())
834            .collect::<Vec<String>>();
835
836        let head = match is_worktree {
837            true => None,
838            false => {
839                match empty {
840                    true => None,
841                    false => Some(self.head_branch()?.name()?),
842                }
843            },
844        };
845
846        let changes = match is_worktree {
847            true => None,
848            false => {
849                let statuses = self
850                    .0
851                    .statuses(Some(
852                        git2::StatusOptions::new().include_ignored(false).include_untracked(true),
853                    ))
854                    .map_err(convert_libgit2_error)?;
855
856                match statuses.is_empty() {
857                    true => None,
858                    false => {
859                        let mut files_new = 0;
860                        let mut files_modified = 0;
861                        let mut files_deleted = 0;
862                        for status in statuses.iter() {
863                            let status_bits = status.status();
864                            if status_bits.intersects(
865                                git2::Status::INDEX_MODIFIED
866                                    | git2::Status::INDEX_RENAMED
867                                    | git2::Status::INDEX_TYPECHANGE
868                                    | git2::Status::WT_MODIFIED
869                                    | git2::Status::WT_RENAMED
870                                    | git2::Status::WT_TYPECHANGE,
871                            ) {
872                                files_modified += 1;
873                            } else if status_bits
874                                .intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW)
875                            {
876                                files_new += 1;
877                            } else if status_bits
878                                .intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
879                            {
880                                files_deleted += 1;
881                            }
882                        }
883                        if (files_new, files_modified, files_deleted) == (0, 0, 0) {
884                            panic!(
885                                "is_empty() returned true, but no file changes were detected. This is a bug!"
886                            );
887                        }
888                        Some(RepoChanges { files_new, files_modified, files_deleted })
889                    },
890                }
891            },
892        };
893
894        let worktrees = self.0.worktrees().unwrap().len();
895
896        let submodules = match is_worktree {
897            true => None,
898            false => {
899                let mut submodules = Vec::new();
900                for submodule in self.0.submodules().unwrap() {
901                    let submodule_name = submodule.name().unwrap().to_string();
902
903                    let submodule_status;
904                    let status = self
905                        .0
906                        .submodule_status(submodule.name().unwrap(), git2::SubmoduleIgnore::None)
907                        .unwrap();
908
909                    if status.intersects(
910                        git2::SubmoduleStatus::WD_INDEX_MODIFIED
911                            | git2::SubmoduleStatus::WD_WD_MODIFIED
912                            | git2::SubmoduleStatus::WD_UNTRACKED,
913                    ) {
914                        submodule_status = SubmoduleStatus::Changed;
915                    } else if status.is_wd_uninitialized() {
916                        submodule_status = SubmoduleStatus::Uninitialized;
917                    } else if status.is_wd_modified() {
918                        submodule_status = SubmoduleStatus::OutOfDate;
919                    } else {
920                        submodule_status = SubmoduleStatus::Clean;
921                    }
922
923                    submodules.push((submodule_name, submodule_status));
924                }
925                Some(submodules)
926            },
927        };
928
929        let mut branches = Vec::new();
930        for (local_branch, _) in self
931            .0
932            .branches(Some(git2::BranchType::Local))
933            .unwrap()
934            .map(|branch_name| branch_name.unwrap())
935        {
936            let branch_name = local_branch.name().unwrap().unwrap().to_string();
937            let remote_branch = match local_branch.upstream() {
938                Ok(remote_branch) => {
939                    let remote_branch_name = remote_branch.name().unwrap().unwrap().to_string();
940
941                    let (ahead, behind) = self
942                        .0
943                        .graph_ahead_behind(
944                            local_branch.get().peel_to_commit().unwrap().id(),
945                            remote_branch.get().peel_to_commit().unwrap().id(),
946                        )
947                        .unwrap();
948
949                    let remote_tracking_status = match (ahead, behind) {
950                        (0, 0) => RemoteTrackingStatus::UpToDate,
951                        (0, d) => RemoteTrackingStatus::Behind(d),
952                        (d, 0) => RemoteTrackingStatus::Ahead(d),
953                        (d1, d2) => RemoteTrackingStatus::Diverged(d1, d2),
954                    };
955                    Some((remote_branch_name, remote_tracking_status))
956                },
957                // Err => no remote branch
958                Err(_) => None,
959            };
960            branches.push((branch_name, remote_branch));
961        }
962
963        Ok(RepoStatus { operation, empty, remotes, head, changes, worktrees, submodules, branches })
964    }
965
966    pub fn get_remote_default_branch(&self, remote_name: &str) -> Result<Option<Branch>, String> {
967        // libgit2's `git_remote_default_branch()` and `Remote::default_branch()`
968        // need an actual connection to the remote, so they may fail.
969        if let Some(mut remote) = self.find_remote(remote_name)? {
970            if remote.connected() {
971                let remote = remote; // unmut
972                if let Ok(remote_default_branch) = remote.default_branch() {
973                    return Ok(Some(self.find_local_branch(&remote_default_branch)?));
974                };
975            }
976        }
977
978        // Note that <remote>/HEAD only exists after a normal clone, there is no way to get the
979        // remote HEAD afterwards. So this is a "best effort" approach.
980        if let Ok(remote_head) = self.find_remote_branch(remote_name, "HEAD") {
981            if let Some(pointer_name) = remote_head.as_reference().symbolic_target() {
982                if let Some(local_branch_name) =
983                    pointer_name.strip_prefix(&format!("refs/remotes/{}/", remote_name))
984                {
985                    return Ok(Some(self.find_local_branch(local_branch_name)?));
986                } else {
987                    eprintln!("Remote HEAD ({}) pointer is invalid", pointer_name);
988                }
989            } else {
990                eprintln!("Remote HEAD does not point to a symbolic target");
991            }
992        }
993        Ok(None)
994    }
995
996    pub fn default_branch(&self) -> Result<Branch, String> {
997        // This is a bit of a guessing game.
998        //
999        // In the best case, there is only one remote. Then, we can check <remote>/HEAD to get the
1000        // default remote branch.
1001        //
1002        // If there are multiple remotes, we first check whether they all have the same
1003        // <remote>/HEAD branch. If yes, good! If not, we use whatever "origin" uses, if that
1004        // exists. If it does not, there is no way to reliably get a remote default branch.
1005        //
1006        // In this case, we just try to guess a local branch from a list. If even that does not
1007        // work, well, bad luck.
1008        let remotes = self.remotes()?;
1009
1010        if remotes.len() == 1 {
1011            let remote_name = &remotes[0];
1012            if let Some(default_branch) = self.get_remote_default_branch(remote_name)? {
1013                return Ok(default_branch);
1014            }
1015        } else {
1016            let mut default_branches: Vec<Branch> = vec![];
1017            for remote_name in remotes {
1018                if let Some(default_branch) = self.get_remote_default_branch(&remote_name)? {
1019                    default_branches.push(default_branch)
1020                }
1021            }
1022
1023            if !default_branches.is_empty()
1024                && (default_branches.len() == 1
1025                    || default_branches.windows(2).all(|w| w[0].name() == w[1].name()))
1026            {
1027                return Ok(default_branches.remove(0));
1028            }
1029        }
1030
1031        for branch_name in &["main", "master"] {
1032            if let Ok(branch) = self.0.find_branch(branch_name, git2::BranchType::Local) {
1033                return Ok(Branch(branch));
1034            }
1035        }
1036
1037        Err(String::from("Could not determine default branch"))
1038    }
1039
1040    // Looks like there is no distinguishing between the error cases
1041    // "no such remote" and "failed to get remote for some reason".
1042    // May be a good idea to handle this explicitly, by returning a
1043    // Result<Option<RemoteHandle>, String> instead, Returning Ok(None)
1044    // on "not found" and Err() on an actual error.
1045    pub fn find_remote(&self, remote_name: &str) -> Result<Option<RemoteHandle>, String> {
1046        let remotes = self.0.remotes().map_err(convert_libgit2_error)?;
1047
1048        if !remotes
1049            .iter()
1050            .any(|remote| remote.expect("Remote name is invalid utf-8") == remote_name)
1051        {
1052            return Ok(None);
1053        }
1054
1055        Ok(Some(RemoteHandle(self.0.find_remote(remote_name).map_err(convert_libgit2_error)?)))
1056    }
1057
1058    pub fn get_worktrees(&self) -> Result<Vec<Worktree>, String> {
1059        Ok(self
1060            .0
1061            .worktrees()
1062            .map_err(convert_libgit2_error)?
1063            .iter()
1064            .map(|name| name.expect("Worktree name is invalid utf-8"))
1065            .map(Worktree::new)
1066            .collect())
1067    }
1068
1069    pub fn remove_worktree(
1070        &self,
1071        base_dir: &Path,
1072        name: &str,
1073        worktree_dir: &Path,
1074        force: bool,
1075        worktree_config: &Option<WorktreeRootConfig>,
1076    ) -> Result<(), WorktreeRemoveFailureReason> {
1077        let fullpath = base_dir.join(worktree_dir);
1078
1079        if !fullpath.exists() {
1080            return Err(WorktreeRemoveFailureReason::Error(format!("{} does not exist", name)));
1081        }
1082        let worktree_repo = RepoHandle::open(&fullpath, false).map_err(|error| {
1083            WorktreeRemoveFailureReason::Error(format!("Error opening repo: {}", error))
1084        })?;
1085
1086        let local_branch = worktree_repo.head_branch().map_err(|error| {
1087            WorktreeRemoveFailureReason::Error(format!("Failed getting head branch: {}", error))
1088        })?;
1089
1090        let branch_name = local_branch.name().map_err(|error| {
1091            WorktreeRemoveFailureReason::Error(format!("Failed getting name of branch: {}", error))
1092        })?;
1093
1094        if branch_name != name {
1095            return Err(WorktreeRemoveFailureReason::Error(format!(
1096                "Branch \"{}\" is checked out in worktree \"{}\", this does not look correct",
1097                &branch_name,
1098                &worktree_dir.display(),
1099            )));
1100        }
1101
1102        let branch = worktree_repo
1103            .find_local_branch(&branch_name)
1104            .map_err(WorktreeRemoveFailureReason::Error)?;
1105
1106        if !force {
1107            let status = worktree_repo.status(false).map_err(WorktreeRemoveFailureReason::Error)?;
1108            if status.changes.is_some() {
1109                return Err(WorktreeRemoveFailureReason::Changes(String::from(
1110                    "Changes found in worktree",
1111                )));
1112            }
1113
1114            let mut is_merged_into_persistent_branch = false;
1115            let mut has_persistent_branches = false;
1116            if let Some(config) = worktree_config {
1117                if let Some(branches) = &config.persistent_branches {
1118                    has_persistent_branches = true;
1119                    for persistent_branch in branches {
1120                        let persistent_branch = worktree_repo
1121                            .find_local_branch(persistent_branch)
1122                            .map_err(WorktreeRemoveFailureReason::Error)?;
1123
1124                        let (ahead, _behind) =
1125                            worktree_repo.graph_ahead_behind(&branch, &persistent_branch).unwrap();
1126
1127                        if ahead == 0 {
1128                            is_merged_into_persistent_branch = true;
1129                        }
1130                    }
1131                }
1132            }
1133
1134            if has_persistent_branches && !is_merged_into_persistent_branch {
1135                return Err(WorktreeRemoveFailureReason::NotMerged(format!(
1136                    "Branch {} is not merged into any persistent branches",
1137                    name
1138                )));
1139            }
1140
1141            if !has_persistent_branches {
1142                match branch.upstream() {
1143                    Ok(remote_branch) => {
1144                        let (ahead, behind) =
1145                            worktree_repo.graph_ahead_behind(&branch, &remote_branch).unwrap();
1146
1147                        if (ahead, behind) != (0, 0) {
1148                            return Err(WorktreeRemoveFailureReason::Changes(format!(
1149                                "Branch {} is not in line with remote branch",
1150                                name
1151                            )));
1152                        }
1153                    },
1154                    Err(_) => {
1155                        return Err(WorktreeRemoveFailureReason::Changes(format!(
1156                            "No remote tracking branch for branch {} found",
1157                            name
1158                        )));
1159                    },
1160                }
1161            }
1162        }
1163
1164        // worktree_dir is a relative path, starting from base_dir. We walk it
1165        // upwards (from subdirectory to parent directories) and remove each
1166        // component, in case it is empty. Only the leaf directory can be
1167        // removed unconditionally (as it contains the worktree itself).
1168        if let Err(e) = std::fs::remove_dir_all(&fullpath) {
1169            return Err(WorktreeRemoveFailureReason::Error(format!(
1170                "Error deleting {}: {}",
1171                &worktree_dir.display(),
1172                e
1173            )));
1174        }
1175
1176        if let Some(current_dir) = worktree_dir.parent() {
1177            for current_dir in current_dir.ancestors() {
1178                let current_dir = base_dir.join(current_dir);
1179                if current_dir
1180                    .read_dir()
1181                    .map_err(|error| {
1182                        WorktreeRemoveFailureReason::Error(format!(
1183                            "Error reading {}: {}",
1184                            &current_dir.display(),
1185                            error
1186                        ))
1187                    })?
1188                    .next()
1189                    .is_none()
1190                {
1191                    if let Err(e) = std::fs::remove_dir(&current_dir) {
1192                        return Err(WorktreeRemoveFailureReason::Error(format!(
1193                            "Error deleting {}: {}",
1194                            &worktree_dir.display(),
1195                            e
1196                        )));
1197                    }
1198                } else {
1199                    break;
1200                }
1201            }
1202        }
1203
1204        self.prune_worktree(name).map_err(WorktreeRemoveFailureReason::Error)?;
1205        branch.delete().map_err(WorktreeRemoveFailureReason::Error)?;
1206
1207        Ok(())
1208    }
1209
1210    pub fn cleanup_worktrees(&self, directory: &Path) -> Result<Vec<String>, String> {
1211        let mut warnings = Vec::new();
1212
1213        let worktrees =
1214            self.get_worktrees().map_err(|error| format!("Getting worktrees failed: {}", error))?;
1215
1216        let config = read_worktree_root_config(directory)?;
1217
1218        let guess_default_branch = || {
1219            self.default_branch()
1220                .map_err(|_| "Could not determine default branch")?
1221                .name()
1222                .map_err(|error| format!("Failed getting default branch name: {}", error))
1223        };
1224
1225        let default_branch_name = match &config {
1226            None => guess_default_branch()?,
1227            Some(config) => {
1228                match &config.persistent_branches {
1229                    None => guess_default_branch()?,
1230                    Some(persistent_branches) => {
1231                        if persistent_branches.is_empty() {
1232                            guess_default_branch()?
1233                        } else {
1234                            persistent_branches[0].clone()
1235                        }
1236                    },
1237                }
1238            },
1239        };
1240
1241        for worktree in worktrees
1242            .iter()
1243            .filter(|worktree| worktree.name() != default_branch_name)
1244            .filter(|worktree| {
1245                match &config {
1246                    None => true,
1247                    Some(config) => {
1248                        match &config.persistent_branches {
1249                            None => true,
1250                            Some(branches) => {
1251                                !branches.iter().any(|branch| branch == worktree.name())
1252                            },
1253                        }
1254                    },
1255                }
1256            })
1257        {
1258            let repo_dir = &directory.join(worktree.name());
1259            if repo_dir.exists() {
1260                match self.remove_worktree(
1261                    directory,
1262                    worktree.name(),
1263                    Path::new(worktree.name()),
1264                    false,
1265                    &config,
1266                ) {
1267                    Ok(_) => print_success(&format!("Worktree {} deleted", &worktree.name())),
1268                    Err(error) => {
1269                        match error {
1270                            WorktreeRemoveFailureReason::Changes(changes) => {
1271                                warnings.push(format!(
1272                                    "Changes found in {}: {}, skipping",
1273                                    &worktree.name(),
1274                                    &changes
1275                                ));
1276                                continue;
1277                            },
1278                            WorktreeRemoveFailureReason::NotMerged(message) => {
1279                                warnings.push(message);
1280                                continue;
1281                            },
1282                            WorktreeRemoveFailureReason::Error(error) => {
1283                                return Err(error);
1284                            },
1285                        }
1286                    },
1287                }
1288            } else {
1289                warnings.push(format!("Worktree {} does not have a directory", &worktree.name()));
1290            }
1291        }
1292        Ok(warnings)
1293    }
1294
1295    pub fn find_unmanaged_worktrees(&self, directory: &Path) -> Result<Vec<String>, String> {
1296        let worktrees =
1297            self.get_worktrees().map_err(|error| format!("Getting worktrees failed: {}", error))?;
1298
1299        let mut unmanaged_worktrees = Vec::new();
1300        for entry in std::fs::read_dir(directory).map_err(|error| error.to_string())? {
1301            let dirname = path::path_as_string(
1302                entry
1303                    .map_err(|error| error.to_string())?
1304                    .path()
1305                    .strip_prefix(directory)
1306                    // that unwrap() is safe as each entry is
1307                    // guaranteed to be a subentry of &directory
1308                    .unwrap(),
1309            );
1310
1311            let config = read_worktree_root_config(directory)?;
1312
1313            let guess_default_branch = || {
1314                self.default_branch()
1315                    .map_err(|error| format!("Failed getting default branch: {}", error))?
1316                    .name()
1317                    .map_err(|error| format!("Failed getting default branch name: {}", error))
1318            };
1319
1320            let default_branch_name = match &config {
1321                None => guess_default_branch().ok(),
1322                Some(config) => {
1323                    match &config.persistent_branches {
1324                        None => guess_default_branch().ok(),
1325                        Some(persistent_branches) => {
1326                            if persistent_branches.is_empty() {
1327                                guess_default_branch().ok()
1328                            } else {
1329                                Some(persistent_branches[0].clone())
1330                            }
1331                        },
1332                    }
1333                },
1334            };
1335
1336            if dirname == worktree::GIT_MAIN_WORKTREE_DIRECTORY {
1337                continue;
1338            }
1339            if dirname == WORKTREE_CONFIG_FILE_NAME {
1340                continue;
1341            }
1342            if let Some(default_branch_name) = default_branch_name {
1343                if dirname == default_branch_name {
1344                    continue;
1345                }
1346            }
1347            if !&worktrees.iter().any(|worktree| worktree.name() == dirname) {
1348                unmanaged_worktrees.push(dirname);
1349            }
1350        }
1351        Ok(unmanaged_worktrees)
1352    }
1353
1354    pub fn detect_worktree(path: &Path) -> bool {
1355        path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY).exists()
1356    }
1357}
1358
1359pub struct RemoteHandle<'a>(git2::Remote<'a>);
1360pub struct Commit<'a>(git2::Commit<'a>);
1361pub struct Reference();
1362pub struct Oid(git2::Oid);
1363
1364impl Oid {
1365    pub fn hex_string(&self) -> String {
1366        self.0.to_string()
1367    }
1368}
1369
1370impl Commit<'_> {
1371    pub fn id(&self) -> Oid {
1372        Oid(self.0.id())
1373    }
1374
1375    pub(self) fn author(&self) -> git2::Signature {
1376        self.0.author()
1377    }
1378}
1379
1380impl<'a> Branch<'a> {
1381    pub fn to_commit(self) -> Result<Commit<'a>, String> {
1382        Ok(Commit(self.0.into_reference().peel_to_commit().map_err(convert_libgit2_error)?))
1383    }
1384}
1385
1386impl<'a> Branch<'a> {
1387    pub fn commit(&self) -> Result<Commit, String> {
1388        Ok(Commit(self.0.get().peel_to_commit().map_err(convert_libgit2_error)?))
1389    }
1390
1391    pub fn commit_owned(self) -> Result<Commit<'a>, String> {
1392        Ok(Commit(self.0.into_reference().peel_to_commit().map_err(convert_libgit2_error)?))
1393    }
1394
1395    pub fn set_upstream(&mut self, remote_name: &str, branch_name: &str) -> Result<(), String> {
1396        self.0
1397            .set_upstream(Some(&format!("{}/{}", remote_name, branch_name)))
1398            .map_err(convert_libgit2_error)?;
1399        Ok(())
1400    }
1401
1402    pub fn name(&self) -> Result<String, String> {
1403        self.0
1404            .name()
1405            .map(|name| name.expect("Branch name is invalid utf-8"))
1406            .map_err(convert_libgit2_error)
1407            .map(|name| name.to_string())
1408    }
1409
1410    pub fn upstream(&self) -> Result<Branch, String> {
1411        Ok(Branch(self.0.upstream().map_err(convert_libgit2_error)?))
1412    }
1413
1414    pub fn delete(mut self) -> Result<(), String> {
1415        self.0.delete().map_err(convert_libgit2_error)
1416    }
1417
1418    pub fn basename(&self) -> Result<String, String> {
1419        let name = self.name()?;
1420        if let Some((_prefix, basename)) = name.split_once('/') {
1421            Ok(basename.to_string())
1422        } else {
1423            Ok(name)
1424        }
1425    }
1426
1427    // only used internally in this module, exposes libgit2 details
1428    fn as_reference(&self) -> &git2::Reference {
1429        self.0.get()
1430    }
1431}
1432
1433fn get_remote_callbacks() -> git2::RemoteCallbacks<'static> {
1434    let mut callbacks = git2::RemoteCallbacks::new();
1435    callbacks.push_update_reference(|_, status| {
1436        if let Some(message) = status {
1437            return Err(git2::Error::new(
1438                git2::ErrorCode::GenericError,
1439                git2::ErrorClass::None,
1440                message,
1441            ));
1442        }
1443        Ok(())
1444    });
1445
1446    callbacks.credentials(|_url, username_from_url, _allowed_types| {
1447        let username = match username_from_url {
1448            Some(username) => username,
1449            None => panic!("Could not get username. This is a bug"),
1450        };
1451        git2::Cred::ssh_key_from_agent(username)
1452    });
1453
1454    callbacks
1455}
1456
1457impl RemoteHandle<'_> {
1458    pub fn url(&self) -> String {
1459        self.0.url().expect("Remote URL is invalid utf-8").to_string()
1460    }
1461
1462    pub fn name(&self) -> String {
1463        self.0.name().expect("Remote name is invalid utf-8").to_string()
1464    }
1465
1466    pub fn connected(&mut self) -> bool {
1467        self.0.connected()
1468    }
1469
1470    pub fn default_branch(&self) -> Result<String, String> {
1471        Ok(self
1472            .0
1473            .default_branch()
1474            .map_err(convert_libgit2_error)?
1475            .as_str()
1476            .expect("Remote branch name is not valid utf-8")
1477            .to_string())
1478    }
1479
1480    pub fn is_pushable(&self) -> Result<bool, String> {
1481        let remote_type =
1482            RemoteType::from_str(self.0.url().expect("Remote name is not valid utf-8"))
1483                .expect("Could not detect remote type");
1484        Ok(matches!(remote_type, RemoteType::SSH | RemoteType::File))
1485    }
1486
1487    pub fn push(
1488        &mut self,
1489        local_branch_name: &str,
1490        remote_branch_name: &str,
1491        _repo: &RepoHandle,
1492    ) -> Result<(), String> {
1493        if !self.is_pushable()? {
1494            return Err(String::from("Trying to push to a non-pushable remote"));
1495        }
1496
1497        let mut push_options = git2::PushOptions::new();
1498        push_options.remote_callbacks(get_remote_callbacks());
1499
1500        let push_refspec =
1501            format!("+refs/heads/{}:refs/heads/{}", local_branch_name, remote_branch_name);
1502        self.0.push(&[push_refspec], Some(&mut push_options)).map_err(|error| {
1503            format!(
1504                "Pushing {} to {} ({}) failed: {}",
1505                local_branch_name,
1506                self.name(),
1507                self.url(),
1508                error
1509            )
1510        })?;
1511        Ok(())
1512    }
1513}
1514
1515pub fn clone_repo(
1516    remote: &Remote,
1517    path: &Path,
1518    is_worktree: bool,
1519) -> Result<(), Box<dyn std::error::Error>> {
1520    let clone_target = match is_worktree {
1521        false => path.to_path_buf(),
1522        true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY),
1523    };
1524
1525    print_action(&format!("Cloning into \"{}\" from \"{}\"", &clone_target.display(), &remote.url));
1526    match remote.remote_type {
1527        RemoteType::HTTPS | RemoteType::File => {
1528            let mut builder = git2::build::RepoBuilder::new();
1529
1530            let fetchopts = git2::FetchOptions::new();
1531
1532            builder.bare(is_worktree);
1533            builder.fetch_options(fetchopts);
1534
1535            builder.clone(&remote.url, &clone_target)?;
1536        },
1537        RemoteType::SSH => {
1538            let mut fo = git2::FetchOptions::new();
1539            fo.remote_callbacks(get_remote_callbacks());
1540
1541            let mut builder = git2::build::RepoBuilder::new();
1542            builder.bare(is_worktree);
1543            builder.fetch_options(fo);
1544
1545            builder.clone(&remote.url, &clone_target)?;
1546        },
1547    }
1548
1549    let repo = RepoHandle::open(&clone_target, false)?;
1550
1551    if is_worktree {
1552        repo.set_config_push(GitPushDefaultSetting::Upstream)?;
1553    }
1554
1555    if remote.name != "origin" {
1556        // unwrap() is safe here as the origin remote will always exist after a successful clone.
1557        // Note that actual errors are handled in the Results Err variant, not in
1558        // the Ok variant option
1559        let origin = repo.find_remote("origin")?.unwrap();
1560        repo.rename_remote(&origin, &remote.name)?;
1561    }
1562
1563    // Initialize local branches. For all remote branches, we set up local
1564    // tracking branches with the same name (just without the remote prefix).
1565    for remote_branch in repo.remote_branches()? {
1566        let local_branch_name = remote_branch.basename()?;
1567
1568        if repo.find_local_branch(&local_branch_name).is_ok() {
1569            continue;
1570        }
1571
1572        // Ignore <remote>/HEAD, as this is not something we can check out
1573        if local_branch_name == "HEAD" {
1574            continue;
1575        }
1576
1577        let mut local_branch = repo.create_branch(&local_branch_name, &remote_branch.commit()?)?;
1578        local_branch.set_upstream(&remote.name, &local_branch_name)?;
1579    }
1580
1581    // If there is no head_branch, we most likely cloned an empty repository and
1582    // there is no point in setting any upstreams.
1583    if let Ok(mut active_branch) = repo.head_branch() {
1584        active_branch.set_upstream(&remote.name, &active_branch.name()?)?;
1585    };
1586
1587    Ok(())
1588}