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