1use std::path::Path;
9
10use anyhow::{Context, Result, bail};
11
12use crate::output::{
13 BranchDeleteOutput, CommitOutput, MergeOutput, WorktreeAddOutput, WorktreeRemoveOutput,
14};
15use crate::{GitModule, git_cmd};
16
17impl GitModule {
18 pub fn worktree_add(
20 &mut self,
21 name: &str,
22 branch: &str,
23 base_branch: Option<&str>,
24 ) -> Result<WorktreeAddOutput> {
25 let wt_dir = self.worktrees_dir();
26 std::fs::create_dir_all(&wt_dir).context("failed to create .worktrees directory")?;
27
28 let wt_path = wt_dir.join(name);
29 if wt_path.exists() {
30 bail!("worktree already exists: {}", wt_path.display());
31 }
32
33 let path_str = wt_path.to_str().unwrap_or(name);
34 if let Some(base) = base_branch {
35 git_cmd(
36 self.session().root(),
37 &["worktree", "add", "-b", branch, path_str, base],
38 )?;
39 } else {
40 git_cmd(
41 self.session().root(),
42 &["worktree", "add", "-b", branch, path_str],
43 )?;
44 }
45
46 let canon = wt_path.canonicalize().unwrap_or_else(|_| wt_path.clone());
47 self.register_worktree(canon);
48 self.register_branch(branch.to_string());
49
50 Ok(WorktreeAddOutput {
51 path: wt_path,
52 branch: branch.to_string(),
53 session: self.session().id().to_string(),
54 })
55 }
56
57 pub fn worktree_remove(&mut self, name: &str) -> Result<WorktreeRemoveOutput> {
59 let wt_path = self.worktrees_dir().join(name);
60 let canon = wt_path.canonicalize().unwrap_or_else(|_| wt_path.clone());
61
62 self.ensure_owned(&canon)
63 .or_else(|_| self.ensure_owned(&wt_path))?;
64
65 git_cmd(
66 self.session().root(),
67 &[
68 "worktree",
69 "remove",
70 "--force",
71 wt_path.to_str().unwrap_or(name),
72 ],
73 )?;
74
75 self.forget_worktree(&canon);
76 self.forget_worktree(&wt_path);
77
78 Ok(WorktreeRemoveOutput { path: wt_path })
79 }
80
81 pub fn commit(
86 &self,
87 working_dir: &Path,
88 message: &str,
89 paths: Option<&[String]>,
90 ) -> Result<CommitOutput> {
91 self.ensure_session_scope(working_dir)?;
92
93 match paths {
94 Some(ps) if !ps.is_empty() => {
95 let mut args = vec!["add", "--"];
96 let owned: Vec<&str> = ps.iter().map(|s| s.as_str()).collect();
97 args.extend(owned);
98 git_cmd(working_dir, &args)?;
99 }
100 _ => {
101 git_cmd(working_dir, &["add", "-A"])?;
102 }
103 }
104
105 git_cmd(working_dir, &["commit", "-m", message])?;
106
107 let sha = git_cmd(working_dir, &["rev-parse", "HEAD"])?;
108 let short_sha = sha[..7.min(sha.len())].to_string();
109 let files_changed = git_cmd(working_dir, &["diff", "--name-only", "HEAD~1..HEAD"])
110 .map(|out| out.lines().filter(|l| !l.is_empty()).count())
111 .unwrap_or_else(|e| {
112 tracing::warn!(error = %e, "git diff HEAD~1..HEAD failed (initial commit?)");
113 0
114 });
115
116 Ok(CommitOutput {
117 sha,
118 short_sha,
119 message: message.to_string(),
120 files_changed,
121 })
122 }
123
124 pub fn merge(
127 &self,
128 branch: &str,
129 into_branch: &str,
130 working_dir: &Path,
131 ) -> Result<MergeOutput> {
132 self.ensure_session_scope(working_dir)?;
133
134 let current = git_cmd(working_dir, &["branch", "--show-current"])?;
135 if current != into_branch {
136 git_cmd(working_dir, &["checkout", into_branch])?;
137 }
138
139 let merge_message = format!("Merge branch '{}' into {}", branch, into_branch);
140 match git_cmd(
141 working_dir,
142 &["merge", "--no-ff", branch, "-m", &merge_message],
143 ) {
144 Ok(raw) => {
145 let sha = git_cmd(working_dir, &["rev-parse", "HEAD"])?;
146 let short_sha = sha[..7.min(sha.len())].to_string();
147 Ok(MergeOutput {
148 branch: branch.to_string(),
149 into_branch: into_branch.to_string(),
150 sha,
151 short_sha,
152 raw,
153 })
154 }
155 Err(e) => {
156 let _ = git_cmd(working_dir, &["merge", "--abort"]);
157 bail!("merge failed (aborted): {e}");
158 }
159 }
160 }
161
162 pub fn branch_delete(&self, branch: &str) -> Result<BranchDeleteOutput> {
165 self.ensure_branch_owned(branch)?;
166 git_cmd(self.session().root(), &["branch", "-d", branch])?;
167 Ok(BranchDeleteOutput {
168 branch: branch.to_string(),
169 })
170 }
171}