1use std::io;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13use crate::diffalgo::{DiffAlgorithm, DiffSettings};
14use crate::git::GitVcs;
15use crate::model::{DiffModel, HunkId};
16use crate::vcs::{
17 BlameSpan, BranchInfo, HeadInfo, LogEntry, NetworkOp, StatusModel, Vcs, VcsError,
18};
19
20pub struct JjVcs {
21 git: GitVcs,
22 root: PathBuf,
23}
24
25impl JjVcs {
26 pub fn open(root: &Path) -> Result<Self, VcsError> {
28 Self::open_with_settings(root, &DiffSettings::default())
29 }
30
31 pub fn open_with_settings(root: &Path, settings: &DiffSettings) -> Result<Self, VcsError> {
32 Ok(Self {
33 git: GitVcs::open_with_settings(root, settings)?,
34 root: root.to_path_buf(),
35 })
36 }
37
38 fn run(&self, args: &[&str]) -> Result<String, VcsError> {
43 let output = Command::new("jj")
44 .current_dir(&self.root)
45 .args(args)
46 .env("JJ_EDITOR", "false")
49 .output()
50 .map_err(|err| match err.kind() {
51 io::ErrorKind::NotFound => {
52 VcsError::Rejected("install jj (jujutsu) on PATH to change this repo".into())
53 }
54 _ => VcsError::Io(err),
55 })?;
56 if output.status.success() {
57 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned());
58 }
59 let stderr = String::from_utf8_lossy(&output.stderr);
60 let mut lines = stderr.lines().map(str::trim);
61 let message = lines
62 .clone()
63 .find(|line| line.starts_with("Error:"))
64 .or_else(|| lines.find(|line| !line.is_empty()))
65 .unwrap_or("jj command failed")
66 .to_owned();
67 Err(VcsError::Rejected(message))
68 }
69
70 fn head_oid(&self) -> Result<String, VcsError> {
73 self.git.resolve("HEAD")
74 }
75}
76
77fn quoted(s: &str) -> String {
81 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
82}
83
84const NO_STAGING: &str = "commit the whole working copy; jj has no staging area";
85const NO_STASH: &str = "run jj new in a shell to set this change aside; jj has no stash";
86const NO_PUSH_PULL: &str = "run jj git push or jj git fetch in a shell to sync a jj repo";
87
88impl Vcs for JjVcs {
89 fn has_index(&self) -> bool {
90 false
91 }
92
93 fn native_git_checkout(&self) -> bool {
94 false
95 }
96
97 fn git_dir(&self) -> Result<PathBuf, VcsError> {
98 self.git.git_dir()
99 }
100
101 fn head(&self) -> Result<HeadInfo, VcsError> {
102 self.git.head()
103 }
104
105 fn status(&self) -> Result<StatusModel, VcsError> {
111 Ok(StatusModel {
112 untracked: DiffModel::default(),
113 unstaged: DiffModel::default(),
114 staged: self.git.working_tree_diff()?,
115 })
116 }
117
118 fn working_tree_diff(&self) -> Result<DiffModel, VcsError> {
119 self.git.working_tree_diff()
120 }
121
122 fn tree_to_workdir_diff(&self, base_oid: &str) -> Result<DiffModel, VcsError> {
123 self.git.tree_to_workdir_diff(base_oid)
124 }
125
126 fn commit_diff(&self, oid: &str) -> Result<DiffModel, VcsError> {
127 self.git.commit_diff(oid)
128 }
129
130 fn range_diff(&self, oldest_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError> {
131 self.git.range_diff(oldest_oid, newest_oid)
132 }
133
134 fn tree_diff(&self, base_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError> {
135 self.git.tree_diff(base_oid, newest_oid)
136 }
137
138 fn merge_base(&self, a: &str, b: &str) -> Result<String, VcsError> {
139 self.git.merge_base(a, b)
140 }
141
142 fn resolve(&self, revision: &str) -> Result<String, VcsError> {
143 self.git.resolve(revision)
144 }
145
146 fn log(&self, limit: usize) -> Result<Vec<LogEntry>, VcsError> {
147 self.git.log(limit)
148 }
149
150 fn default_branch(&self, remote: &str) -> Result<Option<String>, VcsError> {
151 self.git.default_branch(remote)
152 }
153
154 fn commits_between(&self, base: &str, head: &str) -> Result<Vec<LogEntry>, VcsError> {
155 self.git.commits_between(base, head)
156 }
157
158 fn unpushed(&self, limit: usize) -> Result<Option<Vec<LogEntry>>, VcsError> {
159 self.git.unpushed(limit)
160 }
161
162 fn blame(&self, rel: &Path) -> Result<Vec<BlameSpan>, VcsError> {
163 self.git.blame(rel)
164 }
165
166 fn read_at(&self, rev: &str, path: &str) -> Result<Option<String>, VcsError> {
167 self.git.read_at(rev, path)
168 }
169
170 fn read_blob(&self, oid: &str) -> Result<Option<Vec<u8>>, VcsError> {
171 self.git.read_blob(oid)
172 }
173
174 fn tracked_files(&self) -> Result<Vec<PathBuf>, VcsError> {
175 self.git.tracked_files()
176 }
177
178 fn attr(&self, rel: &Path, name: &str) -> bool {
179 self.git.attr(rel, name)
180 }
181
182 fn branches(&self) -> Result<Vec<BranchInfo>, VcsError> {
183 self.git.branches()
184 }
185
186 fn divergence(&self, branch: &str) -> Result<Option<(usize, usize)>, VcsError> {
187 self.git.divergence(branch)
188 }
189
190 fn all_branches(&self) -> Result<Vec<String>, VcsError> {
191 self.git.all_branches()
192 }
193
194 fn stage(&self, _rel: &Path) -> Result<(), VcsError> {
195 Err(VcsError::Rejected(NO_STAGING.into()))
196 }
197
198 fn stage_everything(&self) -> Result<(), VcsError> {
199 Err(VcsError::Rejected(NO_STAGING.into()))
200 }
201
202 fn unstage_everything(&self) -> Result<(), VcsError> {
203 Err(VcsError::Rejected(NO_STAGING.into()))
204 }
205
206 fn stage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
207 Err(VcsError::Rejected(NO_STAGING.into()))
208 }
209
210 fn unstage(&self, _rel: &Path) -> Result<(), VcsError> {
211 Err(VcsError::Rejected(NO_STAGING.into()))
212 }
213
214 fn unstage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
215 Err(VcsError::Rejected(NO_STAGING.into()))
216 }
217
218 fn discard(&self, rel: &Path) -> Result<(), VcsError> {
219 let fileset = format!("root-file:{}", quoted(&rel.to_string_lossy()));
220 self.run(&["restore", "--", &fileset])?;
221 Ok(())
222 }
223
224 fn commit(&self, message: &str) -> Result<String, VcsError> {
227 if message.trim().is_empty() {
228 return Err(VcsError::Rejected("empty commit message".into()));
229 }
230 self.run(&["commit", &format!("--message={message}")])?;
231 self.head_oid()
232 }
233
234 fn head_message(&self) -> Result<String, VcsError> {
235 self.git.head_message()
236 }
237
238 fn amend(&self, message: Option<&str>, use_index: bool) -> Result<String, VcsError> {
245 if let Some(message) = message
246 && message.trim().is_empty()
247 {
248 return Err(VcsError::Rejected("empty commit message".into()));
249 }
250 match message {
251 None => self.run(&["squash", "--use-destination-message"])?,
252 Some(message) if use_index => self.run(&["squash", &format!("--message={message}")])?,
253 Some(message) => {
254 self.run(&["describe", "-r", "@-", &format!("--message={message}")])?
255 }
256 };
257 self.head_oid()
258 }
259
260 fn create_branch(&self, name: &str, _checkout: bool) -> Result<(), VcsError> {
263 self.run(&["bookmark", "create", "-r", "@", "--", name])?;
264 Ok(())
265 }
266
267 fn delete_branch(&self, name: &str) -> Result<(), VcsError> {
269 self.run(&[
270 "bookmark",
271 "delete",
272 "--",
273 &format!("exact:{}", quoted(name)),
274 ])?;
275 Ok(())
276 }
277
278 fn checkout(&self, name: &str) -> Result<(), VcsError> {
282 self.run(&["new", "--", "ed(name)])?;
283 Ok(())
284 }
285
286 fn stash_push(&self, _message: Option<&str>) -> Result<(), VcsError> {
287 Err(VcsError::Rejected(NO_STASH.into()))
288 }
289
290 fn stash_pop(&self) -> Result<(), VcsError> {
291 Err(VcsError::Rejected(NO_STASH.into()))
292 }
293
294 fn network_argv(&self, op: NetworkOp) -> Result<Vec<String>, VcsError> {
295 let args: &[&str] = match op {
296 NetworkOp::Fetch => &["git", "fetch"],
297 NetworkOp::FetchAll => &["git", "fetch", "--all-remotes"],
298 NetworkOp::Push
299 | NetworkOp::PushSetUpstream { .. }
300 | NetworkOp::Pull
301 | NetworkOp::PullFrom { .. }
302 | NetworkOp::PullRebase
303 | NetworkOp::PullMerge => return Err(VcsError::Rejected(NO_PUSH_PULL.into())),
304 };
305 Ok(std::iter::once("jj")
306 .chain(args.iter().copied())
307 .map(str::to_owned)
308 .collect())
309 }
310
311 fn workdir(&self) -> Result<PathBuf, VcsError> {
312 self.git.workdir()
313 }
314
315 fn remote_url(&self, name: &str) -> Result<Option<String>, VcsError> {
316 self.git.remote_url(name)
317 }
318
319 fn remotes(&self) -> Result<Vec<String>, VcsError> {
320 self.git.remotes()
321 }
322
323 fn set_diff_algorithm(&self, algorithm: DiffAlgorithm, indent_heuristic: bool) {
324 self.git.set_diff_algorithm(algorithm, indent_heuristic);
325 }
326}