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| remote.ok_or(Error::WorktreeNameNotUtf8))
1083 .collect::<Result<Vec<_>, Error>>()?
1084 .into_iter()
1085 .map(Worktree::new)
1086 .collect::<Result<Vec<_>, WorktreeValidationError>>()?)
1087 }
1088
1089 pub fn remove_worktree(
1090 &self,
1091 base_dir: &Path,
1092 worktree_name: &WorktreeName,
1093 worktree_dir: &Path,
1094 force: bool,
1095 worktree_config: Option<&WorktreeRootConfig>,
1096 default_branch: &Branch,
1097 ) -> Result<(), WorktreeRemoveError> {
1098 let fullpath = base_dir.join(worktree_dir);
1109
1110 if !fullpath.exists() {
1111 return Err(WorktreeRemoveError::DoesNotExist(fullpath));
1112 }
1113 let worktree_repo = RepoHandle::open(&fullpath)?;
1114
1115 let local_branch = worktree_repo.head_branch()?;
1116
1117 let branch_name = local_branch.name()?;
1118
1119 if branch_name.as_str() != worktree_name.as_str() {
1120 return Err(WorktreeRemoveError::BranchNameMismatch {
1121 worktree_name: worktree_name.clone(),
1122 branch_name,
1123 });
1124 }
1125
1126 let branch = worktree_repo
1127 .find_local_branch(&branch_name)?
1128 .ok_or_else(|| WorktreeRemoveError::BranchNotFound(branch_name.clone()))?;
1129
1130 if !force {
1131 let status = worktree_repo.status(WorktreeSetup::NoWorktree)?;
1132
1133 if let Some(changes) = status.changes {
1134 return Err(WorktreeRemoveError::Changes(changes));
1135 }
1136
1137 let is_merged_into_default_branch = {
1138 let (ahead_of_default_branch, _behind) =
1139 worktree_repo.graph_ahead_behind(&branch, default_branch)?;
1140
1141 ahead_of_default_branch == 0
1142 };
1143
1144 let mut is_merged_into_persistent_branch = false;
1145 let mut has_persistent_branches = false;
1146 if let Some(config) = worktree_config {
1147 if let Some(branches) = config.persistent_branches.as_ref() {
1148 has_persistent_branches = true;
1149 for persistent_branch in branches {
1150 let persistent_branch = worktree_repo
1151 .find_local_branch(persistent_branch)?
1152 .ok_or_else(|| {
1153 WorktreeRemoveError::BranchNotFound(branch_name.clone())
1154 })?;
1155
1156 let (ahead, _behind) =
1157 worktree_repo.graph_ahead_behind(&branch, &persistent_branch)?;
1158
1159 if ahead == 0 {
1160 is_merged_into_persistent_branch = true;
1161 }
1162 }
1163 }
1164 }
1165
1166 let merged_into_default_or_persistent_branches = is_merged_into_default_branch
1167 || (has_persistent_branches && is_merged_into_persistent_branch);
1168
1169 if !merged_into_default_or_persistent_branches {
1170 return Err(WorktreeRemoveError::NotMerged { branch_name });
1171 }
1172
1173 if let Some(remote_branch) = branch.upstream()? {
1174 let (ahead, behind) = worktree_repo.graph_ahead_behind(&branch, &remote_branch)?;
1175
1176 if (ahead, behind) != (0, 0) {
1177 return Err(WorktreeRemoveError::NotInSyncWithRemote { branch_name });
1178 }
1179 }
1180 }
1181
1182 if let Err(e) = std::fs::remove_dir_all(&fullpath) {
1187 return Err(WorktreeRemoveError::RemoveError {
1188 path: fullpath,
1189 error: e,
1190 });
1191 }
1192
1193 if let Some(current_dir) = worktree_dir.parent() {
1194 for current_dir in current_dir.ancestors() {
1195 let current_dir = base_dir.join(current_dir);
1196 if current_dir
1197 .read_dir()
1198 .map_err(|error| WorktreeRemoveError::ReadDirectoryError {
1199 path: current_dir.clone(),
1200 error,
1201 })?
1202 .next()
1203 .is_none()
1204 {
1205 if let Err(e) = std::fs::remove_dir(¤t_dir) {
1206 return Err(WorktreeRemoveError::RemoveError {
1207 path: current_dir,
1208 error: e,
1209 });
1210 }
1211 } else {
1212 break;
1213 }
1214 }
1215 }
1216
1217 self.0.prune_worktree(worktree_name)?;
1218 branch.delete()?;
1219
1220 Ok(())
1221 }
1222
1223 fn new_worktree(
1224 &self,
1225 name: &str,
1226 directory: &Path,
1227 target_branch: &Branch,
1228 ) -> Result<(), Error> {
1229 self.0.0.worktree(
1230 name,
1231 directory.as_std_path(),
1232 Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.as_reference()))),
1233 )?;
1234 Ok(())
1235 }
1236
1237 pub fn add_worktree(
1238 &self,
1239 name: &WorktreeName,
1240 tracking_selection: TrackingSelection,
1241 ) -> Result<Vec<Warning>, Error> {
1242 let mut warnings: Vec<Warning> = vec![];
1243
1244 let repo_directory = self.base_directory()?;
1245
1246 let remotes = self.as_repo().remotes()?;
1247
1248 let config: Option<WorktreeRootConfig> =
1249 config::read_worktree_root_config(repo_directory)?.map(Into::into);
1250
1251 if self.worktree_exists(name)? {
1252 return Err(Error::WorktreeAlreadyExists { name: name.clone() });
1253 }
1254
1255 let track_config = config.and_then(|config| config.track);
1256 let prefix = track_config
1257 .as_ref()
1258 .and_then(|track| track.default_remote_prefix.as_ref());
1259
1260 let default_tracking = track_config
1261 .as_ref()
1262 .map_or(TrackingDefault::NoTrack, |track| track.default);
1263
1264 let default_remote = track_config
1265 .as_ref()
1266 .map(|track| track.default_remote.clone());
1267
1268 let default_branch_head = self.as_repo().default_branch()?.commit_owned()?;
1280
1281 let worktree = NewWorktree::<Init>::new(self)
1282 .set_local_branch_name(&BranchName::new(name.as_str().to_owned()))?;
1283
1284 let get_remote_head = |remote_name: &RemoteName,
1285 remote_branch_name: &BranchName|
1286 -> Result<Option<repo::Commit>, Error> {
1287 Ok(self
1288 .as_repo()
1289 .find_remote_branch(remote_name, remote_branch_name)?
1290 .map(|branch| branch.commit_owned())
1291 .transpose()?)
1292 };
1293
1294 let worktree = if worktree.local_branch_already_exists() {
1295 worktree.select_commit(None)
1296 } else {
1297 if let TrackingSelection::Explicit {
1298 ref remote_name,
1299 ref remote_branch_name,
1300 } = tracking_selection
1301 {
1302 worktree.select_commit(Some(
1303 self.as_repo()
1304 .find_remote_branch(remote_name, remote_branch_name)?
1305 .map_or_else(
1306 || Ok(default_branch_head),
1307 |remote_branch| remote_branch.commit_owned(),
1308 )?,
1309 ))
1310 } else {
1311 match remotes.len() {
1312 0 => worktree.select_commit(Some(default_branch_head)),
1313 1 => {
1314 #[expect(clippy::indexing_slicing, reason = "checked for len() explicitly")]
1315 let remote_name = &remotes[0];
1316 let commit: Option<repo::Commit> = ({
1317 if let Some(prefix) = prefix {
1318 get_remote_head(
1319 remote_name,
1320 &BranchName::new(format!("{prefix}/{name}")),
1321 )?
1322 } else {
1323 None
1324 }
1325 })
1326 .or(get_remote_head(
1327 remote_name,
1328 &BranchName::new(name.as_str().to_owned()),
1329 )?)
1330 .or_else(|| Some(default_branch_head));
1331
1332 worktree.select_commit(commit)
1333 }
1334 _ => {
1335 let commit = if let Some(ref default_remote) = default_remote {
1336 if let Some(prefix) = prefix {
1337 self.as_repo()
1338 .find_remote_branch(default_remote, &BranchName::new(format!("{prefix}/{name}")))?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1339 } else {
1340 None
1341 }
1342 .or({
1343 self.as_repo().find_remote_branch(default_remote, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned() ).transpose()?
1344 })
1345 } else {
1346 None
1347 }.or({
1348 let mut commits = vec![];
1349 for remote_name in &remotes {
1350 let remote_head: Option<repo::Commit> = ({
1351 if let Some(prefix) = prefix {
1352 self.as_repo().find_remote_branch(
1353 remote_name,
1354 &BranchName::new(format!("{prefix}/{name}")),
1355 )?.map(|remote_branch| remote_branch.commit_owned()).transpose()?
1356 } else {
1357 None
1358 }
1359 })
1360 .or({
1361 self.as_repo().find_remote_branch(remote_name, &BranchName::new(name.as_str().to_owned()))?.map(|remote_branch|remote_branch.commit_owned()).transpose()?
1362 })
1363 .or(None);
1364 commits.push(remote_head);
1365 }
1366
1367 let mut commits = commits
1368 .into_iter()
1369 .flatten()
1370 .collect::<Vec<repo::Commit>>();
1373 if commits.is_empty() {
1377 Some(default_branch_head)
1378 } else if commits.len() == 1 {
1379 Some(commits.swap_remove(0))
1380 } else if commits.windows(2).any(
1381 #[expect(
1382 clippy::missing_asserts_for_indexing,
1383 clippy::indexing_slicing,
1384 reason = "windows function always returns two elements"
1385 )]
1386 |window| {
1387 let c1 = &window[0];
1388 let c2 = &window[1];
1389 (*c1).id().hex_string() != (*c2).id().hex_string()
1390 }) {
1391 warnings.push(
1392 Warning("Branch exists on multiple remotes, but they deviate. Selecting default branch instead".to_owned())
1399 );
1400 Some(default_branch_head)
1401 } else {
1402 Some(commits.swap_remove(0))
1403 }
1404 });
1405 worktree.select_commit(commit)
1406 }
1407 }
1408 }
1409 };
1410
1411 let worktree = worktree.set_remote_tracking_branch(match tracking_selection {
1412 TrackingSelection::Disabled => None,
1413 TrackingSelection::Explicit {
1414 remote_name,
1415 remote_branch_name,
1416 } => {
1417 Some(RemoteTrackingBranch {
1418 remote_name,
1419 remote_branch_name,
1420 prefix: None, })
1422 }
1423 TrackingSelection::Automatic => {
1424 if default_tracking == TrackingDefault::NoTrack {
1425 None
1426 } else {
1427 match remotes.len() {
1428 0 => None,
1429 1 =>
1430 {
1431 #[expect(
1432 clippy::indexing_slicing,
1433 reason = "checked for len() explicitly"
1434 )]
1435 Some(RemoteTrackingBranch {
1436 remote_name: remotes[0].clone(),
1437 remote_branch_name: BranchName::new(name.as_str().to_owned()),
1438 prefix: prefix.cloned(),
1439 })
1440 }
1441 _ => default_remote.map(|default_remote| RemoteTrackingBranch {
1442 remote_name: default_remote,
1443 remote_branch_name: BranchName::new(name.as_str().to_owned()),
1444 prefix: prefix.cloned(),
1445 }),
1446 }
1447 }
1448 }
1449 });
1450
1451 worktree.create(repo_directory)?;
1452
1453 Ok(warnings)
1454 }
1455}