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, Some(&remote_annotated_commit),
228 None, 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 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 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, Some(&base_annotated_commit),
356 None, 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 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 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 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 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 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(_) => 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 if let Some(mut remote) = self.find_remote(remote_name)? {
970 if remote.connected() {
971 let remote = remote; 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 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 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 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 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 ¤t_dir.display(),
1193 error
1194 ))
1195 })?
1196 .next()
1197 .is_none()
1198 {
1199 if let Err(e) = std::fs::remove_dir(¤t_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 .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 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 let origin = repo.find_remote("origin")?.unwrap();
1590 repo.rename_remote(&origin, &remote.name)?;
1591 }
1592
1593 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 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 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}