1use std::cell::{Cell, RefCell};
7use std::collections::HashMap;
8use std::fs;
9use 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, Bookmark, BranchInfo, ChangeInfo, ChangeOp, HeadInfo, JjView, LogEntry, NetworkOp,
18 PullOutcome, PullTarget, StatusModel, Vcs, VcsError,
19};
20
21const NO_DESCRIPTION: &str = "(no description set)";
22const FIELD_SEPARATOR: char = '\x1f';
23const LIST_SEPARATOR: char = '\x1e';
24const COMMIT_ID_TEMPLATE: &str = r#"commit_id ++ "\n""#;
25const NEAREST_BOOKMARKS: &str = "heads(::@ & (bookmarks() | remote_bookmarks()))";
27const HISTORY: &str = "::(@ | bookmarks() | remote_bookmarks() | tags())";
29
30pub struct JjVcs {
31 git: GitVcs,
32 root: PathBuf,
33 colocated: bool,
34 view: RefCell<JjView>,
35 base_known: Cell<bool>,
38}
39
40impl JjVcs {
41 pub fn open(root: &Path) -> Result<Self, VcsError> {
42 Self::open_with_settings(root, &DiffSettings::default())
43 }
44
45 pub fn open_with_settings(root: &Path, settings: &DiffSettings) -> Result<Self, VcsError> {
46 let colocated = root.join(".git").exists();
47 let git = if colocated {
48 GitVcs::open_with_settings(root, settings)?
49 } else {
50 GitVcs::open_jj_store(&store_git_dir(root)?, root, settings)?
51 };
52 Ok(Self {
53 git,
54 root: root.to_path_buf(),
55 colocated,
56 view: RefCell::default(),
57 base_known: Cell::new(false),
58 })
59 }
60
61 fn query(
64 &self,
65 revset: &str,
66 template: &str,
67 limit: Option<usize>,
68 ) -> Result<String, VcsError> {
69 let mut args = vec![
70 "log".to_owned(),
71 "--no-graph".to_owned(),
72 "--ignore-working-copy".to_owned(),
73 format!("--revisions={revset}"),
74 format!("--template={template}"),
75 ];
76 if let Some(limit) = limit {
77 args.push(format!("--limit={limit}"));
78 }
79 let args: Vec<&str> = args.iter().map(String::as_str).collect();
80 self.run(&args)
81 }
82
83 fn read_state(&self) -> Result<JjView, VcsError> {
86 let revset = if self.colocated {
87 format!("@ | {NEAREST_BOOKMARKS}")
88 } else {
89 "@ | bookmarks() | remote_bookmarks()".to_owned()
90 };
91 let marker = format!(
92 r#"if(self.current_working_copy(), "@", if(self.contained_in("{NEAREST_BOOKMARKS}"), "^", "-"))"#
93 );
94 let template = line_template(&[
95 &marker,
96 "commit_id",
97 "change_id.short(8)",
98 "description.first_line()",
99 &list_template("parents.map(|c| c.commit_id())"),
100 &list_template("local_bookmarks.map(|b| b.name())"),
101 &list_template(r#"remote_bookmarks.map(|b| b.name() ++ "@" ++ b.remote())"#),
102 r#"committer.timestamp().format("%s")"#,
103 ]);
104 let out = self.query(&revset, &template, None)?;
105 Ok(parse_state(&out, !self.colocated))
106 }
107
108 fn read_change_ids(&self, limit: usize) -> Result<HashMap<String, String>, VcsError> {
111 let template = line_template(&["commit_id", "change_id.short(8)"]);
112 let out = self.query(HISTORY, &template, Some(limit))?;
113 Ok(out
114 .lines()
115 .filter_map(|line| line.split_once(FIELD_SEPARATOR))
116 .map(|(oid, change)| (oid.to_owned(), change.to_owned()))
117 .collect())
118 }
119
120 fn ensure_base(&self) -> Result<(), VcsError> {
124 if self.colocated || self.base_known.get() {
125 return Ok(());
126 }
127 self.reread_state()
128 }
129
130 fn reread_state(&self) -> Result<(), VcsError> {
132 let state = self.read_state()?;
133 let change_ids = std::mem::take(&mut self.view.borrow_mut().change_ids);
134 self.adopt_jj_view(JjView {
135 change_ids,
136 ..state
137 });
138 Ok(())
139 }
140
141 fn decorated(&self, mut entries: Vec<LogEntry>) -> Vec<LogEntry> {
145 let view = self.view.borrow();
146 for entry in &mut entries {
147 entry.change_id = view.change_ids.get(&entry.oid).cloned();
148 if self.colocated {
149 entry.refs.retain(|name| !name.starts_with("jj/keep/"));
150 } else {
151 entry.refs = view
152 .bookmarks
153 .iter()
154 .filter(|bookmark| bookmark.oid == entry.oid)
155 .map(|bookmark| bookmark.name.clone())
156 .collect();
157 }
158 if entry.subject.is_empty() {
159 NO_DESCRIPTION.clone_into(&mut entry.subject);
160 }
161 }
162 entries
163 }
164
165 fn bookmark_oid(&self, name: &str) -> Option<String> {
167 self.view
168 .borrow()
169 .bookmarks
170 .iter()
171 .find(|bookmark| bookmark.name == name)
172 .map(|bookmark| bookmark.oid.clone())
173 }
174
175 fn resolve_revset(&self, revision: &str) -> Result<String, VcsError> {
177 let out = self.query(revision, COMMIT_ID_TEMPLATE, Some(2))?;
178 match out.lines().collect::<Vec<_>>().as_slice() {
179 [oid] if is_root(oid) => Err(VcsError::Rejected(format!(
180 "`{revision}` names jj's root commit, which git has no commit for; name another"
181 ))),
182 [oid] => Ok((*oid).to_owned()),
183 [] => Err(VcsError::Rejected(format!(
184 "`{revision}` matches no commit, name one that exists"
185 ))),
186 _ => Err(VcsError::Rejected(format!(
187 "`{revision}` matches several commits, name one"
188 ))),
189 }
190 }
191
192 fn run(&self, args: &[&str]) -> Result<String, VcsError> {
195 let output = Command::new("jj")
196 .current_dir(&self.root)
197 .args(args)
198 .env("JJ_EDITOR", "false")
201 .output()
202 .map_err(|err| match err.kind() {
203 io::ErrorKind::NotFound => VcsError::Rejected(
204 "install jj (jujutsu) on PATH to read and change this repo".into(),
205 ),
206 _ => VcsError::Io(err),
207 })?;
208 if output.status.success() {
209 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned());
210 }
211 let stderr = String::from_utf8_lossy(&output.stderr);
212 let mut lines = stderr.lines().map(str::trim);
213 let message = lines
214 .clone()
215 .find(|line| line.starts_with("Error:"))
216 .or_else(|| lines.find(|line| !line.is_empty()))
217 .unwrap_or("run the jj command in a shell to see why it failed")
218 .to_owned();
219 Err(VcsError::Rejected(message))
220 }
221
222 fn head_oid(&self) -> Result<String, VcsError> {
225 if !self.colocated {
226 self.reread_state()?;
227 }
228 self.git.resolve("HEAD")
229 }
230
231 fn target(&self, rev: &str) -> Result<String, VcsError> {
235 if is_working_copy_revset(rev) {
236 return Ok(rev.to_owned());
237 }
238 let change = self.query("ed(rev), "change_id", Some(1))?;
239 if change.is_empty() {
240 return Err(VcsError::Rejected(format!(
241 "`{rev}` names no commit; refresh and pick another"
242 )));
243 }
244 Ok(format!("change_id({})", quoted(&change)))
245 }
246
247 fn refuse_over_snapshot(&self, verb: &str) -> Result<(), VcsError> {
251 self.run(&["util", "snapshot"])?;
252 let newest = self.run(&[
253 "op",
254 "log",
255 "--ignore-working-copy",
256 "--no-graph",
257 "--limit=1",
258 "--template=description",
259 ])?;
260 if newest.starts_with("snapshot working copy") {
261 return Err(VcsError::Rejected(format!(
262 "describe or commit your edits first; {verb} would drop them"
263 )));
264 }
265 Ok(())
266 }
267
268 fn squash(&self, rev: &str) -> Result<String, VcsError> {
271 let target = self.target(rev)?;
272 let source = self.query(&target, "description", Some(1))?;
273 let destination = self.query(&format!("({target})-"), "description", Some(1))?;
274 let revision = format!("--revision={target}");
275 if source.is_empty() || destination.is_empty() {
276 return self.run(&["squash", &revision]);
277 }
278 self.run(&[
279 "squash",
280 &revision,
281 &format!("--message={destination}\n\n{source}"),
282 ])
283 }
284
285 fn nearest_bookmark(&self) -> Result<NearestBookmark, VcsError> {
287 let template = line_template(&[
288 r#"if(self.current_working_copy(), "@", if(self.contained_in("@-"), "-", "^"))"#,
289 r#"if(immutable, "1", "0")"#,
290 &list_template("local_bookmarks.map(|b| b.name())"),
291 ]);
292 let out = self.query("@- | heads(::@ & bookmarks())", &template, None)?;
293 Ok(parse_nearest_bookmark(&out))
294 }
295
296 fn push_selection(&self) -> Result<String, VcsError> {
301 let nearest = self.nearest_bookmark()?;
302 match nearest.name {
303 Some(name) if nearest.on_working_line => {
304 return Ok(format!("--bookmark=exact:{}", quoted(&name)));
305 }
306 Some(name) if !nearest.immutable => {
307 return Err(VcsError::Rejected(format!(
308 "move {name} to @- first (jj bookmark move {name} --to @-), then push"
309 )));
310 }
311 _ => {}
312 }
313 if nearest.parent_immutable {
314 return Err(VcsError::Rejected(
315 "commit a change on top of @- first; there is nothing new to push".into(),
316 ));
317 }
318 Ok("--change=@-".into())
319 }
320}
321
322fn store_git_dir(root: &Path) -> Result<PathBuf, VcsError> {
325 if root.join(".jj/repo").is_file() {
327 return Err(VcsError::Rejected(
328 "open the repository's main jj workspace; secondary workspaces aren't supported yet"
329 .into(),
330 ));
331 }
332 let store = root.join(".jj/repo/store");
333 let target = fs::read_to_string(store.join("git_target"))?;
334 Ok(store.join(target.trim()))
335}
336
337fn line_template(fields: &[&str]) -> String {
339 let separator = format!(r#" ++ "\x{:02x}" ++ "#, u32::from(FIELD_SEPARATOR));
340 format!(r#"{} ++ "\n""#, fields.join(&separator))
341}
342
343fn list_template(list: &str) -> String {
344 format!(r#"{list}.join("\x{:02x}")"#, u32::from(LIST_SEPARATOR))
345}
346
347fn split_list(list: &str) -> impl Iterator<Item = &str> {
348 list.split(LIST_SEPARATOR).filter(|item| !item.is_empty())
349}
350
351fn real_remotes(list: &str) -> impl Iterator<Item = &str> {
353 split_list(list).filter(|name| !name.ends_with("@git"))
354}
355
356fn parse_state(out: &str, bookmarks: bool) -> JjView {
359 let mut view = JjView::default();
360 let mut nearest = None;
361 for line in out.lines() {
362 let fields: Vec<&str> = line.split(FIELD_SEPARATOR).collect();
363 let &[
364 marker,
365 oid,
366 change_id,
367 description,
368 parents,
369 local,
370 remote,
371 time,
372 ] = fields.as_slice()
373 else {
374 continue;
375 };
376 let bookmark = || split_list(local).chain(real_remotes(remote)).next();
377 if marker == "@" {
378 view.base = split_list(parents)
379 .next()
380 .filter(|id| !is_root(id))
381 .map(str::to_owned);
382 view.change = Some(ChangeInfo {
383 change_id: change_id.to_owned(),
384 commit_id: oid.to_owned(),
385 bookmark: bookmark().map(str::to_owned),
386 description: description.to_owned(),
387 });
388 } else if marker == "^" && nearest.is_none() {
389 nearest = bookmark();
390 }
391 if bookmarks {
392 let tip_unix = time.parse().unwrap_or_default();
393 let names = split_list(local)
394 .map(|name| (name, false))
395 .chain(real_remotes(remote).map(|name| (name, true)));
396 view.bookmarks.extend(names.map(|(name, remote)| Bookmark {
397 name: name.to_owned(),
398 oid: oid.to_owned(),
399 remote,
400 tip_unix,
401 }));
402 }
403 }
404 if let Some(change) = view.change.as_mut()
405 && change.bookmark.is_none()
406 {
407 change.bookmark = nearest.map(str::to_owned);
408 }
409 view
410}
411
412fn split_ancestry(revision: &str) -> (&str, &str) {
415 let kept = revision
416 .trim_end_matches(|c: char| matches!(c, '~' | '^') || c.is_ascii_digit())
417 .len();
418 revision
419 .get(kept..)
420 .and_then(|tail| tail.find(['~', '^']))
421 .map(|offset| kept + offset)
422 .filter(|&at| at > 0)
423 .and_then(|at| revision.split_at_checked(at))
424 .unwrap_or((revision, ""))
425}
426
427fn is_root(oid: &str) -> bool {
429 oid.bytes().all(|byte| byte == b'0')
430}
431
432fn is_working_copy_revset(revision: &str) -> bool {
435 revision == "@" || revision.starts_with("@-") || revision.starts_with("@+")
436}
437
438fn quoted(s: &str) -> String {
441 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
442}
443
444fn root_file(path: &str) -> String {
446 format!("root-file:{}", quoted(path))
447}
448
449struct NearestBookmark {
452 name: Option<String>,
453 on_working_line: bool,
455 immutable: bool,
456 parent_immutable: bool,
457}
458
459fn parse_nearest_bookmark(out: &str) -> NearestBookmark {
461 let mut nearest = NearestBookmark {
462 name: None,
463 on_working_line: false,
464 immutable: false,
465 parent_immutable: false,
466 };
467 for line in out.lines() {
468 let fields: Vec<&str> = line.split(FIELD_SEPARATOR).collect();
469 let &[marker, immutable, names] = fields.as_slice() else {
470 continue;
471 };
472 let immutable = immutable == "1";
473 if marker == "-" && immutable {
474 nearest.parent_immutable = true;
475 }
476 if nearest.name.is_none()
477 && let Some(name) = split_list(names).next()
478 {
479 nearest.name = Some(name.to_owned());
480 nearest.on_working_line = marker != "^";
481 nearest.immutable = immutable;
482 }
483 }
484 nearest
485}
486
487const EMPTY_MESSAGE: &str = "write a commit message first";
488const NO_STAGING: &str = "commit the whole working copy; jj has no staging area";
489const NO_STASH: &str = "start a new change from the change menu instead; jj has no stash";
490const NO_PULL_MODE: &str = "pull with p p; jj rebases @ onto the fetched bookmark";
491
492impl Vcs for JjVcs {
493 fn has_index(&self) -> bool {
494 false
495 }
496
497 fn native_git_checkout(&self) -> bool {
498 false
499 }
500
501 fn git_dir(&self) -> Result<PathBuf, VcsError> {
502 self.git.git_dir()
503 }
504
505 fn head(&self) -> Result<HeadInfo, VcsError> {
506 self.ensure_base()?;
507 let mut head = self.git.head()?;
508 if head.oid7.is_empty() {
509 head.branch = None;
510 }
511 Ok(head)
512 }
513
514 fn read_jj_view(&self, log_limit: usize) -> Result<Option<JjView>, VcsError> {
517 let view = JjView {
518 change_ids: self.read_change_ids(log_limit.saturating_mul(2))?,
519 ..self.read_state()?
520 };
521 self.adopt_jj_view(view.clone());
522 Ok(Some(view))
523 }
524
525 fn adopt_jj_view(&self, view: JjView) {
526 if !self.colocated {
527 self.base_known.set(true);
528 self.git.set_base(view.base.as_deref()).ok();
530 }
531 *self.view.borrow_mut() = view;
532 }
533
534 fn status(&self) -> Result<StatusModel, VcsError> {
537 Ok(StatusModel {
538 untracked: DiffModel::default(),
539 unstaged: DiffModel::default(),
540 staged: self.working_tree_diff()?,
541 })
542 }
543
544 fn working_tree_diff(&self) -> Result<DiffModel, VcsError> {
545 self.ensure_base()?;
546 self.git.working_tree_diff()
547 }
548
549 fn tree_to_workdir_diff(&self, base_oid: &str) -> Result<DiffModel, VcsError> {
550 self.git.tree_to_workdir_diff(base_oid)
551 }
552
553 fn commit_diff(&self, oid: &str) -> Result<DiffModel, VcsError> {
554 self.git.commit_diff(oid)
555 }
556
557 fn range_diff(&self, oldest_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError> {
558 self.git.range_diff(oldest_oid, newest_oid)
559 }
560
561 fn tree_diff(&self, base_oid: &str, newest_oid: &str) -> Result<DiffModel, VcsError> {
562 self.git.tree_diff(base_oid, newest_oid)
563 }
564
565 fn merge_base(&self, a: &str, b: &str) -> Result<String, VcsError> {
566 self.git.merge_base(a, b)
567 }
568
569 fn resolve(&self, revision: &str) -> Result<String, VcsError> {
577 if is_working_copy_revset(revision) {
578 return self.resolve_revset(revision);
579 }
580 self.ensure_base()?;
581 let (name, ancestry) = split_ancestry(revision);
582 if !self.colocated
583 && let Some(oid) = self.bookmark_oid(name)
584 {
585 return self.git.resolve(&format!("{oid}{ancestry}"));
586 }
587 let resolved = self.git.resolve(revision);
588 if resolved.is_ok() || name == "HEAD" {
590 return resolved;
591 }
592 if !ancestry.is_empty() {
593 return Err(VcsError::Rejected(format!(
594 "`{name}` names no bookmark, ref or commit; name one before `{ancestry}`"
595 )));
596 }
597 self.resolve_revset(revision)
598 }
599
600 fn log(&self, limit: usize, all_branches: bool) -> Result<Vec<LogEntry>, VcsError> {
601 self.ensure_base()?;
602 let entries = {
603 let view = self.view.borrow();
604 let tips: Vec<&str> = view
605 .bookmarks
606 .iter()
607 .map(|bookmark| bookmark.oid.as_str())
608 .filter(|oid| !is_root(oid))
609 .collect();
610 self.git.log_from(limit, all_branches, &tips)?
611 };
612 Ok(self.decorated(entries))
613 }
614
615 fn default_branch(&self, remote: &str) -> Result<Option<String>, VcsError> {
616 self.git.default_branch(remote)
617 }
618
619 fn commits_between(&self, base: &str, head: &str) -> Result<Vec<LogEntry>, VcsError> {
620 self.ensure_base()?;
621 Ok(self.decorated(self.git.commits_between(base, head)?))
622 }
623
624 fn unpushed(&self, limit: usize) -> Result<Option<Vec<LogEntry>>, VcsError> {
625 self.ensure_base()?;
626 Ok(self
627 .git
628 .unpushed(limit)?
629 .map(|entries| self.decorated(entries)))
630 }
631
632 fn blame(&self, rel: &Path) -> Result<Vec<BlameSpan>, VcsError> {
633 self.ensure_base()?;
634 self.git.blame(rel)
635 }
636
637 fn read_at(&self, rev: &str, path: &str) -> Result<Option<String>, VcsError> {
638 self.ensure_base()?;
639 self.git.read_at(rev, path)
640 }
641
642 fn read_blob(&self, oid: &str) -> Result<Option<Vec<u8>>, VcsError> {
643 self.git.read_blob(oid)
644 }
645
646 fn tracked_files(&self) -> Result<Vec<PathBuf>, VcsError> {
647 self.ensure_base()?;
648 self.git.tracked_files()
649 }
650
651 fn attr(&self, rel: &Path, name: &str) -> bool {
652 self.git.attr(rel, name)
653 }
654
655 fn branches(&self) -> Result<Vec<BranchInfo>, VcsError> {
658 if self.colocated {
659 return self.git.branches();
660 }
661 let view = self.view.borrow();
662 let here = view.change.as_ref().and_then(|c| c.bookmark.as_deref());
663 Ok(view
664 .bookmarks
665 .iter()
666 .filter(|bookmark| !bookmark.remote)
667 .map(|bookmark| BranchInfo {
668 name: bookmark.name.clone(),
669 is_head: here == Some(bookmark.name.as_str()),
670 tip_unix: bookmark.tip_unix,
671 divergence: None,
672 })
673 .collect())
674 }
675
676 fn divergence(&self, branch: &str) -> Result<Option<(usize, usize)>, VcsError> {
679 if self.colocated {
680 return self.git.divergence(branch);
681 }
682 let view = self.view.borrow();
683 let local = view
684 .bookmarks
685 .iter()
686 .find(|bookmark| !bookmark.remote && bookmark.name == branch);
687 let upstream = view.bookmarks.iter().find(|bookmark| {
688 bookmark.remote
689 && bookmark
690 .name
691 .rsplit_once('@')
692 .is_some_and(|(name, _)| name == branch)
693 });
694 let (Some(local), Some(upstream)) = (local, upstream) else {
695 return Ok(None);
696 };
697 self.git.ahead_behind(&local.oid, &upstream.oid).map(Some)
698 }
699
700 fn all_branches(&self) -> Result<Vec<String>, VcsError> {
701 if self.colocated {
702 return self.git.all_branches();
703 }
704 Ok(self
705 .view
706 .borrow()
707 .bookmarks
708 .iter()
709 .map(|bookmark| bookmark.name.clone())
710 .collect())
711 }
712
713 fn stage(&self, _rel: &Path) -> Result<(), VcsError> {
714 Err(VcsError::Rejected(NO_STAGING.into()))
715 }
716
717 fn stage_everything(&self) -> Result<(), VcsError> {
718 Err(VcsError::Rejected(NO_STAGING.into()))
719 }
720
721 fn unstage_everything(&self) -> Result<(), VcsError> {
722 Err(VcsError::Rejected(NO_STAGING.into()))
723 }
724
725 fn stage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
726 Err(VcsError::Rejected(NO_STAGING.into()))
727 }
728
729 fn unstage(&self, _rel: &Path) -> Result<(), VcsError> {
730 Err(VcsError::Rejected(NO_STAGING.into()))
731 }
732
733 fn unstage_hunk(&self, _rel: &Path, _hunk: &HunkId) -> Result<(), VcsError> {
734 Err(VcsError::Rejected(NO_STAGING.into()))
735 }
736
737 fn discard(&self, rel: &Path) -> Result<(), VcsError> {
738 self.run(&["restore", "--", &root_file(&rel.to_string_lossy())])?;
739 Ok(())
740 }
741
742 fn commit_paths(&self, message: &str, paths: &[String]) -> Result<String, VcsError> {
743 if message.trim().is_empty() {
744 return Err(VcsError::Rejected(EMPTY_MESSAGE.into()));
745 }
746 let message = format!("--message={message}");
747 let filesets: Vec<String> = paths
749 .iter()
750 .map(|path| format!("root:{}", quoted(path)))
751 .collect();
752 let mut args = vec!["commit", message.as_str(), "--"];
753 args.extend(filesets.iter().map(String::as_str));
754 self.run(&args)?;
755 self.head_oid()
756 }
757
758 fn change(&self, op: &ChangeOp) -> Result<(), VcsError> {
759 match op {
760 ChangeOp::New(rev) => self.run(&["new", "--", &self.target(rev)?]),
761 ChangeOp::Edit(rev) => self.run(&["edit", "--", &self.target(rev)?]),
762 ChangeOp::Describe { rev, message } => self.run(&[
763 "describe",
764 &format!("--message={message}"),
765 "--",
766 &self.target(rev)?,
767 ]),
768 ChangeOp::Squash(rev) => self.squash(rev),
769 ChangeOp::Abandon(rev) => self.run(&["abandon", "--", &self.target(rev)?]),
770 ChangeOp::Undo => {
771 self.refuse_over_snapshot("undo")?;
772 self.run(&["undo"])
773 }
774 ChangeOp::Redo => {
775 self.refuse_over_snapshot("redo")?;
776 self.run(&["redo"])
777 }
778 }?;
779 Ok(())
780 }
781
782 fn pull_target(&self) -> Result<Option<PullTarget>, VcsError> {
784 let Some(bookmark) = self.nearest_bookmark()?.name else {
785 return Ok(None);
786 };
787 let tracked = self.run(&[
788 "bookmark",
789 "list",
790 "--ignore-working-copy",
791 "--tracked",
792 r#"--template=if(remote, remote ++ "\n")"#,
793 "--",
794 &format!("exact:{}", quoted(&bookmark)),
795 ])?;
796 let remotes: Vec<&str> = tracked.lines().filter(|r| *r != "git").collect();
797 let remote = remotes
798 .iter()
799 .find(|r| **r == "origin")
800 .or_else(|| remotes.first())
801 .map(|r| (*r).to_owned());
802 Ok(Some(PullTarget { bookmark, remote }))
803 }
804
805 fn rebase_onto_remote(&self, bookmark: &str, remote: &str) -> Result<PullOutcome, VcsError> {
806 let onto = format!("{bookmark}@{remote}");
807 let target = format!(
808 "remote_bookmarks(exact:{}, exact:{})",
809 quoted(bookmark),
810 quoted(remote)
811 );
812 if self.query(&target, COMMIT_ID_TEMPLATE, Some(1))?.is_empty() {
813 return Ok(PullOutcome::Gone { onto });
814 }
815 if !self
816 .query(&format!("({target}) & ::@"), COMMIT_ID_TEMPLATE, Some(1))?
817 .is_empty()
818 {
819 return Ok(PullOutcome::UpToDate { onto });
820 }
821 self.run(&["rebase", "--branch=@", &format!("--onto={target}")])?;
822 let conflicts = self
823 .query(
824 &format!("({target})..@ & conflicts()"),
825 COMMIT_ID_TEMPLATE,
826 None,
827 )?
828 .lines()
829 .count();
830 let bookmark_conflicted = !self
831 .run(&[
832 "bookmark",
833 "list",
834 "--ignore-working-copy",
835 "--conflicted",
836 "--",
837 &format!("exact:{}", quoted(bookmark)),
838 ])?
839 .is_empty();
840 Ok(PullOutcome::Rebased {
841 onto,
842 conflicts,
843 bookmark_conflicted,
844 })
845 }
846
847 fn commit_message(&self, oid: &str) -> Result<String, VcsError> {
848 self.git.commit_message(oid)
849 }
850
851 fn commit(&self, message: &str) -> Result<String, VcsError> {
854 if message.trim().is_empty() {
855 return Err(VcsError::Rejected(EMPTY_MESSAGE.into()));
856 }
857 self.run(&["commit", &format!("--message={message}")])?;
858 self.head_oid()
859 }
860
861 fn head_message(&self) -> Result<String, VcsError> {
862 self.ensure_base()?;
863 self.git.head_message()
864 }
865
866 fn amend(&self, message: Option<&str>, use_index: bool) -> Result<String, VcsError> {
869 if let Some(message) = message
870 && message.trim().is_empty()
871 {
872 return Err(VcsError::Rejected(EMPTY_MESSAGE.into()));
873 }
874 match message {
875 None => self.run(&["squash", "--use-destination-message"])?,
876 Some(message) if use_index => self.run(&["squash", &format!("--message={message}")])?,
877 Some(message) => {
878 self.run(&["describe", "-r", "@-", &format!("--message={message}")])?
879 }
880 };
881 self.head_oid()
882 }
883
884 fn create_branch(&self, name: &str, _checkout: bool) -> Result<(), VcsError> {
886 self.run(&["bookmark", "create", "-r", "@", "--", name])?;
887 Ok(())
888 }
889
890 fn delete_branch(&self, name: &str) -> Result<(), VcsError> {
892 self.run(&[
893 "bookmark",
894 "delete",
895 "--",
896 &format!("exact:{}", quoted(name)),
897 ])?;
898 Ok(())
899 }
900
901 fn checkout(&self, name: &str) -> Result<(), VcsError> {
904 self.run(&["new", "--", "ed(name)])?;
905 Ok(())
906 }
907
908 fn stash_push(&self, _message: Option<&str>) -> Result<(), VcsError> {
909 Err(VcsError::Rejected(NO_STASH.into()))
910 }
911
912 fn stash_pop(&self) -> Result<(), VcsError> {
913 Err(VcsError::Rejected(NO_STASH.into()))
914 }
915
916 fn network_argv(&self, op: NetworkOp) -> Result<Vec<String>, VcsError> {
920 let mut args = vec!["git".to_owned()];
921 match op {
922 NetworkOp::Fetch | NetworkOp::Pull => args.push("fetch".into()),
923 NetworkOp::FetchAll => args.extend(["fetch".into(), "--all-remotes".into()]),
924 NetworkOp::PullFrom { remote, .. } => {
925 args.extend(["fetch".into(), format!("--remote={remote}")]);
926 }
927 NetworkOp::Push | NetworkOp::PushSetUpstream { .. } => {
928 args.push("push".into());
929 if let NetworkOp::PushSetUpstream { remote } = op {
930 args.push(format!("--remote={remote}"));
931 }
932 args.push(self.push_selection()?);
933 }
934 NetworkOp::PullRebase | NetworkOp::PullMerge => {
935 return Err(VcsError::Rejected(NO_PULL_MODE.into()));
936 }
937 }
938 Ok(std::iter::once("jj".to_owned()).chain(args).collect())
939 }
940
941 fn workdir(&self) -> Result<PathBuf, VcsError> {
942 self.git.workdir()
943 }
944
945 fn remote_url(&self, name: &str) -> Result<Option<String>, VcsError> {
946 self.git.remote_url(name)
947 }
948
949 fn remotes(&self) -> Result<Vec<String>, VcsError> {
950 self.git.remotes()
951 }
952
953 fn set_diff_algorithm(&self, algorithm: DiffAlgorithm, indent_heuristic: bool) {
954 self.git.set_diff_algorithm(algorithm, indent_heuristic);
955 }
956}