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 tracked_files(&self) -> Result<Vec<PathBuf>, VcsError> {
171 self.git.tracked_files()
172 }
173
174 fn attr(&self, rel: &Path, name: &str) -> bool {
175 self.git.attr(rel, name)
176 }
177
178 fn branches(&self) -> Result<Vec<BranchInfo>, VcsError> {
179 self.git.branches()
180 }
181
182 fn divergence(&self, branch: &str) -> Result<Option<(usize, usize)>, VcsError> {
183 self.git.divergence(branch)
184 }
185
186 fn all_branches(&self) -> Result<Vec<String>, VcsError> {
187 self.git.all_branches()
188 }
189
190 fn stage(&self, _rel: &Path) -> Result<(), VcsError> {
191 Err(VcsError::Rejected(NO_STAGING.into()))
192 }
193
194 fn stage_everything(&self) -> Result<(), VcsError> {
195 Err(VcsError::Rejected(NO_STAGING.into()))
196 }
197
198 fn unstage_everything(&self) -> Result<(), VcsError> {
199 Err(VcsError::Rejected(NO_STAGING.into()))
200 }
201
202 fn stage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
203 Err(VcsError::Rejected(NO_STAGING.into()))
204 }
205
206 fn unstage(&self, _rel: &Path) -> Result<(), VcsError> {
207 Err(VcsError::Rejected(NO_STAGING.into()))
208 }
209
210 fn unstage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
211 Err(VcsError::Rejected(NO_STAGING.into()))
212 }
213
214 fn discard(&self, rel: &Path) -> Result<(), VcsError> {
215 let fileset = format!("root-file:{}", quoted(&rel.to_string_lossy()));
216 self.run(&["restore", "--", &fileset])?;
217 Ok(())
218 }
219
220 fn commit(&self, message: &str) -> Result<String, VcsError> {
223 if message.trim().is_empty() {
224 return Err(VcsError::Rejected("empty commit message".into()));
225 }
226 self.run(&["commit", &format!("--message={message}")])?;
227 self.head_oid()
228 }
229
230 fn head_message(&self) -> Result<String, VcsError> {
231 self.git.head_message()
232 }
233
234 fn amend(&self, message: Option<&str>, use_index: bool) -> Result<String, VcsError> {
241 if let Some(message) = message
242 && message.trim().is_empty()
243 {
244 return Err(VcsError::Rejected("empty commit message".into()));
245 }
246 match message {
247 None => self.run(&["squash", "--use-destination-message"])?,
248 Some(message) if use_index => self.run(&["squash", &format!("--message={message}")])?,
249 Some(message) => {
250 self.run(&["describe", "-r", "@-", &format!("--message={message}")])?
251 }
252 };
253 self.head_oid()
254 }
255
256 fn create_branch(&self, name: &str, _checkout: bool) -> Result<(), VcsError> {
259 self.run(&["bookmark", "create", "-r", "@", "--", name])?;
260 Ok(())
261 }
262
263 fn delete_branch(&self, name: &str) -> Result<(), VcsError> {
265 self.run(&[
266 "bookmark",
267 "delete",
268 "--",
269 &format!("exact:{}", quoted(name)),
270 ])?;
271 Ok(())
272 }
273
274 fn checkout(&self, name: &str) -> Result<(), VcsError> {
278 self.run(&["new", "--", "ed(name)])?;
279 Ok(())
280 }
281
282 fn stash_push(&self, _message: Option<&str>) -> Result<(), VcsError> {
283 Err(VcsError::Rejected(NO_STASH.into()))
284 }
285
286 fn stash_pop(&self) -> Result<(), VcsError> {
287 Err(VcsError::Rejected(NO_STASH.into()))
288 }
289
290 fn network_argv(&self, op: NetworkOp) -> Result<Vec<String>, VcsError> {
291 let args: &[&str] = match op {
292 NetworkOp::Fetch => &["git", "fetch"],
293 NetworkOp::FetchAll => &["git", "fetch", "--all-remotes"],
294 NetworkOp::Push
295 | NetworkOp::PushSetUpstream { .. }
296 | NetworkOp::Pull
297 | NetworkOp::PullFrom { .. }
298 | NetworkOp::PullRebase
299 | NetworkOp::PullMerge => return Err(VcsError::Rejected(NO_PUSH_PULL.into())),
300 };
301 Ok(std::iter::once("jj")
302 .chain(args.iter().copied())
303 .map(str::to_owned)
304 .collect())
305 }
306
307 fn workdir(&self) -> Result<PathBuf, VcsError> {
308 self.git.workdir()
309 }
310
311 fn remote_url(&self, name: &str) -> Result<Option<String>, VcsError> {
312 self.git.remote_url(name)
313 }
314
315 fn remotes(&self) -> Result<Vec<String>, VcsError> {
316 self.git.remotes()
317 }
318
319 fn set_diff_algorithm(&self, algorithm: DiffAlgorithm, indent_heuristic: bool) {
320 self.git.set_diff_algorithm(algorithm, indent_heuristic);
321 }
322}