grm/
repo.rs

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