1use std::collections::BTreeSet;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use crate::error::PawError;
11use crate::specs::SpecEntry;
12
13pub fn validate_repo(path: &Path) -> Result<PathBuf, PawError> {
17 let output = Command::new("git")
18 .current_dir(path)
19 .args(["rev-parse", "--show-toplevel"])
20 .output()
21 .map_err(|e| PawError::BranchError(format!("failed to run git: {e}")))?;
22
23 if !output.status.success() {
24 return Err(PawError::NotAGitRepo);
25 }
26
27 let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
28 Ok(PathBuf::from(root))
29}
30
31pub fn list_branches(repo_root: &Path) -> Result<Vec<String>, PawError> {
38 let output = Command::new("git")
39 .current_dir(repo_root)
40 .args(["branch", "-a", "--format=%(refname:short)"])
41 .output()
42 .map_err(|e| PawError::BranchError(format!("failed to run git branch: {e}")))?;
43
44 if !output.status.success() {
45 let stderr = String::from_utf8_lossy(&output.stderr);
46 return Err(PawError::BranchError(format!(
47 "git branch failed: {stderr}"
48 )));
49 }
50
51 let stdout = String::from_utf8_lossy(&output.stdout);
52 let branches: BTreeSet<String> = stdout
53 .lines()
54 .filter(|line| !line.trim().is_empty() && !line.contains("HEAD"))
55 .map(|line| {
56 let mut branch_name = line.trim().to_string();
58
59 if let Some(stripped) = branch_name.strip_prefix("refs/remotes/") {
61 branch_name = stripped.to_string();
62 }
63 if let Some(stripped) = branch_name.strip_prefix("origin/") {
65 branch_name = stripped.to_string();
66 }
67
68 branch_name
69 })
70 .collect();
71
72 let mut unique: Vec<String> = branches.into_iter().collect();
74 unique.sort();
75 Ok(unique)
76}
77
78pub fn worktree_dir_name(project: &str, branch: &str) -> String {
82 let project_safe: String = project
83 .chars()
84 .map(|c| if c.is_alphanumeric() { c } else { '-' })
85 .collect();
86 let branch_safe: String = branch
87 .chars()
88 .map(|c| if c.is_alphanumeric() { c } else { '-' })
89 .collect();
90 format!("{project_safe}-{branch_safe}")
91}
92
93pub fn default_branch(repo_root: &Path) -> Result<String, PawError> {
95 let output = Command::new("git")
96 .current_dir(repo_root)
97 .args(["symbolic-ref", "refs/remotes/origin/HEAD"])
98 .output()
99 .map_err(|e| PawError::BranchError(format!("failed to run git symbolic-ref: {e}")))?;
100
101 if !output.status.success() {
102 let stderr = String::from_utf8_lossy(&output.stderr);
103 return Err(PawError::BranchError(format!(
104 "git symbolic-ref failed: {stderr}"
105 )));
106 }
107
108 let ref_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
109 if let Some(branch) = ref_name.strip_prefix("refs/remotes/origin/") {
110 Ok(branch.to_string())
111 } else {
112 Err(PawError::BranchError(format!(
113 "unexpected ref format: {ref_name}"
114 )))
115 }
116}
117
118pub fn current_branch(repo_root: &Path) -> Result<String, PawError> {
120 let output = Command::new("git")
121 .current_dir(repo_root)
122 .args(["branch", "--show-current"])
123 .output()
124 .map_err(|e| PawError::BranchError(format!("failed to run git branch: {e}")))?;
125
126 if !output.status.success() {
127 let stderr = String::from_utf8_lossy(&output.stderr);
128 return Err(PawError::BranchError(format!(
129 "git branch failed: {stderr}"
130 )));
131 }
132
133 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
134 if branch.is_empty() {
135 return Err(PawError::BranchError(
136 "not on any branch (detached HEAD)".to_string(),
137 ));
138 }
139 Ok(branch)
140}
141
142pub fn project_name(repo_root: &Path) -> String {
144 repo_root
145 .file_name()
146 .and_then(std::ffi::OsStr::to_str)
147 .unwrap_or("unknown")
148 .to_string()
149}
150
151#[derive(Debug)]
153pub struct WorktreeCreation {
154 pub path: PathBuf,
156 pub branch_created: bool,
158}
159
160fn find_worktree_for_branch(repo_root: &Path, branch: &str) -> Result<Option<PathBuf>, PawError> {
163 let list = Command::new("git")
164 .current_dir(repo_root)
165 .args(["worktree", "list", "--porcelain"])
166 .output()
167 .map_err(|e| PawError::WorktreeError(format!("failed to run git worktree list: {e}")))?;
168 if !list.status.success() {
169 return Ok(None);
170 }
171 let listing = String::from_utf8_lossy(&list.stdout);
172 let expected_branch_ref = format!("refs/heads/{branch}");
173 let mut current_path: Option<PathBuf> = None;
174 for line in listing.lines() {
175 if let Some(rest) = line.strip_prefix("worktree ") {
176 current_path = Some(PathBuf::from(rest));
177 } else if let Some(rest) = line.strip_prefix("branch ")
178 && rest == expected_branch_ref
179 && let Some(p) = current_path.take()
180 {
181 return Ok(Some(p));
182 }
183 }
184 Ok(None)
185}
186
187fn rebase_branch_onto_default(repo_root: &Path, branch: &str) -> Result<(), PawError> {
198 let default = default_branch(repo_root)?;
199
200 let occupied_at = find_worktree_for_branch(repo_root, branch)?;
201 let (workdir, original_head): (PathBuf, Option<String>) = if let Some(wt) = occupied_at {
202 (wt, None)
203 } else {
204 let original = Command::new("git")
205 .current_dir(repo_root)
206 .args(["symbolic-ref", "--short", "HEAD"])
207 .output()
208 .ok()
209 .filter(|o| o.status.success())
210 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
211 (repo_root.to_path_buf(), original)
212 };
213
214 let mut invocation = Command::new("git");
215 invocation.current_dir(&workdir);
216 if original_head.is_some() {
217 invocation.args(["rebase", &default, branch]);
218 } else {
219 invocation.args(["rebase", &default]);
220 }
221 let output = invocation
222 .output()
223 .map_err(|e| PawError::WorktreeError(format!("failed to run git rebase: {e}")))?;
224
225 if !output.status.success() {
226 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
227 let _ = Command::new("git")
228 .current_dir(&workdir)
229 .args(["rebase", "--abort"])
230 .output();
231 if let Some(orig) = &original_head
232 && orig != branch
233 {
234 let _ = Command::new("git")
235 .current_dir(repo_root)
236 .args(["checkout", orig])
237 .output();
238 }
239 return Err(PawError::WorktreeError(format!(
240 "rebase onto main failed: {stderr}"
241 )));
242 }
243
244 if let Some(orig) = original_head
245 && orig != branch
246 {
247 let _ = Command::new("git")
248 .current_dir(repo_root)
249 .args(["checkout", &orig])
250 .output();
251 }
252
253 Ok(())
254}
255
256pub fn create_worktree(
272 repo_root: &Path,
273 branch: &str,
274 rebase_onto_main: bool,
275) -> Result<WorktreeCreation, PawError> {
276 let project = project_name(repo_root);
277 let dir_name = worktree_dir_name(&project, branch);
278
279 let parent = repo_root.parent().ok_or_else(|| {
280 PawError::WorktreeError("cannot determine parent directory of repo".to_string())
281 })?;
282 let worktree_path = parent.join(&dir_name);
283
284 if rebase_onto_main {
292 let branch_exists = Command::new("git")
293 .current_dir(repo_root)
294 .args(["rev-parse", "--verify", &format!("refs/heads/{branch}")])
295 .output()
296 .is_ok_and(|o| o.status.success());
297 if branch_exists {
298 rebase_branch_onto_default(repo_root, branch)?;
299 }
300 }
301
302 if worktree_path.exists() {
308 let expected_canonical = std::fs::canonicalize(&worktree_path).ok();
312 let list = Command::new("git")
313 .current_dir(repo_root)
314 .args(["worktree", "list", "--porcelain"])
315 .output()
316 .map_err(|e| {
317 PawError::WorktreeError(format!("failed to run git worktree list: {e}"))
318 })?;
319 if list.status.success() {
320 let listing = String::from_utf8_lossy(&list.stdout);
321 let expected_branch_ref = format!("refs/heads/{branch}");
322 let mut current_path: Option<PathBuf> = None;
325 for line in listing.lines() {
326 if let Some(rest) = line.strip_prefix("worktree ") {
327 current_path = std::fs::canonicalize(PathBuf::from(rest)).ok();
328 } else if let Some(rest) = line.strip_prefix("branch ") {
329 let path_matches = match (¤t_path, &expected_canonical) {
330 (Some(p), Some(e)) => p == e,
331 _ => false,
332 };
333 if path_matches && rest == expected_branch_ref {
334 return Ok(WorktreeCreation {
335 path: worktree_path,
336 branch_created: false,
337 });
338 }
339 }
340 }
341 }
342 }
346
347 let output = Command::new("git")
349 .current_dir(repo_root)
350 .args(["worktree", "add", &worktree_path.to_string_lossy(), branch])
351 .output()
352 .map_err(|e| PawError::WorktreeError(format!("failed to run git worktree add: {e}")))?;
353
354 if output.status.success() {
355 return Ok(WorktreeCreation {
356 path: worktree_path,
357 branch_created: false,
358 });
359 }
360
361 let stderr = String::from_utf8_lossy(&output.stderr);
362
363 if stderr.contains("invalid reference") {
365 let output = Command::new("git")
366 .current_dir(repo_root)
367 .args([
368 "worktree",
369 "add",
370 "-b",
371 branch,
372 &worktree_path.to_string_lossy(),
373 ])
374 .output()
375 .map_err(|e| {
376 PawError::WorktreeError(format!("failed to run git worktree add -b: {e}"))
377 })?;
378
379 if output.status.success() {
380 return Ok(WorktreeCreation {
381 path: worktree_path,
382 branch_created: true,
383 });
384 }
385
386 let stderr = String::from_utf8_lossy(&output.stderr);
387 return Err(PawError::WorktreeError(format!(
388 "git worktree add -b failed for branch '{branch}': {stderr}"
389 )));
390 }
391
392 Err(PawError::WorktreeError(format!(
393 "git worktree add failed for branch '{branch}': {stderr}"
394 )))
395}
396
397pub fn remove_worktree(repo_root: &Path, worktree_path: &Path) -> Result<(), PawError> {
401 let output = Command::new("git")
408 .current_dir(repo_root)
409 .args(["worktree", "remove", "--force"])
410 .arg(worktree_path.as_os_str())
411 .output()
412 .map_err(|e| {
413 PawError::WorktreeError(format!(
414 "failed to remove worktree at {}: {e}",
415 worktree_path.display()
416 ))
417 })?;
418
419 if !output.status.success() {
420 let stderr = String::from_utf8_lossy(&output.stderr);
421 return Err(PawError::WorktreeError(format!(
422 "git worktree remove failed for worktree at {}: {stderr}",
423 worktree_path.display()
424 )));
425 }
426
427 Ok(())
428}
429
430pub fn prune_worktrees(repo_root: &Path) -> Result<(), PawError> {
434 let output = Command::new("git")
435 .current_dir(repo_root)
436 .args(["worktree", "prune"])
437 .output()
438 .map_err(|e| PawError::WorktreeError(format!("failed to prune worktrees: {e}")))?;
439
440 if !output.status.success() {
441 let stderr = String::from_utf8_lossy(&output.stderr);
442 return Err(PawError::WorktreeError(format!(
443 "git worktree prune failed: {stderr}"
444 )));
445 }
446
447 Ok(())
448}
449
450pub fn check_uncommitted_specs(
461 repo_root: &Path,
462 specs: &[SpecEntry],
463) -> Result<Vec<String>, PawError> {
464 let mut uncommitted_specs = Vec::new();
465
466 let specs_dir = repo_root.join("specs");
467
468 for spec in specs {
469 let dir_path = specs_dir.join(&spec.id);
470 let file_path = specs_dir.join(format!("{}.md", spec.id));
471
472 let porcelain_target = if dir_path.is_dir() {
473 format!("specs/{}", spec.id)
474 } else if file_path.is_file() {
475 format!("specs/{}.md", spec.id)
476 } else {
477 continue;
478 };
479
480 let output = Command::new("git")
481 .current_dir(repo_root)
482 .args(["status", "--porcelain", "--", &porcelain_target])
483 .output()
484 .map_err(|e| {
485 PawError::BranchError(format!(
486 "failed to run git status for spec {}: {e}",
487 spec.id
488 ))
489 })?;
490
491 if !output.status.success() {
492 let stderr = String::from_utf8_lossy(&output.stderr);
493 return Err(PawError::BranchError(format!(
494 "git status failed for spec {}: {stderr}",
495 spec.id
496 )));
497 }
498
499 let status_output = String::from_utf8_lossy(&output.stdout).trim().to_string();
500 if !status_output.is_empty() {
501 uncommitted_specs.push(spec.id.clone());
502 }
503 }
504
505 Ok(uncommitted_specs)
506}
507
508pub fn uncommitted_files(worktree_root: &Path) -> Result<Vec<String>, PawError> {
516 let output = Command::new("git")
517 .current_dir(worktree_root)
518 .args(["status", "--porcelain"])
519 .output()
520 .map_err(|e| {
521 PawError::WorktreeError(format!(
522 "failed to run git status in {}: {e}",
523 worktree_root.display()
524 ))
525 })?;
526
527 if !output.status.success() {
528 let stderr = String::from_utf8_lossy(&output.stderr);
529 return Err(PawError::WorktreeError(format!(
530 "git status failed in {}: {stderr}",
531 worktree_root.display()
532 )));
533 }
534
535 let stdout = String::from_utf8_lossy(&output.stdout);
536 let mut files = Vec::new();
537 for line in stdout.lines() {
538 if line.len() <= 3 {
539 continue;
540 }
541 let path = &line[3..];
545 let reported = path.rsplit(" -> ").next().unwrap_or(path);
546 files.push(reported.trim().to_string());
547 }
548 Ok(files)
549}
550
551pub fn merge_branch(repo_root: &Path, branch: &str) -> Result<bool, PawError> {
555 let output = Command::new("git")
556 .current_dir(repo_root)
557 .args(["merge", "--no-ff", "--no-commit", branch])
558 .output()
559 .map_err(|e| {
560 PawError::WorktreeError(format!("failed to run git merge for branch {branch}: {e}"))
561 })?;
562
563 if !output.status.success() {
564 let stderr = String::from_utf8_lossy(&output.stderr);
565 if output.status.code() == Some(1) {
567 return Ok(false);
568 }
569 return Err(PawError::WorktreeError(format!(
570 "git merge failed for branch {branch}: {stderr}"
571 )));
572 }
573
574 Ok(true)
575}
576
577pub fn delete_branch(repo_root: &Path, branch: &str) -> Result<(), PawError> {
579 let output = Command::new("git")
580 .current_dir(repo_root)
581 .args(["branch", "-D", branch])
582 .output()
583 .map_err(|e| PawError::BranchError(format!("failed to delete branch {branch}: {e}")))?;
584
585 if !output.status.success() {
586 let stderr = String::from_utf8_lossy(&output.stderr);
587 return Err(PawError::BranchError(format!(
588 "git branch -D failed for branch {branch}: {stderr}"
589 )));
590 }
591
592 Ok(())
593}
594
595pub fn exclude_from_git(worktree_root: &Path, filename: &str) -> Result<(), PawError> {
601 let exclude_file = worktree_root.join(".git/info/exclude");
602
603 let existing = if exclude_file.exists() {
605 std::fs::read_to_string(&exclude_file).unwrap_or_default()
606 } else {
607 String::new()
608 };
609
610 if !existing.lines().any(|line| line.trim() == filename) {
612 let mut updated = existing;
613 if !updated.ends_with('\n') && !updated.is_empty() {
614 updated.push('\n');
615 }
616 updated.push_str(filename);
617 updated.push('\n');
618
619 if let Some(parent) = exclude_file.parent() {
621 if let Some(git_dir) = parent.parent()
623 && git_dir.is_file()
624 {
625 let main_git_dir = std::fs::read_to_string(git_dir)
628 .ok()
629 .and_then(|s| s.strip_prefix("gitdir: ").map(|s| s.trim().to_owned()))
630 .unwrap_or_default();
631 let main_git_info = PathBuf::from(main_git_dir).join("info");
632 if !main_git_info.try_exists().unwrap_or(false) {
633 std::fs::create_dir_all(&main_git_info).map_err(|e| {
634 PawError::SessionError(format!("failed to create main .git/info: {e}"))
635 })?;
636 }
637 let main_exclude = main_git_info.join("exclude");
638 std::fs::write(&main_exclude, updated).map_err(|e| {
639 PawError::SessionError(format!(
640 "failed to write to main .git/info/exclude: {e}"
641 ))
642 })?;
643 return Ok(());
644 }
645 if parent.exists() && parent.is_file() {
646 std::fs::remove_file(parent).map_err(|e| {
647 PawError::SessionError(format!("failed to remove .git/info file: {e}"))
648 })?;
649 }
650 std::fs::create_dir_all(parent).map_err(|e| {
651 PawError::SessionError(format!("failed to create .git/info directory: {e}"))
652 })?;
653 }
654
655 std::fs::write(&exclude_file, updated).map_err(|e| {
656 PawError::SessionError(format!("failed to write to .git/info/exclude: {e}"))
657 })?;
658 }
659
660 Ok(())
661}
662
663pub fn assume_unchanged(worktree_root: &Path, filename: &str) -> Result<(), PawError> {
669 let _ = std::process::Command::new("git")
675 .current_dir(worktree_root)
676 .args(["update-index", "--assume-unchanged", filename])
677 .output();
678 Ok(())
679}
680
681#[cfg(test)]
682mod tests {
683 use std::path::{Path, PathBuf};
684 use std::process::Command;
685
686 use tempfile::TempDir;
687
688 use crate::error::PawError;
689 use crate::git::{WorktreeCreation, create_worktree};
690
691 struct RebaseRepo {
696 _sandbox: TempDir,
697 repo: PathBuf,
698 }
699
700 impl RebaseRepo {
701 fn path(&self) -> &Path {
702 &self.repo
703 }
704 }
705
706 fn run_git(dir: &Path, args: &[&str]) {
707 let output = Command::new("git")
708 .current_dir(dir)
709 .args(args)
710 .output()
711 .expect("run git command");
712 assert!(
713 output.status.success(),
714 "git {} failed: {}",
715 args.join(" "),
716 String::from_utf8_lossy(&output.stderr)
717 );
718 }
719
720 fn capture_git(dir: &Path, args: &[&str]) -> String {
721 let output = Command::new("git")
722 .current_dir(dir)
723 .args(args)
724 .output()
725 .expect("run git command");
726 assert!(
727 output.status.success(),
728 "git {} failed: {}",
729 args.join(" "),
730 String::from_utf8_lossy(&output.stderr)
731 );
732 String::from_utf8_lossy(&output.stdout).trim().to_string()
733 }
734
735 fn setup_rebase_repo() -> RebaseRepo {
739 let sandbox = TempDir::new().expect("tempdir");
740 let bare = sandbox.path().join("bare.git");
741 let repo = sandbox.path().join("repo");
742 std::fs::create_dir_all(&bare).unwrap();
743
744 run_git(&bare, &["init", "--bare", "-b", "main"]);
745
746 let status = Command::new("git")
748 .args([
749 "clone",
750 bare.to_str().unwrap(),
751 repo.to_str().unwrap(),
752 "--origin",
753 "origin",
754 ])
755 .status()
756 .expect("git clone");
757 assert!(status.success());
758
759 run_git(&repo, &["config", "user.email", "test@test.com"]);
760 run_git(&repo, &["config", "user.name", "Test"]);
761 run_git(&repo, &["checkout", "-b", "main"]);
762 std::fs::write(repo.join("a.txt"), "one\n").unwrap();
763 run_git(&repo, &["add", "."]);
764 run_git(&repo, &["commit", "-m", "init"]);
765 run_git(&repo, &["push", "-u", "origin", "main"]);
766 run_git(&bare, &["symbolic-ref", "HEAD", "refs/heads/main"]);
767 run_git(&repo, &["remote", "set-head", "origin", "main"]);
768 run_git(&repo, &["branch", "feat/example"]);
769
770 RebaseRepo {
771 _sandbox: sandbox,
772 repo,
773 }
774 }
775
776 fn advance_main(repo: &Path, commits: usize) {
777 for i in 0..commits {
778 std::fs::write(repo.join(format!("main-{i}.txt")), format!("v{i}\n")).unwrap();
779 run_git(repo, &["add", "."]);
780 run_git(repo, &["commit", "-m", &format!("main commit {i}")]);
781 }
782 }
783
784 fn head_sha(repo: &Path, branch: &str) -> String {
785 capture_git(repo, &["rev-parse", branch])
786 }
787
788 #[test]
789 fn create_worktree_rebases_branch_when_behind_main() {
790 let r = setup_rebase_repo();
791 advance_main(r.path(), 2);
792
793 let result = create_worktree(r.path(), "feat/example", true).expect("rebase succeeds");
794 assert!(
795 matches!(
796 result,
797 WorktreeCreation {
798 branch_created: false,
799 ..
800 }
801 ),
802 "branch existed, branch_created must be false"
803 );
804 assert!(result.path.exists(), "worktree directory must be created");
805
806 let count = capture_git(r.path(), &["rev-list", "--count", "feat/example..main"]);
808 assert_eq!(count, "0", "feat/example must include main's commits");
809 }
810
811 #[test]
812 fn create_worktree_rebase_noop_when_branch_up_to_date() {
813 let r = setup_rebase_repo();
814 let before = head_sha(r.path(), "feat/example");
816 let _result =
817 create_worktree(r.path(), "feat/example", true).expect("noop rebase succeeds");
818 let after = head_sha(r.path(), "feat/example");
819 assert_eq!(before, after, "noop rebase must not change HEAD");
820 }
821
822 #[test]
823 fn create_worktree_rebase_conflict_aborts_and_errors() {
824 let r = setup_rebase_repo();
825
826 run_git(r.path(), &["checkout", "feat/example"]);
829 std::fs::write(r.path().join("a.txt"), "feat-version\n").unwrap();
830 run_git(r.path(), &["add", "."]);
831 run_git(r.path(), &["commit", "-m", "feat edit"]);
832 run_git(r.path(), &["checkout", "main"]);
833 std::fs::write(r.path().join("a.txt"), "main-version\n").unwrap();
834 run_git(r.path(), &["add", "."]);
835 run_git(r.path(), &["commit", "-m", "main edit"]);
836
837 let pre = head_sha(r.path(), "feat/example");
838 let result = create_worktree(r.path(), "feat/example", true);
839 let err = result.expect_err("rebase must error on conflict");
840 match err {
841 PawError::WorktreeError(msg) => assert!(
842 msg.contains("rebase onto main failed"),
843 "expected 'rebase onto main failed' in error, got: {msg}"
844 ),
845 other => panic!("expected WorktreeError, got {other:?}"),
846 }
847
848 let post = head_sha(r.path(), "feat/example");
849 assert_eq!(pre, post, "branch HEAD must be restored after abort");
850
851 let git_dir = r.path().join(".git");
852 assert!(
853 !git_dir.join("rebase-merge").exists(),
854 "rebase-merge dir must not survive abort"
855 );
856 assert!(
857 !git_dir.join("rebase-apply").exists(),
858 "rebase-apply dir must not survive abort"
859 );
860 }
861
862 #[test]
863 fn create_worktree_no_rebase_preserves_v0_5_behaviour() {
864 let r = setup_rebase_repo();
865 advance_main(r.path(), 2);
866
867 let before = head_sha(r.path(), "feat/example");
868 let result =
869 create_worktree(r.path(), "feat/example", false).expect("no-rebase path succeeds");
870 let after = head_sha(r.path(), "feat/example");
871 assert_eq!(before, after, "rebase_onto_main=false must not change HEAD");
872 assert!(result.path.exists(), "worktree directory must be created");
873 }
874
875 #[test]
876 fn create_worktree_new_branch_skips_rebase_regardless_of_flag() {
877 let r = setup_rebase_repo();
878 let result =
880 create_worktree(r.path(), "feat/new", true).expect("new-branch creation succeeds");
881 assert!(
882 matches!(
883 result,
884 WorktreeCreation {
885 branch_created: true,
886 ..
887 }
888 ),
889 "new branch must report branch_created=true"
890 );
891 assert!(result.path.exists(), "worktree directory must be created");
892 }
893
894 #[cfg(unix)]
895 #[test]
896 fn remove_worktree_does_not_panic_on_non_utf8_path() {
897 use std::ffi::OsString;
904 use std::os::unix::ffi::OsStringExt;
905 use std::path::PathBuf;
906
907 use super::remove_worktree;
908
909 let repo = tempfile::tempdir().expect("tempdir");
910
911 let non_utf8 = OsString::from_vec(vec![b'f', 0x80, b'f']);
913 let worktree_path = PathBuf::from(non_utf8);
914
915 let result = remove_worktree(repo.path(), &worktree_path);
919 assert!(result.is_err(), "expected Err for non-existent worktree");
920 }
921}