1mod error;
210
211pub use error::{
212 CleanupWorktreeError, CleanupWorktreeWarning, CleanupWorktreeWarningReason, Error,
213 WorktreeConversionError, WorktreeRemoveError, WorktreeValidationError,
214 WorktreeValidationErrorReason,
215};
216
217use std::{fmt, iter, sync::mpsc};
218
219use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
220
221use super::{Branch, BranchName, RemoteName, RepoHandle, Warning, config};
222use crate::{
223 path,
224 repo::{self, RepoChanges},
225};
226
227pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
228
229pub struct Worktree {
230 name: WorktreeName,
231}
232
233impl Worktree {
234 fn new(name: &str) -> Result<Self, WorktreeValidationError> {
237 Ok(Self {
238 name: WorktreeName::new(name.to_owned())?,
239 })
240 }
241
242 pub fn name(&self) -> &WorktreeName {
243 &self.name
244 }
245
246 fn into_name(self) -> WorktreeName {
247 self.name
248 }
249
250 pub fn forward_branch(&self, rebase: bool, stash: bool) -> Result<Option<Warning>, Error> {
251 let repo = RepoHandle::open(Path::new(&self.name.as_str()))?;
252
253 let branch_name = BranchName::new(self.name.as_str().to_owned());
254
255 if let Some(remote_branch) = repo
256 .find_local_branch(&branch_name)?
257 .ok_or(Error::BranchNotFound(branch_name))?
258 .upstream()?
259 {
260 let status = repo.status(WorktreeSetup::NoWorktree)?;
261 let mut stashed_changes = false;
262
263 if !status.clean() {
264 if stash {
265 repo.stash()?;
266 stashed_changes = true;
267 } else {
268 return Ok(Some(Warning(String::from("Worktree contains changes"))));
269 }
270 }
271
272 let unstash = || -> Result<(), Error> {
273 if stashed_changes {
274 repo.stash_pop()?;
275 }
276 Ok(())
277 };
278
279 let remote_annotated_commit = repo
280 .0
281 .find_annotated_commit(remote_branch.commit()?.id().0)?;
282
283 if rebase {
284 let mut rebase = repo.0.rebase(
285 None, Some(&remote_annotated_commit),
287 None, Some(&mut git2::RebaseOptions::new()),
289 )?;
290
291 while let Some(operation) = rebase.next() {
292 let operation = operation?;
293
294 let rebased_commit = repo.0.find_commit(operation.id())?;
297 let committer = rebased_commit.committer();
298
299 let mut index = repo.0.index()?;
302 index.add_all(iter::once("."), git2::IndexAddOption::CHECK_PATHSPEC, None)?;
303
304 if let Err(error) = rebase.commit(None, &committer, None) {
305 if error.code() == git2::ErrorCode::Applied {
306 continue;
307 }
308 rebase.abort()?;
309 unstash()?;
310 return Err(error.into());
311 }
312 }
313
314 rebase.finish(None)?;
315 } else {
316 let (analysis, _preference) = repo.0.merge_analysis(&[&remote_annotated_commit])?;
317
318 if analysis.is_up_to_date() {
319 unstash()?;
320 return Ok(None);
321 }
322 if !analysis.is_fast_forward() {
323 unstash()?;
324 return Ok(Some(Warning(String::from(
325 "Worktree cannot be fast forwarded",
326 ))));
327 }
328
329 repo.0.reset(
330 remote_branch.commit()?.0.as_object(),
331 git2::ResetType::Hard,
332 Some(git2::build::CheckoutBuilder::new().safe()),
333 )?;
334 }
335 unstash()?;
336 } else {
337 return Ok(Some(Warning(String::from(
338 "No remote branch to rebase onto",
339 ))));
340 }
341
342 Ok(None)
343 }
344
345 pub fn rebase_onto_default(
346 &self,
347 config: &Option<WorktreeRootConfig>,
348 stash: bool,
349 ) -> Result<Option<Warning>, Error> {
350 let repo = RepoHandle::open(Path::new(&self.name.as_str()))?;
351
352 let guess_default_branch = || repo.default_branch()?.name();
353
354 let default_branch_name = match *config {
355 None => guess_default_branch()?,
356 Some(ref config) => match config.persistent_branches {
357 None => guess_default_branch()?,
358 Some(ref persistent_branches) => {
359 if let Some(branch) = persistent_branches.first() {
360 branch.clone()
361 } else {
362 guess_default_branch()?
363 }
364 }
365 },
366 };
367
368 let status = repo.status(WorktreeSetup::NoWorktree)?;
369 let mut stashed_changes = false;
370
371 if !status.clean() {
372 if stash {
373 repo.stash()?;
374 stashed_changes = true;
375 } else {
376 return Ok(Some(Warning("Worktree contains changes".to_owned())));
377 }
378 }
379
380 let unstash = || -> Result<(), Error> {
381 if stashed_changes {
382 repo.stash_pop()?;
383 }
384 Ok(())
385 };
386
387 let base_branch = repo
388 .find_local_branch(&default_branch_name)?
389 .ok_or(Error::BranchNotFound(default_branch_name))?;
390 let base_annotated_commit = repo.0.find_annotated_commit(base_branch.commit()?.id().0)?;
391
392 let mut rebase = repo.0.rebase(
393 None, Some(&base_annotated_commit),
395 None, Some(&mut git2::RebaseOptions::new()),
397 )?;
398
399 while let Some(operation) = rebase.next() {
400 let operation = operation?;
401
402 let rebased_commit = repo.0.find_commit(operation.id())?;
405 let committer = rebased_commit.committer();
406
407 let mut index = repo.0.index()?;
410 index.add_all(iter::once("."), git2::IndexAddOption::CHECK_PATHSPEC, None)?;
411
412 if let Err(error) = rebase.commit(None, &committer, None) {
413 if error.code() == git2::ErrorCode::Applied {
414 continue;
415 }
416 rebase.abort()?;
417 unstash()?;
418 return Err(error.into());
419 }
420 }
421
422 rebase.finish(None)?;
423 unstash()?;
424 Ok(None)
425 }
426}
427
428#[derive(Debug, PartialEq, Eq, Clone, Copy)]
429pub enum WorktreeSetup {
430 Worktree,
431 NoWorktree,
432}
433
434impl WorktreeSetup {
435 pub fn is_worktree(&self) -> bool {
436 *self == Self::Worktree
437 }
438
439 pub fn detect(path: &Path) -> Self {
440 if path.join(GIT_MAIN_WORKTREE_DIRECTORY).exists() {
441 Self::Worktree
442 } else {
443 Self::NoWorktree
444 }
445 }
446}
447
448impl From<bool> for WorktreeSetup {
449 fn from(value: bool) -> Self {
450 if value {
451 Self::Worktree
452 } else {
453 Self::NoWorktree
454 }
455 }
456}
457
458struct Init;
459
460enum LocalBranchInfo<'a> {
461 NoBranch,
462 Branch(repo::Branch<'a>),
463}
464
465struct WithLocalBranchName<'a> {
466 local_branch_name: BranchName,
467 local_branch: LocalBranchInfo<'a>,
468}
469
470struct WithLocalTargetSelected<'a> {
471 local_branch_name: BranchName,
472 local_branch: Option<repo::Branch<'a>>,
473 target_commit: Option<repo::Commit<'a>>,
474}
475
476struct RemoteTrackingBranch {
477 remote_name: RemoteName,
478 remote_branch_name: BranchName,
479 prefix: Option<String>,
480}
481
482struct WithRemoteTrackingBranch<'a> {
483 local_branch_name: BranchName,
484 local_branch: Option<repo::Branch<'a>>,
485 target_commit: Option<repo::Commit<'a>>,
486 remote_tracking_branch: Option<RemoteTrackingBranch>,
487}
488
489struct NewWorktree<'a, S: WorktreeState> {
490 repo: &'a WorktreeRepoHandle,
491 extra: S,
492}
493
494impl<'a> WithLocalBranchName<'a> {
495 fn new(name: &BranchName, worktree: &NewWorktree<'a, Init>) -> Result<Self, Error> {
496 Ok(Self {
497 local_branch_name: name.clone(),
498 local_branch: {
499 let branch = worktree.repo.as_repo().find_local_branch(name)?;
500 match branch {
501 Some(branch) => LocalBranchInfo::Branch(branch),
502 None => LocalBranchInfo::NoBranch,
503 }
504 },
505 })
506 }
507}
508
509trait WorktreeState {}
510
511impl WorktreeState for Init {}
512impl WorktreeState for WithLocalBranchName<'_> {}
513impl WorktreeState for WithLocalTargetSelected<'_> {}
514impl WorktreeState for WithRemoteTrackingBranch<'_> {}
515
516impl<'a> NewWorktree<'a, Init> {
517 fn new(repo: &'a WorktreeRepoHandle) -> Self {
518 Self {
519 repo,
520 extra: Init {},
521 }
522 }
523
524 fn set_local_branch_name(
525 self,
526 name: &BranchName,
527 ) -> Result<NewWorktree<'a, WithLocalBranchName<'a>>, Error> {
528 Ok(NewWorktree::<WithLocalBranchName> {
529 repo: self.repo,
530 extra: WithLocalBranchName::new(name, &self)?,
531 })
532 }
533}
534
535impl<'a, 'b> NewWorktree<'a, WithLocalBranchName<'b>>
536where
537 'a: 'b,
538{
539 fn local_branch_already_exists(&self) -> bool {
540 matches!(
541 self.extra.local_branch,
542 LocalBranchInfo::Branch(ref _branch)
543 )
544 }
545
546 fn select_commit(
547 self,
548 commit: Option<repo::Commit<'b>>,
549 ) -> NewWorktree<'a, WithLocalTargetSelected<'b>> {
550 NewWorktree::<'a, WithLocalTargetSelected> {
551 repo: self.repo,
552 extra: WithLocalTargetSelected::<'b> {
553 local_branch_name: self.extra.local_branch_name,
554 local_branch: match self.extra.local_branch {
557 LocalBranchInfo::NoBranch => None,
558 LocalBranchInfo::Branch(branch) => Some(branch),
559 },
560 target_commit: commit,
561 },
562 }
563 }
564}
565
566impl<'a> NewWorktree<'a, WithLocalTargetSelected<'a>> {
567 fn set_remote_tracking_branch(
568 self,
569 branch: Option<RemoteTrackingBranch>,
570 ) -> NewWorktree<'a, WithRemoteTrackingBranch<'a>> {
571 NewWorktree::<WithRemoteTrackingBranch> {
572 repo: self.repo,
573 extra: WithRemoteTrackingBranch {
574 local_branch_name: self.extra.local_branch_name,
575 local_branch: self.extra.local_branch,
576 target_commit: self.extra.target_commit,
577 remote_tracking_branch: branch,
578 },
579 }
580 }
581}
582
583impl<'a> NewWorktree<'a, WithRemoteTrackingBranch<'a>> {
584 fn create(self, directory: &Path) -> Result<Option<Vec<Warning>>, Error> {
585 let mut warnings: Vec<Warning> = vec![];
586
587 let mut branch = if let Some(branch) = self.extra.local_branch {
588 branch
589 } else {
590 self.repo.as_repo().create_branch(
591 &self.extra.local_branch_name,
592 &self
596 .extra
597 .target_commit
598 .expect("target_commit must not be empty"),
599 )?
600 };
601
602 if let Some(remote_branch_config) = self.extra.remote_tracking_branch {
603 let remote_branch_with_prefix = if let Some(ref prefix) = remote_branch_config.prefix {
604 self.repo.as_repo().find_remote_branch(
605 &remote_branch_config.remote_name,
606 &BranchName::new(format!(
607 "{prefix}/{}",
608 remote_branch_config.remote_branch_name
609 )),
610 )?
611 } else {
612 None
613 };
614
615 let remote_branch_without_prefix = self.repo.as_repo().find_remote_branch(
616 &remote_branch_config.remote_name,
617 &remote_branch_config.remote_branch_name,
618 )?;
619
620 let remote_branch = if let Some(ref _prefix) = remote_branch_config.prefix {
621 remote_branch_with_prefix
622 } else {
623 remote_branch_without_prefix
624 };
625
626 if let Some(remote_branch) = remote_branch {
627 if branch.commit()?.id().hex_string() != remote_branch.commit()?.id().hex_string() {
628 warnings.push(Warning(format!("The local branch \"{}\" and the remote branch \"{}/{}\" differ. Make sure to push/pull afterwards!", &self.extra.local_branch_name, &remote_branch_config.remote_name, &remote_branch_config.remote_branch_name)));
629 }
630
631 branch.set_upstream(
632 &remote_branch_config.remote_name,
633 &remote_branch.basename()?,
634 )?;
635 } else {
636 let Some(mut remote) = self
637 .repo
638 .as_repo()
639 .find_remote(&remote_branch_config.remote_name)?
640 else {
641 return Err(Error::RemoteNotFound {
642 name: remote_branch_config.remote_name,
643 });
644 };
645
646 if !remote.is_pushable()? {
647 return Err(Error::RemoteNotPushable {
648 name: remote_branch_config.remote_name,
649 });
650 }
651
652 if let Some(prefix) = remote_branch_config.prefix {
653 remote.push(
654 &self.extra.local_branch_name,
655 &BranchName::new(format!(
656 "{prefix}/{}",
657 remote_branch_config.remote_branch_name
658 )),
659 self.repo.as_repo(),
660 )?;
661
662 branch.set_upstream(
663 &remote_branch_config.remote_name,
664 &BranchName::new(format!(
665 "{prefix}/{}",
666 remote_branch_config.remote_branch_name
667 )),
668 )?;
669 } else {
670 remote.push(
671 &self.extra.local_branch_name,
672 &remote_branch_config.remote_branch_name,
673 self.repo.as_repo(),
674 )?;
675
676 branch.set_upstream(
677 &remote_branch_config.remote_name,
678 &remote_branch_config.remote_branch_name,
679 )?;
680 }
681 }
682 }
683
684 let branch_name = self.extra.local_branch_name.into_string();
685 if branch_name.contains('/') {
688 let path = Path::new(&branch_name);
689 if let Some(base) = path.parent() {
690 std::fs::create_dir_all(
749 directory
750 .join(GIT_MAIN_WORKTREE_DIRECTORY)
751 .join("worktrees")
752 .join(base),
753 )?;
754 std::fs::create_dir_all(base)?;
755 }
756 }
757
758 self.repo
759 .new_worktree(&branch_name, &directory.join(&branch_name), &branch)?;
760
761 Ok(if warnings.is_empty() {
762 None
763 } else {
764 Some(warnings)
765 })
766 }
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct WorktreeName(String);
771
772impl fmt::Display for WorktreeName {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 write!(f, "{}", self.0)
775 }
776}
777
778impl WorktreeName {
779 pub fn new(name: String) -> Result<Self, WorktreeValidationError> {
780 if name.starts_with('/') || name.ends_with('/') {
781 return Err(WorktreeValidationError {
782 name,
783 reason: WorktreeValidationErrorReason::SlashAtStartOrEnd,
784 });
785 }
786
787 if name.contains("//") {
788 return Err(WorktreeValidationError {
789 name,
790 reason: WorktreeValidationErrorReason::ConsecutiveSlashes,
791 });
792 }
793
794 if name.contains(char::is_whitespace) {
795 return Err(WorktreeValidationError {
796 name,
797 reason: WorktreeValidationErrorReason::ContainsWhitespace,
798 });
799 }
800
801 Ok(Self(name))
802 }
803
804 pub fn as_str(&self) -> &str {
805 &self.0
806 }
807}
808
809pub enum TrackingSelection {
810 Explicit {
811 remote_name: RemoteName,
812 remote_branch_name: BranchName,
813 },
814 Automatic,
815 Disabled,
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821
822 #[test]
823 fn invalid_worktree_names() {
824 assert!(WorktreeName::new("/leadingslash".to_owned()).is_err());
825 assert!(WorktreeName::new("trailingslash/".to_owned()).is_err());
826 assert!(WorktreeName::new("//".to_owned()).is_err());
827 assert!(WorktreeName::new("test//test".to_owned()).is_err());
828 assert!(WorktreeName::new("test test".to_owned()).is_err());
829 assert!(WorktreeName::new("test\ttest".to_owned()).is_err());
830 }
831}
832
833pub struct WorktreeRootConfig {
834 pub persistent_branches: Option<Vec<BranchName>>,
835 pub track: Option<TrackingConfig>,
836}
837
838#[derive(Clone, Copy, PartialEq, Eq)]
839pub enum TrackingDefault {
840 Track,
841 NoTrack,
842}
843
844pub struct TrackingConfig {
845 pub default: TrackingDefault,
846 pub default_remote: RemoteName,
847 pub default_remote_prefix: Option<String>,
848}
849
850impl From<config::TrackingConfig> for TrackingConfig {
851 fn from(other: config::TrackingConfig) -> Self {
852 Self {
853 default: if other.default {
854 TrackingDefault::Track
855 } else {
856 TrackingDefault::NoTrack
857 },
858 default_remote: RemoteName::new(other.default_remote),
859 default_remote_prefix: other.default_remote_prefix,
860 }
861 }
862}
863
864impl From<config::WorktreeRootConfig> for WorktreeRootConfig {
865 fn from(other: config::WorktreeRootConfig) -> Self {
866 Self {
867 persistent_branches: other
868 .persistent_branches
869 .map(|branches| branches.into_iter().map(BranchName::new).collect()),
870 track: other.track.map(Into::into),
871 }
872 }
873}
874
875pub struct WorktreeRepoHandle(super::RepoHandle);
876
877impl WorktreeRepoHandle {
878 pub fn open(path: &Path) -> Result<Self, super::Error> {
879 Ok(Self(super::RepoHandle::open_with_worktree_setup(
880 path,
881 WorktreeSetup::Worktree,
882 )?))
883 }
884
885 pub fn as_repo(&self) -> &super::RepoHandle {
886 &self.0
887 }
888
889 fn base_directory(&self) -> Result<&Path, Error> {
890 let commondir = self.0.commondir()?;
891 commondir
892 .parent()
893 .ok_or_else(|| Error::InvalidBaseDirectory {
894 git_dir: commondir.to_owned(),
895 })
896 }
897
898 pub fn from_handle_unchecked(handle: super::RepoHandle) -> Self {
899 Self(handle)
900 }
901
902 pub fn into_handle(self) -> super::RepoHandle {
903 self.0
904 }
905
906 fn worktree_exists(&self, name: &WorktreeName) -> Result<bool, Error> {
907 match self.0.0.find_worktree(name.as_str()) {
908 Ok(_worktree) => Ok(true),
909 Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(false),
910 Err(e) => Err(e.into()),
911 }
912 }
913
914 pub fn default_branch(&self) -> Result<Branch<'_>, Error> {
915 Ok(self.0.default_branch()?)
916 }
917
918 fn find_local_branch(&self, name: &BranchName) -> Result<Option<Branch<'_>>, Error> {
919 Ok(self.0.find_local_branch(name)?)
920 }
921
922 pub fn cleanup_worktrees(
923 &self,
924 directory: &Path,
925 deletion_notify_channel: &mpsc::SyncSender<WorktreeName>,
926 ) -> Result<Vec<CleanupWorktreeWarning>, CleanupWorktreeError> {
927 let mut warnings = Vec::new();
928
929 let worktrees = self.get_worktrees()?;
930
931 let config: Option<WorktreeRootConfig> = config::read_worktree_root_config(directory)
932 .map_err(|e| <config::Error as Into<Error>>::into(e))?
933 .map(Into::into);
934
935 let default_branch = match config {
936 None => self.default_branch()?,
937 Some(ref config) => match config.persistent_branches.as_ref() {
938 None => self.default_branch()?,
939 Some(persistent_branches) => {
940 if let Some(branch) = persistent_branches.first() {
941 self.find_local_branch(branch)?.ok_or_else(|| {
942 CleanupWorktreeError::BranchNotFound {
943 branch_name: branch.to_owned(),
944 }
945 })?
946 } else {
947 self.default_branch()?
948 }
949 }
950 },
951 };
952
953 let default_branch_name = default_branch
954 .name()
955 .map_err(|err| CleanupWorktreeError::BranchName(err))?;
956
957 for worktree in worktrees
958 .into_iter()
959 .filter(|worktree| worktree.name().as_str() != default_branch_name.as_str())
960 .filter(|worktree| match config {
961 None => true,
962 Some(ref config) => match config.persistent_branches.as_ref() {
963 None => true,
964 Some(branches) => !branches
965 .iter()
966 .any(|branch| branch.as_str() == worktree.name().as_str()),
967 },
968 })
969 {
970 let repo_dir = &directory.join(worktree.name().as_str());
971 if repo_dir.exists() {
972 match self.remove_worktree(
973 directory,
974 worktree.name(),
975 Path::new(worktree.name().as_str()),
976 false,
977 config.as_ref(),
978 &default_branch,
979 ) {
980 Ok(()) => {
981 #[expect(
982 clippy::missing_panics_doc,
983 reason = "this is a clear bug, cannot be recovered anyway"
984 )]
985 deletion_notify_channel
986 .send(worktree.into_name())
987 .expect("receiving channel must be open until we are done");
988 }
989 Err(error) => match error {
990 WorktreeRemoveError::Changes(ref changes) => {
991 warnings.push(CleanupWorktreeWarning {
992 worktree_name: worktree.name().to_owned(),
993 reason: CleanupWorktreeWarningReason::UncommittedChanges(*changes),
994 });
995 }
996 WorktreeRemoveError::NotMerged { branch_name } => {
997 warnings.push(CleanupWorktreeWarning {
998 worktree_name: worktree.name().to_owned(),
999 reason: CleanupWorktreeWarningReason::NotMerged { branch_name },
1000 });
1001 }
1002 _ => return Err(CleanupWorktreeError::RemoveError(error)),
1003 },
1004 }
1005 } else {
1006 warnings.push(CleanupWorktreeWarning {
1007 worktree_name: worktree.name().to_owned(),
1008 reason: CleanupWorktreeWarningReason::NoDirectory,
1009 });
1010 }
1011 }
1012 Ok(warnings)
1013 }
1014
1015 pub fn find_unmanaged_worktrees(&self, directory: &Path) -> Result<Vec<PathBuf>, Error> {
1016 let worktrees = self.get_worktrees()?;
1017
1018 let mut unmanaged_worktrees = Vec::new();
1019 for entry in directory.read_dir_utf8()? {
1020 let entry = entry?;
1021 #[expect(clippy::missing_panics_doc, reason = "see expect() message")]
1022 let dirname = entry
1023 .path()
1024 .strip_prefix(directory)
1025 .expect("each entry is guaranteed to have the prefix");
1028
1029 let config: Option<WorktreeRootConfig> =
1030 config::read_worktree_root_config(directory)?.map(Into::into);
1031
1032 let guess_default_branch = || {
1033 self.0
1034 .default_branch()
1035 .map_err(|error| format!("Failed getting default branch: {error}"))?
1036 .name()
1037 .map_err(|error| format!("Failed getting default branch name: {error}"))
1038 };
1039
1040 let default_branch_name = match config {
1041 None => guess_default_branch().ok(),
1042 Some(ref config) => match config.persistent_branches.as_ref() {
1043 None => guess_default_branch().ok(),
1044 Some(persistent_branches) => {
1045 if let Some(branch) = persistent_branches.first() {
1046 Some(branch.clone())
1047 } else {
1048 guess_default_branch().ok()
1049 }
1050 }
1051 },
1052 };
1053
1054 if dirname == GIT_MAIN_WORKTREE_DIRECTORY {
1055 continue;
1056 }
1057
1058 if dirname == config::WORKTREE_CONFIG_FILE_NAME {
1059 continue;
1060 }
1061 if let Some(default_branch_name) = default_branch_name {
1062 if dirname == default_branch_name.as_str() {
1063 continue;
1064 }
1065 }
1066 if !&worktrees
1067 .iter()
1068 .any(|worktree| worktree.name().as_str() == dirname)
1069 {
1070 unmanaged_worktrees.push(PathBuf::from(dirname));
1071 }
1072 }
1073 Ok(unmanaged_worktrees)
1074 }
1075
1076 pub fn get_worktrees(&self) -> Result<Vec<Worktree>, Error> {
1077 Ok(self
1078 .0
1079 .0
1080 .worktrees()?
1081 .iter()
1082 .map(|remote| {
1083 remote
1084 .map_err(|_err| Error::WorktreeNameNotUtf8)
1085 .and_then(|name| name.ok_or(Error::WorktreeNameEmpty))
1086 })
1087 .collect::<Result<Vec<_>, Error>>()?
1088 .into_iter()
1089 .map(Worktree::new)
1090 .collect::<Result<Vec<_>, WorktreeValidationError>>()?)
1091 }
1092
1093 pub fn remove_worktree(
1094 &self,
1095 base_dir: &Path,
1096 worktree_name: &WorktreeName,
1097 worktree_dir: &Path,
1098 force: bool,
1099 worktree_config: Option<&WorktreeRootConfig>,
1100 default_branch: &Branch,
1101 ) -> Result<(), WorktreeRemoveError> {
1102 let fullpath = base_dir.join(worktree_dir);
1113
1114 if !fullpath.exists() {
1115 return Err(WorktreeRemoveError::DoesNotExist(fullpath));
1116 }
1117 let worktree_repo = RepoHandle::open(&fullpath)?;
1118
1119 let local_branch = worktree_repo.head_branch()?;
1120
1121 let branch_name = local_branch.name()?;
1122
1123 if branch_name.as_str() != worktree_name.as_str() {
1124 return Err(WorktreeRemoveError::BranchNameMismatch {
1125 worktree_name: worktree_name.clone(),
1126 branch_name,
1127 });
1128 }
1129
1130 let branch = worktree_repo
1131 .find_local_branch(&branch_name)?
1132 .ok_or_else(|| WorktreeRemoveError::BranchNotFound(branch_name.clone()))?;
1133
1134 if !force {
1135 let status = worktree_repo.status(WorktreeSetup::NoWorktree)?;
1136
1137 if let Some(changes) = status.changes {
1138 return Err(WorktreeRemoveError::Changes(changes));
1139 }
1140
1141 let is_merged_into_default_branch = {
1142 let (ahead_of_default_branch, _behind) =
1143 worktree_repo.graph_ahead_behind(&branch, default_branch)?;
1144
1145 ahead_of_default_branch == 0
1146 };
1147
1148 let mut is_merged_into_persistent_branch = false;
1149 let mut has_persistent_branches = false;
1150 if let Some(config) = worktree_config {
1151 if let Some(branches) = config.persistent_branches.as_ref() {
1152 has_persistent_branches = true;
1153 for persistent_branch in branches {
1154 let persistent_branch = worktree_repo
1155 .find_local_branch(persistent_branch)?
1156 .ok_or_else(|| {
1157 WorktreeRemoveError::BranchNotFound(branch_name.clone())
1158 })?;
1159
1160 let (ahead, _behind) =
1161 worktree_repo.graph_ahead_behind(&branch, &persistent_branch)?;
1162
1163 if ahead == 0 {
1164 is_merged_into_persistent_branch = true;
1165 }
1166 }
1167 }
1168 }
1169
1170 let merged_into_default_or_persistent_branches = is_merged_into_default_branch
1171 || (has_persistent_branches && is_merged_into_persistent_branch);
1172
1173 if !merged_into_default_or_persistent_branches {
1174 return Err(WorktreeRemoveError::NotMerged { branch_name });
1175 }
1176
1177 if let Some(remote_branch) = branch.upstream()? {
1178 let (ahead, behind) = worktree_repo.graph_ahead_behind(&branch, &remote_branch)?;
1179
1180 if (ahead, behind) != (0, 0) {
1181 return Err(WorktreeRemoveError::NotInSyncWithRemote { branch_name });
1182 }
1183 }
1184 }
1185
1186 if let Err(e) = std::fs::remove_dir_all(&fullpath) {
1191 return Err(WorktreeRemoveError::RemoveError {
1192 path: fullpath,
1193 error: e,
1194 });
1195 }
1196
1197 if let Some(current_dir) = worktree_dir.parent() {
1198 for current_dir in current_dir.ancestors() {
1199 let current_dir = base_dir.join(current_dir);
1200 if current_dir
1201 .read_dir()
1202 .map_err(|error| WorktreeRemoveError::ReadDirectoryError {
1203 path: current_dir.clone(),
1204 error,
1205 })?
1206 .next()
1207 .is_none()
1208 {
1209 if let Err(e) = std::fs::remove_dir(¤t_dir) {
1210 return Err(WorktreeRemoveError::RemoveError {
1211 path: current_dir,
1212 error: e,
1213 });
1214 }
1215 } else {
1216 break;
1217 }
1218 }
1219 }
1220
1221 self.0.prune_worktree(worktree_name)?;
1222 branch.delete()?;
1223
1224 Ok(())
1225 }
1226
1227 fn new_worktree(
1228 &self,
1229 name: &str,
1230 directory: &Path,
1231 target_branch: &Branch,
1232 ) -> Result<(), Error> {
1233 self.0.0.worktree(
1234 name,
1235 directory.as_std_path(),
1236 Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.as_reference()))),
1237 )?;
1238 Ok(())
1239 }
1240
1241 pub fn add_worktree(
1242 &self,
1243 name: &WorktreeName,
1244 tracking_selection: TrackingSelection,
1245 ) -> Result<Vec<Warning>, Error> {
1246 let mut warnings: Vec<Warning> = vec![];
1247
1248 let repo_directory = self.base_directory()?;
1249
1250 let remotes = self.as_repo().remotes()?;
1251
1252 let config: Option<WorktreeRootConfig> =
1253 config::read_worktree_root_config(repo_directory)?.map(Into::into);
1254
1255 if self.worktree_exists(name)? {
1256 return Err(Error::WorktreeAlreadyExists { name: name.clone() });
1257 }
1258
1259 let track_config = config.and_then(|config| config.track);
1260 let prefix = track_config
1261 .as_ref()
1262 .and_then(|track| track.default_remote_prefix.as_ref());
1263
1264 let default_tracking = track_config
1265 .as_ref()
1266 .map_or(TrackingDefault::NoTrack, |track| track.default);
1267
1268 let default_remote = track_config
1269 .as_ref()
1270 .map(|track| track.default_remote.clone());
1271
1272 let default_branch_head = self.as_repo().default_branch()?.commit_owned()?;
1284
1285 let worktree = NewWorktree::<Init>::new(self)
1286 .set_local_branch_name(&BranchName::new(name.as_str().to_owned()))?;
1287
1288 let get_remote_head = |remote_name: &RemoteName,
1289 remote_branch_name: &BranchName|
1290 -> Result<Option<repo::Commit>, Error> {
1291 Ok(self
1292 .as_repo()
1293 .find_remote_branch(remote_name, remote_branch_name)?
1294 .map(|branch| branch.commit_owned())
1295 .transpose()?)
1296 };
1297
1298 let worktree = if worktree.local_branch_already_exists() {
1299 worktree.select_commit(None)
1300 } else {
1301 if let TrackingSelection::Explicit {
1302 ref remote_name,
1303 ref remote_branch_name,
1304 } = tracking_selection
1305 {
1306 worktree.select_commit(Some(
1307 self.as_repo()
1308 .find_remote_branch(remote_name, remote_branch_name)?
1309 .map_or_else(
1310 || Ok(default_branch_head),
1311 |remote_branch| remote_branch.commit_owned(),
1312 )?,
1313 ))
1314 } else {
1315 match remotes.len() {
1316 0 => worktree.select_commit(Some(default_branch_head)),
1317 1 => {
1318 #[expect(clippy::indexing_slicing, reason = "checked for len() explicitly")]
1319 let remote_name = &remotes[0];
1320 let commit: Option<repo::Commit> = ({
1321 if let Some(prefix) = prefix {
1322 get_remote_head(
1323 remote_name,
1324 &BranchName::new(format!("{prefix}/{name}")),
1325 )?
1326 } else {
1327 None
1328 }
1329 })
1330 .or(get_remote_head(
1331 remote_name,
1332 &BranchName::new(name.as_str().to_owned()),
1333 )?)
1334 .or_else(|| Some(default_branch_head));
1335
1336 worktree.select_commit(commit)
1337 }
1338 _ => {
1339 let commit = if let Some(ref default_remote) = default_remote {
1340 if let Some(prefix) = prefix {
1341 self.as_repo()
1342 .find_remote_branch(default_remote, &BranchName::new(format!("{prefix}/{name}")))?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1343 } else {
1344 None
1345 }
1346 .or({
1347 self.as_repo().find_remote_branch(default_remote, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned() ).transpose()?
1348 })
1349 } else {
1350 None
1351 }.or({
1352 let mut commits = vec![];
1353 for remote_name in &remotes {
1354 let remote_head: Option<repo::Commit> = ({
1355 if let Some(prefix) = prefix {
1356 self.as_repo().find_remote_branch(
1357 remote_name,
1358 &BranchName::new(format!("{prefix}/{name}")),
1359 )?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1360 } else {
1361 None
1362 }
1363 })
1364 .or({
1365 self.as_repo().find_remote_branch(remote_name, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned()).transpose()?
1366 })
1367 .or(None);
1368 commits.push(remote_head);
1369 }
1370
1371 let mut commits = commits
1372 .into_iter()
1373 .flatten()
1374 .collect::<Vec<repo::Commit>>();
1377 if commits.is_empty() {
1381 Some(default_branch_head)
1382 } else if commits.len() == 1 {
1383 Some(commits.swap_remove(0))
1384 } else if commits.windows(2).any(
1385 #[expect(
1386 clippy::missing_asserts_for_indexing,
1387 clippy::indexing_slicing,
1388 reason = "windows function always returns two elements"
1389 )]
1390 |window| {
1391 let c1 = &window[0];
1392 let c2 = &window[1];
1393 (*c1).id().hex_string() != (*c2).id().hex_string()
1394 }) {
1395 warnings.push(
1396 Warning("Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_owned())
1403 );
1404 Some(default_branch_head)
1405 } else {
1406 Some(commits.swap_remove(0))
1407 }
1408 });
1409 worktree.select_commit(commit)
1410 }
1411 }
1412 }
1413 };
1414
1415 let worktree = worktree.set_remote_tracking_branch(match tracking_selection {
1416 TrackingSelection::Disabled => None,
1417 TrackingSelection::Explicit {
1418 remote_name,
1419 remote_branch_name,
1420 } => {
1421 Some(RemoteTrackingBranch {
1422 remote_name,
1423 remote_branch_name,
1424 prefix: None, })
1426 }
1427 TrackingSelection::Automatic => {
1428 if default_tracking == TrackingDefault::NoTrack {
1429 None
1430 } else {
1431 match remotes.len() {
1432 0 => None,
1433 1 =>
1434 {
1435 #[expect(
1436 clippy::indexing_slicing,
1437 reason = "checked for len() explicitly"
1438 )]
1439 Some(RemoteTrackingBranch {
1440 remote_name: remotes[0].clone(),
1441 remote_branch_name: BranchName::new(name.as_str().to_owned()),
1442 prefix: prefix.cloned(),
1443 })
1444 }
1445 _ => default_remote.map(|default_remote| RemoteTrackingBranch {
1446 remote_name: default_remote,
1447 remote_branch_name: BranchName::new(name.as_str().to_owned()),
1448 prefix: prefix.cloned(),
1449 }),
1450 }
1451 }
1452 }
1453 });
1454
1455 worktree.create(repo_directory)?;
1456
1457 Ok(warnings)
1458 }
1459}