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