on master help sign in
j/src/jj.rs
4978 lines 223 kB history raw
1//! jj-lib backend (§7): repository location (§7.1), building the `Repo`
2//! value with snapshotting (§7.2/§7.4), persistence (§7.5), and the lock
3//! (§7.7). Reserved commands (fetch/push/undo/ops/init/clone/remote) remain
4//! stubs for a later session.
6use crate::config::Config;
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
8use crate::eval::Interp;
9use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, LazyBlob, ListVal, ThunkVal, Value};
10use futures::{StreamExt, TryStreamExt};
11use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
12use jj_lib::commit::Commit;
13use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14use jj_lib::conflicts::ConflictMarkerStyle;
15use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
16use jj_lib::gitignore::GitIgnoreFile;
17use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
18use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
19use jj_lib::merge::{Diff, Merge};
20use jj_lib::merged_tree::MergedTree;
21use jj_lib::merged_tree_builder::MergedTreeBuilder;
22use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
23use jj_lib::object_id::ObjectId;
24use jj_lib::op_store::OperationId;
25use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
26use jj_lib::repo::{ReadonlyRepo, Repo};
27use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent, RepoPathComponentBuf};
28use jj_lib::revset::{ResolvedRevsetExpression, RevsetStreamExt};
29use jj_lib::settings::UserSettings;
30use jj_lib::store::Store;
31use jj_lib::transaction::UnpublishedOperation;
32use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
33use jj_lib::workspace::Workspace;
34use std::cell::{Cell, RefCell};
35use std::collections::{BTreeMap, BTreeSet, HashMap};
36use std::future::Future;
37use std::os::unix::io::AsRawFd;
38use std::path::PathBuf;
39use std::rc::{Rc, Weak};
40use std::sync::{Arc, Mutex, OnceLock};
42fn block_on<F: Future>(f: F) -> F::Output {
43 pollster::block_on(f)
46type OpenError = (u8, String);
48/// The successful effects of a push, after duplicate requests are merged.
49/// All zeroes means no bookmarks were selected and nothing was sent or saved.
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
51pub struct PushOutcome {
52 pub published: usize,
53 pub deleted: usize,
54 pub unchanged: usize,
57// ----------------------------------------------------------------------
58// locating and opening the repository (§7.1)
59// ----------------------------------------------------------------------
61fn find_repo_root() -> Option<PathBuf> {
62 let cwd = std::env::current_dir().ok()?;
63 for dir in cwd.ancestors() {
64 if dir.join(".jj").is_dir() {
65 return Some(dir.to_path_buf());
66 }
67 }
68 None
71fn toml_string(s: &str) -> String {
72 let mut out = String::with_capacity(s.len() + 2);
73 out.push('"');
74 for c in s.chars() {
75 match c {
76 '"' => out.push_str("\\\""),
77 '\\' => out.push_str("\\\\"),
78 '\n' => out.push_str("\\n"),
79 '\r' => out.push_str("\\r"),
80 '\t' => out.push_str("\\t"),
81 c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
82 c => out.push(c),
83 }
84 }
85 out.push('"');
86 out
89/// UserSettings from a StackedConfig containing only `user.name` and
90/// `user.email` (§7.9). Falls back to a placeholder identity when the
91/// config's `user` record cannot be evaluated here (undo/redo/ops read no
92/// config, §6.1).
93fn user_settings(cfg: Option<&Config>) -> UserSettings {
94 let (name, email) = match cfg {
95 Some(cfg) => crate::config::eval_user_only(&mut Interp::dummy(), cfg)
96 .unwrap_or_else(|_| ("j".to_string(), "j".to_string())),
97 None => ("j".to_string(), "j".to_string()),
98 };
99 let mut config = StackedConfig::with_defaults();
100 let text = format!(
101 "[user]\nname = {}\nemail = {}\n",
102 toml_string(&name),
103 toml_string(&email)
104 );
105 let layer =
106 ConfigLayer::parse(ConfigSource::User, &text).expect("generated user config should parse");
107 config.add_layer(layer);
108 UserSettings::from_config(config).expect("generated user config should be valid")
111// ----------------------------------------------------------------------
112// repo -> value mapping (§7.2)
113// ----------------------------------------------------------------------
115struct StoredCommit {
116 commit: Commit,
117 change_id: String,
118 /// change id of the first jj parent ("" for the root commit)
119 first_parent: String,
120 /// change ids of all jj parents ("" entries impossible: the root has none)
121 all_parents: Vec<String>,
122 committer_millis: i64,
125struct VisibleRepo {
126 /// change id -> stored commit
127 commits: BTreeMap<String, StoredCommit>,
128 /// change id -> remote bookmarks of `origin` pointing at it (§7.6)
129 labels: BTreeMap<String, Vec<String>>,
130 /// change id -> child change ids (first-parent tree)
131 children: BTreeMap<String, Vec<String>>,
132 /// the default workspace's working-copy commit, as a change id
133 wc_change_id: String,
136impl Clone for VisibleRepo {
137 fn clone(&self) -> Self {
138 VisibleRepo {
139 commits: self
140 .commits
141 .iter()
142 .map(|(k, v)| {
143 (
144 k.clone(),
145 StoredCommit {
146 commit: v.commit.clone(),
147 change_id: v.change_id.clone(),
148 first_parent: v.first_parent.clone(),
149 all_parents: v.all_parents.clone(),
150 committer_millis: v.committer_millis,
151 },
152 )
153 })
154 .collect(),
155 labels: self.labels.clone(),
156 children: self.children.clone(),
157 wc_change_id: self.wc_change_id.clone(),
158 }
159 }
162async fn read_visible(repo: &Arc<ReadonlyRepo>) -> Result<VisibleRepo, OpenError> {
163 let store = repo.store().clone();
164 // visible commits = ancestors (inclusive) of the visible heads (§7.2)
165 let mut commits: BTreeMap<String, StoredCommit> = BTreeMap::new();
166 let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
167 let mut count_by_change: BTreeMap<String, usize> = BTreeMap::new();
168 let mut visited: BTreeSet<CommitId> = BTreeSet::new();
169 let mut stack: Vec<CommitId> = repo.view().heads().iter().cloned().collect();
170 while let Some(id) = stack.pop() {
171 if !visited.insert(id.clone()) {
172 continue;
173 }
174 let commit = store
175 .get_commit_async(&id)
176 .await
177 .map_err(|e| (2, format!("cannot read commit: {}", e)))?;
178 let change_id = commit.change_id().reverse_hex();
179 *count_by_change.entry(change_id.clone()).or_default() += 1;
180 let mut first_parent = String::new();
181 if let Some(first) = commit.parent_ids().first() {
182 let parent_commit = store
183 .get_commit_async(first)
184 .await
185 .map_err(|e| (2, format!("cannot read commit: {}", e)))?;
186 let pcid = parent_commit.change_id().reverse_hex();
187 children.entry(pcid.clone()).or_default().push(change_id.clone());
188 first_parent = pcid;
189 }
190 let parent_ids = commit.parent_ids().to_vec();
191 let mut all_parents = Vec::new();
192 for pid in &parent_ids {
193 let pcommit = store
194 .get_commit_async(pid)
195 .await
196 .map_err(|e| (2, format!("cannot read commit: {}", e)))?;
197 all_parents.push(pcommit.change_id().reverse_hex());
198 }
199 commits.insert(
200 change_id.clone(),
201 StoredCommit {
202 committer_millis: commit.committer().timestamp.timestamp.0,
203 commit,
204 change_id,
205 first_parent,
206 all_parents,
207 },
208 );
209 stack.extend(parent_ids);
210 }
211 // §7.2: one change id on two visible commits is unsupported
212 for (change_id, n) in &count_by_change {
213 if *n > 1 {
214 return Err((
215 2,
216 format!(
217 "change {} has two visible commits; j cannot operate on this repository",
218 change_id
219 ),
220 ));
221 }
222 }
224 // labels: remote bookmarks of `origin` pointing at a commit (§7.6)
225 let mut labels: BTreeMap<String, Vec<String>> = BTreeMap::new();
226 for (name, remote_ref) in repo.view().remote_bookmarks(RemoteName::new("origin")) {
227 for cid in remote_ref.target.added_ids() {
228 if let Ok(c) = store.get_commit_async(cid).await {
229 labels
230 .entry(c.change_id().reverse_hex())
231 .or_default()
232 .push(name.as_str().to_string());
233 }
234 }
235 }
237 let wc_commit_id = repo
238 .view()
239 .get_wc_commit_id(WorkspaceName::DEFAULT)
240 .cloned()
241 .ok_or_else(|| (2, "the default workspace has no working-copy commit".to_string()))?;
242 let wc_commit = store
243 .get_commit_async(&wc_commit_id)
244 .await
245 .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
247 Ok(VisibleRepo {
248 commits,
249 labels,
250 children,
251 wc_change_id: wc_commit.change_id().reverse_hex(),
252 })
255// ----------------------------------------------------------------------
256// the backend
257// ----------------------------------------------------------------------
259pub struct JjInner {
260 workspace_root: PathBuf,
261 /// the one workspace (and store) for the whole run; every repo load and
262 /// working-copy mutation goes through it so trees always live in the
263 /// same store
264 workspace: Mutex<Workspace>,
265 /// repo at the operation the program is working against; updated after a
266 /// snapshot so persistence and the snapshot form one operation (§1.2)
267 repo: Mutex<Arc<ReadonlyRepo>>,
268 workspace_name: WorkspaceNameBuf,
269 /// visible map backing the `Backend` lookups during evaluation; filled by
270 /// build_interp with the post-snapshot map, or the loaded one when the
271 /// snapshot goes into a new child of an immutable focus (§7.2)
272 visible: Mutex<Option<Arc<VisibleRepo>>>,
273 lock_guard: Mutex<Option<FileLock>>,
274 /// the snapshot's unpublished rewrite, kept between build_interp and
275 /// persist so the snapshot and the expression's edits form one jj
276 /// operation (§1.2); dropped uncommitted by printing runs. The
277 /// working-copy lock is not held: its saved state is still the tree
278 /// recorded before the snapshot until `persist`'s checkout scans the
279 /// directory again
280 pending: Mutex<Option<PendingSnapshot>>,
281 /// whether the workspace's filesystem folds case, probed on first use
282 /// (`Backend::folds_case`)
283 folds_case: OnceLock<bool>,
284 /// whether the workspace's filesystem holds each name longer than
285 /// `NAME_MAX` asked about so far (`Backend::name_fits`)
286 long_names: Mutex<HashMap<String, bool>>,
289/// holding the file keeps the flock
290pub struct FileLock(#[allow(dead_code)] std::fs::File);
292struct PendingSnapshot {
293 /// repo as loaded at head, before the snapshot
294 pre_repo: Arc<ReadonlyRepo>,
295 /// the snapshot's tree for the wc commit, to be folded into the
296 /// persisting operation (§1.2 step 8, §7.7)
297 tree: MergedTree,
298 /// false when the wc commit is immutable: the snapshot then belongs to
299 /// the focus's new child (§7.2), and the wc commit is kept as stored
300 fold_into_wc: bool,
301 /// the tree the scan found in the working directory, which `persist`'s
302 /// checkout starts from: `tree` itself, unless the working copy was
303 /// stale and `tree` merges the directory's changes onto the commit
304 /// (`snapshot_tree`)
305 scanned: MergedTree,
308/// How every scan of the working directory reads it (§7.4): each file
309/// not ignored by the directory's own `.gitignore`s is tracked, whatever
310/// its size. `persist`'s checkout rescans with the same options and
311/// relies on getting the snapshot's tree back.
312fn snapshot_options() -> SnapshotOptions<'static> {
313 SnapshotOptions {
314 base_ignores: GitIgnoreFile::empty(),
315 progress: None,
316 start_tracking_matcher: &EverythingMatcher,
317 force_tracking_matcher: &NothingMatcher,
318 max_new_file_size: u64::MAX,
319 }
322/// Scan the working directory with `options`, as `wc.snapshot` does, where
323/// `base` is the tree the working copy's state records, as `saved` holds
324/// it; return the tree scanned. jj's scan loses a regular file found where
325/// `base` has a conflict it cannot write a file into, one with a side that
326/// is no file, and keeps the conflict, so the file reaches no tree (§7.4):
327///
328/// - at a conflicted path, where a checkout wrote jj's description of the
329/// sides and a file written since holds something else
330/// (`FilesOnConflicts::at`). The state is first reset to a resolved file
331/// there, so that the scan reads the file whole, as it reads any tracked
332/// file; the path stays tracked where a `.gitignore` has come to match it.
333/// - above one, where `base` has a directory holding a conflict, whose
334/// differing sides the scan keeps at the path while it removes the
335/// entries below (and a debug build fails an assertion). Such a file is
336/// first left untracked, which scans it as if it had been moved away and
337/// records the directory's removal; a second scan then records the file
338/// where nothing is, as a later run would once it was put back.
339async fn scan(
340 wc: &mut dyn LockedWorkingCopy,
341 root: &std::path::Path,
342 base: &MergedTree,
343 saved: &SavedState,
344 options: &SnapshotOptions<'_>,
345) -> Result<MergedTree, String> {
346 let files = files_on_conflicts(root, base, Some(saved))?;
347 let mut state = base.clone();
348 if !files.at.is_empty() {
349 // the empty file: a reset records the file at each path it changes
350 // as an empty one, not executable, modified at the epoch, so the
351 // scan reads any other file there, and keeps this one for such a file
352 let store = base.store();
353 let id = store.write_file(RepoPath::root(), &mut &[][..]).await.map_err(|e| e.to_string())?;
354 let copy_id = CopyId::placeholder();
355 let empty = Merge::normal(TreeValue::File { id, executable: false, copy_id });
356 state = reset_paths(wc, &state, files.at.into_iter().map(|path| (path, empty.clone()))).await?;
357 }
358 if !files.over.is_empty() {
359 let untracked = FilesMatcher::new(&files.over);
360 let start = DifferenceMatcher::new(options.start_tracking_matcher, &untracked);
361 let first = SnapshotOptions { start_tracking_matcher: &start, ..options.clone() };
362 state = scan_pass(wc, root, &state, &first).await?;
363 // a state that agrees with its tree tracks every entry below the
364 // directory, so none is left; were a conflict left below, the
365 // second scan would lose the file after all
366 if let Some(path) = files_on_conflicts(root, &state, None)?.over.first() {
367 return Err(format!(
368 "cannot record the file `{}` in place of a directory holding a conflict",
369 path.as_internal_file_string()
370 ));
371 }
372 }
373 scan_pass(wc, root, &state, options).await
376/// `wc.snapshot(options)`, where `state` is the tree the working copy's
377/// state records; return the tree scanned. jj's scan reads a directory a
378/// `.gitignore` ignores only at the paths the state tracks there, and where
379/// it cannot look at one of them because a directory above it has been
380/// replaced by a file, it fails instead of taking the path for deleted, as
381/// it takes one it does not find. The paths below a file are then dropped
382/// from the state, which records them as deleted, and the directory is
383/// scanned again (§7.4). A scan fails so while it walks the directory,
384/// before it changes anything in the state.
385async fn scan_pass(
386 wc: &mut dyn LockedWorkingCopy,
387 root: &std::path::Path,
388 state: &MergedTree,
389 options: &SnapshotOptions<'_>,
390) -> Result<MergedTree, String> {
391 let err = match wc.snapshot(options).await {
392 Ok((scanned, _stats)) => return Ok(scanned),
393 Err(err) => err,
394 };
395 let not_a_dir = |e: &std::io::Error| e.kind() == std::io::ErrorKind::NotADirectory;
396 let SnapshotError::Other { err: source, .. } = &err else {
397 return Err(err.to_string());
398 };
399 if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400 return Err(err.to_string());
401 }
402 // one `lstat` per path the state tracks, only once a scan has failed
403 let mut gone = Vec::new();
404 for (path, value) in state.entries() {
405 value.map_err(|e| e.to_string())?;
406 let Ok(disk) = path.to_fs_path(root) else {
407 continue;
408 };
409 if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410 gone.push((path, Merge::absent()));
411 }
412 }
413 if gone.is_empty() {
414 return Err(err.to_string());
415 }
416 reset_paths(wc, state, gone).await?;
417 let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
418 Ok(scanned)
421/// Give the working copy's state, which records `state`, the tree `state`
422/// with `values` at their paths, and return it; nothing in the working
423/// directory is read or written. A reset takes each path it changes for one
424/// the next scan is to read again.
425async fn reset_paths(
426 wc: &mut dyn LockedWorkingCopy,
427 state: &MergedTree,
428 values: impl IntoIterator<Item = (RepoPathBuf, jj_lib::backend::MergedTreeValue)>,
429) -> Result<MergedTree, String> {
430 let mut builder = MergedTreeBuilder::new(state.clone());
431 for (path, value) in values {
432 builder.set_or_remove(path, value);
433 }
434 let tree = builder.write_tree().await.map_err(|e| e.to_string())?;
435 // a reset reads only the commit's tree
436 let commit = with_tree(&state.store().root_commit(), tree.clone());
437 wc.reset(&commit).await.map_err(|e| e.to_string())?;
438 Ok(tree)
441/// Where the working directory holds a regular file that jj's scan reads
442/// into no tree, as `tree` has a conflict there it cannot write a file into
443struct FilesOnConflicts {
444 /// the conflicted paths with a side that is no file (a directory, a
445 /// symlink, a submodule), which a checkout writes as jj's description of
446 /// the sides, whose file was written since and describes no such
447 /// conflict
448 at: Vec<RepoPathBuf>,
449 /// the directories holding a conflict, that is those above a conflicted
450 /// path: a directory's sides differ exactly when a conflict is below
451 /// it, as jj writes no tree whose sides differ where every path resolves
452 over: Vec<RepoPathBuf>,
455/// Where the working copy saves its state, to read back what the state a
456/// scan's lock loaded records of a file (`files_on_conflicts`). Nothing is
457/// read until a scan asks.
458struct SavedState {
459 store: Arc<Store>,
460 root: PathBuf,
461 path: PathBuf,
462 settings: UserSettings,
465impl SavedState {
466 /// Where `ws` saves its working copy's state; taken before the lock,
467 /// which borrows `ws`
468 fn of(ws: &Workspace) -> SavedState {
469 let root = ws.workspace_root().to_owned();
470 let path = match ws.working_copy().downcast_ref::<LocalWorkingCopy>() {
471 Some(wc) => wc.state_path().to_owned(),
472 None => root.join(".jj").join("working_copy"),
473 };
474 SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
475 }
477 /// The state as saved, where it records `tree`, with the time it was
478 /// saved; None where it cannot be read or records another tree. A state
479 /// file that is not there is not read, as jj would write one to read.
480 fn load(&self, tree: &MergedTree) -> Option<(TreeState, i64)> {
481 let meta = self.path.join("tree_state").symlink_metadata().ok()?;
482 let saved_at = millis(meta.modified().ok()?)?;
483 let settings = TreeStateSettings::try_from_user_settings(&self.settings).ok()?;
484 let state = TreeState::load(self.store.clone(), self.root.clone(), self.path.clone(), &settings).ok()?;
485 (state.current_tree().tree_ids() == tree.tree_ids()).then_some((state, saved_at))
486 }
489/// A modification time in milliseconds since the epoch, as jj's working
490/// copy records one
491fn millis(time: std::time::SystemTime) -> Option<i64> {
492 match time.duration_since(std::time::UNIX_EPOCH) {
493 Ok(d) => i64::try_from(d.as_millis()).ok(),
494 Err(e) => i64::try_from(e.duration().as_millis()).ok().map(|m| -m),
495 }
498/// Whether the working copy's state, saved at `saved_at`, records `state`
499/// for the regular file `meta` describes as it is now: jj's own test of a
500/// file left as it was (`FileState::is_clean`, the executable bit aside),
501/// under which it reads nothing. A file modified in the millisecond the
502/// state was saved, or later, may have been written after it was.
503fn left_as_recorded(state: &FileState, meta: &std::fs::Metadata, saved_at: i64) -> bool {
504 matches!(state.file_type, FileType::Normal { .. })
505 && state.size == meta.len()
506 && meta.modified().ok().and_then(millis) == Some(state.mtime.0)
507 && state.mtime.0 < saved_at
510/// The terms of a conflict that `text`, jj's description of one, names:
511/// those removed and those added, each without the label a checkout may
512/// write after it, with each removed that is also added cancelled against
513/// it, as simplifying the conflict does, sorted; None for text that is no
514/// description. Two descriptions naming the same terms describe one
515/// conflict, whatever its labels or the sides it has lost since.
516fn described_terms(text: &[u8]) -> Option<(Vec<&str>, Vec<&str>)> {
517 let lines = std::str::from_utf8(text).ok()?.strip_prefix("Conflict:\n")?;
518 if !lines.is_empty() && !lines.ends_with('\n') {
519 return None;
520 }
521 let (mut removes, mut adds) = (Vec::new(), Vec::new());
522 for line in lines.split_terminator('\n') {
523 let (terms, term) = match (line.strip_prefix(" Removing "), line.strip_prefix(" Adding ")) {
524 (Some(term), _) => (&mut removes, term),
525 (_, Some(term)) => (&mut adds, term),
526 _ => return None,
527 };
528 // a term ends with its object's id, which holds no space
529 let id = term.find(" with id ")? + " with id ".len();
530 let end = term[id..].find(' ').map_or(term.len(), |n| id + n);
531 let (term, label) = term.split_at(end);
532 if !label.is_empty() && !(label.starts_with(" (") && label.ends_with(')')) {
533 return None;
534 }
535 terms.push(term);
536 }
537 removes.retain(|term| match adds.iter().position(|t| t == term) {
538 Some(n) => {
539 adds.swap_remove(n);
540 false
541 }
542 None => true,
543 });
544 removes.sort_unstable();
545 adds.sort_unstable();
546 Some((removes, adds))
549/// Look on disk at `tree`'s conflicts for `FilesOnConflicts`; with no
550/// `saved`, for `over` only. Nothing is looked at when `tree` is resolved;
551/// each directory above a conflict at most once, with one `lstat`; and a
552/// conflict with a side that is no file below directories, with one more,
553/// and a read where its file is as long as its description. Only a file
554/// that holds something else is looked up in the saved state, which is read
555/// once, and read itself where the state records it otherwise.
556fn files_on_conflicts(
557 root: &std::path::Path,
558 tree: &MergedTree,
559 saved: Option<&SavedState>,
560) -> Result<FilesOnConflicts, String> {
561 let mut files = FilesOnConflicts { at: Vec::new(), over: Vec::new() };
562 // whether each directory looked at so far is one on disk
563 let mut dirs: HashMap<RepoPathBuf, bool> = HashMap::new();
564 // the state as saved, read once a file differs from its description,
565 // and what it records of the files, taken once
566 let loaded: std::cell::OnceCell<Option<(TreeState, i64)>> = std::cell::OnceCell::new();
567 let recorded: std::cell::OnceCell<Option<(FileStates<'_>, i64)>> = std::cell::OnceCell::new();
568 for (path, value) in tree.conflicts() {
569 let value = value.map_err(|e| e.to_string())?;
570 let parent = path.parent().expect("a conflicted path has a parent");
571 let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
572 // from the top down, stopping at the first that is not a directory
573 let below_dirs = above.into_iter().rev().all(|dir| match dirs.get(dir) {
574 Some(&is_dir) => is_dir,
575 None => {
576 let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
577 if meta.as_ref().is_some_and(|m| m.is_file()) {
578 files.over.push(dir.to_owned());
579 }
580 let is_dir = meta.is_some_and(|m| m.is_dir());
581 dirs.insert(dir.to_owned(), is_dir);
582 is_dir
583 }
584 });
585 let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
586 continue;
587 };
588 let Some((disk, meta)) = path.to_fs_path(root).ok().and_then(|disk| {
589 let meta = disk.symlink_metadata().ok()?;
590 meta.is_file().then_some((disk, meta))
591 }) else {
592 continue;
593 };
594 // what jj's checkout writes for it (§7.4)
595 let written = value.describe(tree.labels());
596 let mut bytes = None;
597 if meta.len() == written.len() as u64 {
598 match std::fs::read(&disk) {
599 Ok(read) if read == written.as_bytes() => continue,
600 Ok(read) => bytes = Some(read),
601 Err(_) => continue,
602 }
603 }
604 // the file a checkout wrote, left as it was: it describes the
605 // conflict as the checkout had it, which the state may since
606 // record otherwise with no file written, as a scan that drops a
607 // pair of sides that cancel, or a reset that only relabels, does
608 let states = recorded.get_or_init(|| {
609 let (state, saved_at) = loaded.get_or_init(|| saved.load(tree)).as_ref()?;
610 Some((state.file_states(), *saved_at))
611 });
612 if states.as_ref().is_some_and(|(file_states, saved_at)| {
613 file_states.get(&path).is_some_and(|state| left_as_recorded(&state, &meta, *saved_at))
614 }) {
615 continue;
616 }
617 // written since, or not known to be left: the conflict is kept
618 // while the file describes it, as when an editor saves it unchanged
619 let Some(bytes) = bytes.or_else(|| std::fs::read(&disk).ok()) else {
620 continue;
621 };
622 let unlabelled = value.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
623 let terms = described_terms(unlabelled.as_bytes());
624 if terms.is_none() || described_terms(&bytes) != terms {
625 files.at.push(path);
626 }
627 }
628 Ok(files)
631/// The tree a snapshot records in the working-copy commit, whose tree is
632/// `wc`, where the working copy's state records `state` and a scan of the
633/// working directory found `scanned` (§7.4): `scanned` itself where the
634/// state is the commit's, and also where it is `current`, saved at the
635/// operation the repository is at, which jj takes for the files last
636/// checked out whatever it records: one lost, which jj makes again
637/// recording no file, or one put in its place. Such a state may record none
638/// of the commit's conflicts, so a file holding what a checkout wrote for
639/// one is read back as it there (`conflicts_written`). Otherwise the state
640/// is stale (a checkout that failed or was cut short after its operation
641/// was recorded, a `fetch` that rebased the commit, jj run with
642/// `--ignore-working-copy` or from another workspace), and what the
643/// directory holds is a change to `state` rather than to the commit: that
644/// change is replayed onto the commit, a three-way merge with `state` as
645/// the base, in which a path both changed differently is a conflict, but
646/// for one setting a directory against anything else, which takes what the
647/// working directory holds there, as where the state is current. What a
648/// checkout cut short wrote is the commit's own content, which the merge
649/// takes as it is: where the directory holds what the commit does, the base
650/// is taken to hold it too, so that a conflict the state records there,
651/// whose sides would not cancel against the commit's, does not make one of
652/// a change both made. That includes a conflict the checkout wrote, which
653/// the scan read as its text (`conflicts_written`).
654async fn snapshot_tree(
655 root: &std::path::Path,
656 markers: ConflictMarkerStyle,
657 wc: &MergedTree,
658 state: &MergedTree,
659 current: bool,
660 scanned: &MergedTree,
661) -> Result<MergedTree, String> {
662 if state.tree_ids() == wc.tree_ids() {
663 return Ok(scanned.clone());
664 }
665 if current {
666 return conflicts_written(root, markers, wc, state, scanned).await;
667 }
668 if scanned.tree_ids() == state.tree_ids() {
669 return Ok(wc.clone());
670 }
671 let scanned = &conflicts_written(root, markers, wc, state, scanned).await?;
672 let err = |e: jj_lib::backend::BackendError| e.to_string();
673 // the paths where the directory holds something other than the commit
674 let mut apart = std::collections::HashSet::new();
675 let mut diff = wc.diff_stream(scanned, &EverythingMatcher);
676 while let Some(entry) = diff.next().await {
677 entry.values.map_err(err)?;
678 apart.insert(entry.path);
679 }
680 // the base: the state, but for what the directory changed as the
681 // commit did
682 let mut base = MergedTreeBuilder::new(state.clone());
683 let mut same = false;
684 let mut diff = state.diff_stream(scanned, &EverythingMatcher);
685 while let Some(entry) = diff.next().await {
686 let Diff { after, .. } = entry.values.map_err(err)?;
687 if !apart.contains(&entry.path) {
688 base.set_or_remove(entry.path, after);
689 same = true;
690 }
691 }
692 let base = if same { base.write_tree().await.map_err(err)? } else { state.clone() };
693 let merged = MergedTree::merge(Merge::from_removes_adds(
694 vec![(base.clone(), "last checkout".to_string())],
695 vec![
696 (wc.clone(), "working-copy commit".to_string()),
697 (scanned.clone(), "working directory".to_string()),
698 ],
699 ))
700 .await
701 .map_err(err)?;
702 // A conflict setting a directory against a file, a symlink or nothing,
703 // which jj keeps at the path itself, cannot be a blob that lists the
704 // directory's entries (§7.3), and a checkout would write jj's
705 // description of it in place of what the directory holds there,
706 // removing a directory's files: where the directory changed the path,
707 // take what it holds, as a snapshot over a current state does. The
708 // commit's change there is left in the operation before. Where the
709 // directory holds what the base does, the merge took the commit's value
710 // as it is, so such a conflict is one the commit already has, and it
711 // stays. A tree with no conflict is not walked.
712 let mut kept = MergedTreeBuilder::new(merged.clone());
713 let mut mixed = false;
714 for (path, value) in merged.conflicts() {
715 if value.map_err(err)?.iter().any(|term| matches!(term, Some(TreeValue::Tree(_)))) {
716 let held = scanned.path_value(&path).await.map_err(err)?;
717 if held == base.path_value(&path).await.map_err(err)? {
718 continue;
719 }
720 kept.set_or_remove(path, held);
721 mixed = true;
722 }
723 }
724 if !mixed {
725 return Ok(merged);
726 }
727 kept.write_tree().await.map_err(err)
730/// `scanned` with the commit's conflict at each path the working-copy state
731/// records something else at, where the file holds what jj's checkout
732/// writes for that conflict (`holds_written`): its markers, or its
733/// description where a side is no file (§7.4). A checkout records the
734/// conflict in the state as it writes the file, so a scan reads such a file
735/// back as the conflict; one cut short, or one over a stale state, records
736/// nothing, nor does a state made again where it was lost, and the scan
737/// reads the file as the text it holds. Each of the commit's conflicts is
738/// looked at, and a file read, only where neither the state nor the scan
739/// has the conflict.
740async fn conflicts_written(
741 root: &std::path::Path,
742 markers: ConflictMarkerStyle,
743 wc: &MergedTree,
744 state: &MergedTree,
745 scanned: &MergedTree,
746) -> Result<MergedTree, String> {
747 let err = |e: jj_lib::backend::BackendError| e.to_string();
748 let mut tree = MergedTreeBuilder::new(scanned.clone());
749 let mut found = false;
750 for (path, value) in wc.conflicts() {
751 let value = value.map_err(err)?;
752 if state.path_value(&path).await.map_err(err)? == value
753 || scanned.path_value(&path).await.map_err(err)? == value
754 {
755 continue;
756 }
757 let Ok(disk) = path.to_fs_path(root) else {
758 continue;
759 };
760 if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761 tree.set_or_remove(path, value);
762 found = true;
763 }
764 }
765 if !found {
766 return Ok(scanned.clone());
767 }
768 tree.write_tree().await.map_err(err)
771/// Whether `disk`, the entry at `path`, holds what a checkout writes there
772/// for `value`: a file's content or a symlink's target, whatever its
773/// executable bit, or, for a conflict, exactly its markers, compared byte
774/// for byte (j's settings convert no line endings), or, where a side is no
775/// file, a description of the same sides, whatever their labels and less
776/// any it both removes and adds, as a scan reads one back
777/// (`files_on_conflicts`); nothing is written to the store
778async fn holds_written(
779 store: &Arc<Store>,
780 path: &RepoPath,
781 value: jj_lib::backend::MergedTreeValue,
782 labels: &jj_lib::conflict_labels::ConflictLabels,
783 markers: ConflictMarkerStyle,
784 disk: &std::path::Path,
785) -> Result<bool, String> {
786 use jj_lib::conflicts::{self, MaterializedTreeValue};
787 let symlink = matches!(value.as_normal(), Some(TreeValue::Symlink(_)));
788 let Some(meta) = disk.symlink_metadata().ok().filter(|m| if symlink { m.is_symlink() } else { m.is_file() })
789 else {
790 return Ok(false);
791 };
792 let written = conflicts::materialize_tree_value(store, path, value, labels)
793 .await
794 .map_err(|e| e.to_string())?;
795 match written {
796 MaterializedTreeValue::Symlink { target, .. } => {
797 Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
798 }
799 MaterializedTreeValue::File(mut file) => {
800 let bytes = file.read_all(path).await.map_err(|e| e.to_string())?;
801 Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
802 }
803 MaterializedTreeValue::FileConflict(file) => {
804 let options = conflicts::ConflictMaterializeOptions {
805 marker_style: markers,
806 marker_len: Some(conflicts::choose_materialized_conflict_marker_len(&file.contents)),
807 merge: store.merge_options().clone(),
808 };
809 let bytes: Vec<u8> =
810 conflicts::materialize_merge_result_to_bytes(&file.contents, &file.labels, &options).into();
811 Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
812 }
813 MaterializedTreeValue::OtherConflict { id, .. } => {
814 let unlabelled = id.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
815 let terms = described_terms(unlabelled.as_bytes());
816 Ok(terms.is_some() && std::fs::read(disk).is_ok_and(|b| described_terms(&b) == terms))
817 }
818 _ => Ok(false),
819 }
822/// How the working copy writes a conflict, as `settings` say
823fn marker_style(settings: &UserSettings) -> Result<ConflictMarkerStyle, String> {
824 Ok(TreeStateSettings::try_from_user_settings(settings).map_err(|e| e.to_string())?.conflict_marker_style)
827/// The first of the directories between the root and `path`, from the top
828/// down, that is no directory on disk, with the type of what stands there
829/// (none where nothing does, or it cannot be looked at), or none if each
830/// is: a symlink is not one, and is not followed, as no directory below
831/// one is looked at. Each directory not looked at before costs one
832/// `lstat`, and what it found is kept in `dirs`.
833fn first_not_a_dir(
834 root: &std::path::Path,
835 path: &RepoPath,
836 dirs: &mut HashMap<RepoPathBuf, Option<std::fs::FileType>>,
837) -> Option<(RepoPathBuf, Option<std::fs::FileType>)> {
838 let parent = path.parent()?;
839 let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
840 for dir in above.into_iter().rev() {
841 let stands = match dirs.get(dir) {
842 Some(&stands) => stands,
843 None => {
844 let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
845 let stands = meta.map(|m| m.file_type());
846 dirs.insert(dir.to_owned(), stands);
847 stands
848 }
849 };
850 if !stands.is_some_and(|t| t.is_dir()) {
851 return Some((dir.to_owned(), stands));
852 }
853 }
854 None
857/// The first path where the working directory, scanned again as
858/// `rescanned`, no longer holds what it held as `on_disk`, the run's
859/// snapshot of it, and a checkout from `on_disk` to `to` writes or removes
860/// a file: a change made while the program ran, which no snapshot holds and
861/// jj's checkout would write over without looking (§7.5 step 7). Paths are
862/// compared file by file, as a directory is by what it holds; one lookup
863/// in `to` per path the directory changed at.
864async fn written_over(
865 on_disk: &MergedTree,
866 rescanned: &MergedTree,
867 to: &MergedTree,
868) -> Result<Option<RepoPathBuf>, String> {
869 let err = |e: jj_lib::backend::BackendError| e.to_string();
870 let mut diff = on_disk.diff_stream(rescanned, &EverythingMatcher);
871 while let Some(entry) = diff.next().await {
872 let Diff { before, .. } = entry.values.map_err(err)?;
873 // a diff has no tree on either side
874 let focus = to.path_value(&entry.path).await.map_err(err)?;
875 let focus = if focus.is_tree() { Merge::absent() } else { focus };
876 if focus != before {
877 return Ok(Some(entry.path));
878 }
879 }
880 Ok(None)
883/// What stands in the way of a path a checkout adds: `holder`, in the
884/// working directory, which does not track it, at `path`, above it, or in
885/// a directory at it
886struct InTheWay {
887 path: RepoPathBuf,
888 holder: RepoPathBuf,
891impl InTheWay {
892 /// Whether it is a directory at `path`, holding `holder`
893 fn is_dir(&self) -> bool {
894 self.holder != self.path && self.holder.starts_with(&self.path)
895 }
897 /// What a refused checkout says of it among others: a directory by its
898 /// own path, with what it holds, anything else by its path
899 fn named(&self) -> String {
900 if self.is_dir() {
901 format!(
902 "`{}` (a directory holding untracked files, such as `{}`)",
903 self.path.as_internal_file_string(),
904 self.holder.as_internal_file_string()
905 )
906 } else {
907 format!("`{}`", self.holder.as_internal_file_string())
908 }
909 }
912/// Why a checkout is refused over `in_the_way`, which is not empty, as
913/// git names each untracked file in the way: one with the path the focus
914/// has there, more by listing the first ten and counting the rest
915fn in_the_way_of(in_the_way: &[InTheWay]) -> String {
916 const LISTED: usize = 10;
917 match in_the_way {
918 [one] if one.is_dir() => format!(
919 "it has `{0}`, and `{0}` in the working directory is a directory holding untracked files, such as `{1}`; move it aside",
920 one.path.as_internal_file_string(),
921 one.holder.as_internal_file_string()
922 ),
923 [one] => format!(
924 "it has `{}`, and an untracked `{}` in the working directory is in its way; move it aside",
925 one.path.as_internal_file_string(),
926 one.holder.as_internal_file_string()
927 ),
928 _ => {
929 let mut named: Vec<String> = in_the_way.iter().take(LISTED).map(InTheWay::named).collect();
930 let last = match in_the_way.len() - named.len() {
931 0 => named.pop().unwrap_or_default(),
932 more => format!("{} more", more),
933 };
934 format!(
935 "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
936 named.join(", "),
937 last
938 )
939 }
940 }
943/// What stands in the working directory where a checkout from `from`, the
944/// tree the directory holds as scanned, to `to` adds a file (or a symlink,
945/// or a conflict): at each path where `to` has one and `from` has none.
946/// jj's checkout writes nothing over anything already there, and skips
947/// the path without failing, recording it as written, so the next scan
948/// would read what stands there into the focus, an ignored file's content
949/// in place of the focus's, or take the focus's file for deleted (§7.4).
950/// What `from` tracks there the checkout removes first; trees of empty
951/// directories, which no scan sees, and a file holding just what the
952/// checkout writes there (`holds_written`), as a checkout cut short leaves
953/// at an ignored path, are cleared for it (`clear_the_way`). The result is
954/// the paths where nothing else stands, or, walking the whole diff all the
955/// same, each thing standing where anything else does (`InTheWay`), in
956/// path order, what stands above several paths once. No symlink is
957/// followed. Each path costs one `lstat`, and each directory above one not
958/// looked at before one more; a file standing at a path is read, and a
959/// directory read until something untracked is found in it, each entry in
960/// it that is no directory looked up in `from`.
961async fn look_where_added(
962 root: &std::path::Path,
963 markers: ConflictMarkerStyle,
964 from: &MergedTree,
965 to: &MergedTree,
966) -> Result<Result<Vec<RepoPathBuf>, Vec<InTheWay>>, String> {
967 let err = |e: jj_lib::backend::BackendError| e.to_string();
968 let mut dirs = HashMap::new();
969 // each thing that is no directory looked up in `from`, above a path
970 let mut looked_up = std::collections::HashSet::new();
971 let mut clear = Vec::new();
972 let mut in_the_way = Vec::new();
973 let mut diff = from.diff_stream(to, &EverythingMatcher);
974 while let Some(entry) = diff.next().await {
975 let Diff { before, after } = entry.values.map_err(err)?;
976 // a submodule's directory is the submodule's own, which jj keeps
977 if before.is_present() || after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
978 continue;
979 }
980 let path = entry.path;
981 if let Some((dir, stands)) = first_not_a_dir(root, &path, &mut dirs) {
982 // where nothing stands above the path, or what `from` tracks
983 // and the checkout removes, nothing stands at it
984 if stands.is_some() && looked_up.insert(dir.clone()) {
985 let is_tracked = from.path_value(&dir).await.map_err(err)?.is_present();
986 if !is_tracked {
987 in_the_way.push(InTheWay { path, holder: dir });
988 }
989 }
990 continue;
991 }
992 let Ok(disk) = path.to_fs_path(root) else {
993 continue;
994 };
995 let Ok(meta) = disk.symlink_metadata() else {
996 continue;
997 };
998 let holder = if meta.is_dir() {
999 untracked_below(&disk, &path, from)
1000 } else if holds_written(to.store(), &path, after, to.labels(), markers, &disk).await? {
1001 None
1002 } else {
1003 Some(path.clone())
1004 };
1005 match holder {
1006 Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007 None => clear.push(path),
1010 if in_the_way.is_empty() {
1011 Ok(Ok(clear))
1012 } else {
1013 Ok(Err(in_the_way))
1017/// Something that is no directory in the directory `disk`, standing at
1018/// `path`, or in a directory in it, that `from` does not track, or a
1019/// directory that cannot be read; none where it holds nothing else. Entries
1020/// are told apart without following a symlink, and `from` is read below
1021/// `path` only once there is something to look up.
1022fn untracked_below(disk: &std::path::Path, path: &RepoPath, from: &MergedTree) -> Option<RepoPathBuf> {
1023 let mut tracked: Option<std::collections::HashSet<RepoPathBuf>> = None;
1024 let mut dirs = vec![(disk.to_owned(), path.to_owned())];
1025 while let Some((dir, at)) = dirs.pop() {
1026 let Ok(entries) = std::fs::read_dir(&dir) else {
1027 return Some(at);
1028 };
1029 for entry in entries {
1030 let Ok(entry) = entry else {
1031 return Some(at);
1032 };
1033 let name = entry.file_name();
1034 // a name no path of a tree holds
1035 let Some(below) = name.to_str().and_then(|n| RepoPathComponent::new(n).ok()).map(|c| at.join(c)) else {
1036 return Some(at);
1037 };
1038 if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039 dirs.push((entry.path(), below));
1040 continue;
1042 let tracked = tracked.get_or_insert_with(|| {
1043 let under = jj_lib::matchers::PrefixMatcher::new([path]);
1044 from.entries_matching(&under).filter(|(_, value)| value.is_ok()).map(|(p, _)| p).collect()
1045 });
1046 if !tracked.contains(&below) {
1047 return Some(below);
1051 None
1054/// Clear each of `paths`, where a checkout from the tree the working
1055/// directory holds to `to` adds a file (`look_where_added`): a file there
1056/// is moved aside while it holds what the checkout writes
1057/// (`holds_written`), and in a directory there every directory holding no
1058/// file is removed, deepest first, and it too once empty. A path below
1059/// anything that is no directory on disk, a symlink included, is left
1060/// alone, and no symlink in one is followed; a directory is removed only
1061/// while empty, so a file put there meanwhile stays, as does one changed
1062/// meanwhile, and the checkout skips the path (§7.5 step 7). So does one
1063/// that cannot be moved, which the result names.
1064async fn clear_the_way(
1065 root: &std::path::Path,
1066 markers: ConflictMarkerStyle,
1067 to: &MergedTree,
1068 paths: &[RepoPathBuf],
1069) -> Aside {
1070 fn clear(dir: &std::path::Path) {
1071 if let Ok(entries) = std::fs::read_dir(dir) {
1072 for entry in entries.flatten() {
1073 if entry.file_type().is_ok_and(|t| t.is_dir()) {
1074 clear(&entry.path());
1078 let _ = std::fs::remove_dir(dir);
1080 let mut aside = Aside { root: root.to_owned(), dir: None, stuck: None, returned: false };
1081 let mut dirs = HashMap::new();
1082 for path in paths {
1083 if first_not_a_dir(root, path, &mut dirs).is_some() {
1084 continue;
1086 let Ok(disk) = path.to_fs_path(root) else {
1087 continue;
1088 };
1089 let Ok(meta) = disk.symlink_metadata() else {
1090 continue;
1091 };
1092 if meta.is_dir() {
1093 clear(&disk);
1094 continue;
1096 let Ok(value) = to.path_value(path).await else {
1097 continue;
1098 };
1099 if holds_written(to.store(), path, value, to.labels(), markers, &disk).await.unwrap_or(false) {
1100 if let Err(e) = aside.take(path, &disk) {
1101 aside.stuck.get_or_insert((path.clone(), e));
1105 aside
1108/// The start of the name of each directory in `.jj` that `Aside` moves
1109/// files into
1110const ASIDE: &str = "aside";
1112/// The files `clear_the_way` moved out of a checkout's way, each holding
1113/// what the checkout writes where it stood, in a directory in `.jj` that
1114/// holds each at its path in the working directory `root`, which no scan
1115/// reads; the first file it could not move, with why; and whether the
1116/// checkout returned rather than failing, and so wrote in full each file
1117/// it wrote. Dropped, once the checkout has written what it could, it puts
1118/// each back (`put_back`) where nothing stands now, as where a checkout
1119/// that failed or skipped paths did not get to write, and, unless the
1120/// checkout returned, where a start of it does, as where it was cut short
1121/// writing it, and removes the directory with the rest: each holds just
1122/// what the focus has at its path, which the repository keeps, and what
1123/// stands there, what the checkout wrote or something put there since,
1124/// stays (§7.5 step 7). One a run killed meanwhile left, the next puts
1125/// back (`put_back_left`).
1126struct Aside {
1127 root: std::path::PathBuf,
1128 dir: Option<tempfile::TempDir>,
1129 stuck: Option<(RepoPathBuf, std::io::Error)>,
1130 returned: bool,
1133impl Aside {
1134 /// Move `disk`, the file at `path`, into the directory, made on first
1135 /// use; renamed, it is neither copied nor followed if a symlink
1136 fn take(&mut self, path: &RepoPath, disk: &std::path::Path) -> std::io::Result<()> {
1137 let dir = match &mut self.dir {
1138 Some(dir) => dir,
1139 none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
1140 };
1141 let to = path.to_fs_path(dir.path()).map_err(std::io::Error::other)?;
1142 if let Some(parent) = to.parent() {
1143 std::fs::create_dir_all(parent)?;
1145 std::fs::rename(disk, to)
1149impl Drop for Aside {
1150 fn drop(&mut self) {
1151 if let Some(dir) = self.dir.take() {
1152 put_back(&self.root, dir.path(), !self.returned);
1157/// Put each file in `aside`, where `Aside` moved it, back at its path in
1158/// the working directory `root` where nothing stands, making the
1159/// directories above it that are missing, and, where the checkout may have
1160/// been `cut` short, where a start of it stands, less than all of it, as a
1161/// checkout cut short while writing it leaves (`cut_short`), which it
1162/// replaces; one that cannot be put back, as something else stands there
1163/// or in place of a directory above it, stays in `aside`. No symlink is
1164/// followed.
1165fn put_back(root: &std::path::Path, aside: &std::path::Path, cut: bool) {
1166 let missing = |p: &std::path::Path| {
1167 matches!(p.symlink_metadata(), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
1168 };
1169 // whether each directory above `path` in `root` is one, or is made
1170 let dirs_above = |path: &std::path::Path| {
1171 let mut at = root.to_owned();
1172 for name in path.parent().into_iter().flat_map(|p| p.components()) {
1173 at.push(name);
1174 let made = match at.symlink_metadata() {
1175 Ok(meta) => meta.is_dir(),
1176 Err(e) => e.kind() == std::io::ErrorKind::NotFound && std::fs::create_dir(&at).is_ok(),
1177 };
1178 if !made {
1179 return false;
1182 true
1183 };
1184 let mut dirs = vec![std::path::PathBuf::new()];
1185 while let Some(dir) = dirs.pop() {
1186 let Ok(entries) = std::fs::read_dir(aside.join(&dir)) else {
1187 continue;
1188 };
1189 for entry in entries.flatten() {
1190 let path = dir.join(entry.file_name());
1191 if entry.file_type().is_ok_and(|t| t.is_dir()) {
1192 dirs.push(path);
1193 continue;
1195 let disk = root.join(&path);
1196 // the directories above are looked at first, so that nothing
1197 // is looked at through a symlink
1198 if dirs_above(&path) && (missing(&disk) || (cut && cut_short(&disk, &entry.path()))) {
1199 let _ = std::fs::rename(entry.path(), disk);
1205/// Whether `disk` is a file holding a start of `whole`, a file moved
1206/// aside, and less than all of it, or, where `whole` is a symlink, an
1207/// empty file: what jj's checkout leaves where it was cut short (a full
1208/// disk, a run killed) while it wrote `whole` there, as it makes a file
1209/// empty before it writes it, and tries a symlink's path with an empty
1210/// file first. Reads no more of either than `disk` holds. No symlink is
1211/// followed.
1212fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1213 use std::io::Read;
1214 use std::os::unix::fs::OpenOptionsExt;
1215 let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1216 return false;
1217 };
1218 if !stands.is_file() {
1219 return false;
1221 if copy.is_symlink() {
1222 return stands.len() == 0;
1224 if !copy.is_file() || stands.len() >= copy.len() {
1225 return false;
1227 let stands_file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW).open(disk);
1228 let (Ok(mut stands_file), Ok(mut copy_file)) = (stands_file, std::fs::File::open(whole)) else {
1229 return false;
1230 };
1231 let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232 let mut rest = stands.len();
1233 while rest > 0 {
1234 let n = rest.min(a.len() as u64) as usize;
1235 let same = stands_file.read_exact(&mut a[..n]).is_ok()
1236 && copy_file.read_exact(&mut b[..n]).is_ok()
1237 && a[..n] == b[..n];
1238 if !same {
1239 return false;
1241 rest -= n as u64;
1243 true
1246/// Put back what a run killed while its checkout ran left moved aside in
1247/// `.jj` (`Aside`), over a write the kill may have cut short too, and
1248/// remove the directory it left there. A run does this once it holds the
1249/// repository's lock, before it looks at the working directory, so no
1250/// other run is using such a directory.
1251fn put_back_left(root: &std::path::Path) {
1252 let Ok(entries) = std::fs::read_dir(root.join(".jj")) else {
1253 return;
1254 };
1255 for entry in entries.flatten() {
1256 let named = entry.file_name().to_str().is_some_and(|n| n.starts_with(ASIDE));
1257 if named && entry.file_type().is_ok_and(|t| t.is_dir()) {
1258 put_back(root, &entry.path(), true);
1259 let _ = std::fs::remove_dir_all(entry.path());
1264/// The first path a checkout from `from` to `to` that skipped some left
1265/// holding something other than what it writes there: with no directory
1266/// above it, or a directory at it, or, where it adds the path, anything but
1267/// what it writes (`holds_written`); none if no such path is found. Only
1268/// once a checkout has skipped a path is the directory looked at again,
1269/// reading the files it added until one differs.
1270async fn first_skipped(
1271 root: &std::path::Path,
1272 markers: ConflictMarkerStyle,
1273 from: &MergedTree,
1274 to: &MergedTree,
1275) -> Result<Option<RepoPathBuf>, String> {
1276 let mut dirs = HashMap::new();
1277 let mut diff = from.diff_stream(to, &EverythingMatcher);
1278 while let Some(entry) = diff.next().await {
1279 let Diff { before, after } = entry.values.map_err(|e| e.to_string())?;
1280 if after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
1281 continue;
1283 let path = entry.path;
1284 if let Some((_, stands)) = first_not_a_dir(root, &path, &mut dirs) {
1285 if stands.is_some() {
1286 return Ok(Some(path));
1288 continue;
1290 let Ok(disk) = path.to_fs_path(root) else {
1291 continue;
1292 };
1293 let Ok(meta) = disk.symlink_metadata() else {
1294 continue;
1295 };
1296 if meta.is_dir()
1297 || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1299 return Ok(Some(path));
1302 Ok(None)
1305/// Save the working copy's state as a scan that found `scanned` left it:
1306/// at `head`, the operation the repository is at, where that is the
1307/// working-copy commit's tree `wc`; otherwise at the operation it was
1308/// saved at before, so that it stays stale for jj too, which takes a state
1309/// saved at the head for a current one and would record the directory
1310/// into the commit whole (§7.4)
1311async fn save_scan(
1312 mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313 scanned: &MergedTree,
1314 wc: &MergedTree,
1315 head: &OperationId,
1316) -> Result<(), String> {
1317 let op_id = if scanned.tree_ids() == wc.tree_ids() {
1318 head.clone()
1319 } else {
1320 locked_ws.locked_wc().old_operation_id().clone()
1321 };
1322 locked_ws.finish(op_id).await.map_err(|e| e.to_string())
1325/// The signal that asked the run to stop while its checkout was not to be
1326/// cut short (`Deferral`), or 0
1327static DEFERRED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1329extern "C" fn defer(sig: libc::c_int) {
1330 DEFERRED.store(sig, std::sync::atomic::Ordering::SeqCst);
1333/// While it lives, SIGINT, SIGTERM, SIGHUP and SIGQUIT, whichever thread
1334/// gets them, only note that they came: it lives from the operation's
1335/// publishing until the working copy's state is saved, so that a checkout
1336/// the user interrupts completes, leaving the working copy current, and
1337/// through a clone, which one that comes before the working-copy commit is
1338/// made fails (`unless_interrupted`), so that it removes what it made
1339/// (§1.3). A signal the process ignores stays ignored, as it does for the
1340/// git a clone runs (`nohup j clone`, a background job). Dropping it gives
1341/// each signal back what it did before, and `raise_deferred` then acts on
1342/// one that came.
1343struct Deferral {
1344 previous: Vec<(libc::c_int, libc::sigaction)>,
1347impl Deferral {
1348 fn start() -> Deferral {
1349 let mut previous = Vec::new();
1350 for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1351 // SAFETY: the handler only stores to an atomic, which is
1352 // async-signal-safe; both actions are initialised before use
1353 unsafe {
1354 let mut old: libc::sigaction = std::mem::zeroed();
1355 if libc::sigaction(sig, std::ptr::null(), &mut old) != 0
1356 || old.sa_sigaction == libc::SIG_IGN
1358 continue;
1360 let mut action: libc::sigaction = std::mem::zeroed();
1361 action.sa_sigaction = defer as extern "C" fn(libc::c_int) as libc::sighandler_t;
1362 action.sa_flags = libc::SA_RESTART;
1363 libc::sigemptyset(&mut action.sa_mask);
1364 if libc::sigaction(sig, &action, std::ptr::null_mut()) == 0 {
1365 previous.push((sig, old));
1369 Deferral { previous }
1373impl Drop for Deferral {
1374 fn drop(&mut self) {
1375 for (sig, old) in &self.previous {
1376 // SAFETY: `old` is the action `sigaction` gave back for `sig`
1377 unsafe { libc::sigaction(*sig, old, std::ptr::null_mut()) };
1382/// Act on a signal a checkout or a clone deferred (§1.3) as it would have
1383/// acted then: with its default action, stopping the process (`Deferral`
1384/// defers no signal the process ignores). Called once the run has reported
1385/// what it did.
1386pub fn raise_deferred() {
1387 let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
1388 if sig != 0 {
1389 // a process a signal stops does not flush what it printed
1390 use std::io::Write;
1391 let _ = std::io::stdout().flush();
1392 // SAFETY: raising a signal whose action `Deferral` gave back
1393 unsafe { libc::raise(sig) };
1397/// `done`, the outcome of a step of a clone, unless a signal to stop came
1398/// while `cmd_clone`'s `Deferral` held it off: the clone then fails, before
1399/// it records anything, so that it removes what it made (§1.3, §7.8). A
1400/// Ctrl-C at the terminal stops the git the step runs too, which fails the
1401/// step itself: `j` may take the signal only after that, and then reports
1402/// the step's failure.
1403fn unless_interrupted<T>(done: Result<T, OpenError>) -> Result<T, OpenError> {
1404 if DEFERRED.load(std::sync::atomic::Ordering::SeqCst) != 0 {
1405 Err((1, "clone interrupted".to_string()))
1406 } else {
1407 done
1411/// `commit` with `tree` in place of its own, for the working copy to read:
1412/// a checkout and a reset read only a commit's tree. It is never written.
1413fn with_tree(commit: &Commit, tree: MergedTree) -> Commit {
1414 let (tree_ids, labels) = tree.into_tree_ids_and_labels();
1415 Commit::new(
1416 commit.store().clone(),
1417 commit.id().clone(),
1418 Arc::new(jj_lib::backend::Commit {
1419 root_tree: tree_ids,
1420 conflict_labels: labels.into_merge(),
1421 ..commit.store_commit().as_ref().clone()
1422 }),
1426#[derive(Clone)]
1427pub struct JjBackend {
1428 inner: Arc<JjInner>,
1429 replay_cache: Rc<ReplayCache>,
1432impl JjBackend {
1433 pub fn open(cfg: Option<&Config>) -> Result<JjBackend, OpenError> {
1434 let root = find_repo_root().ok_or_else(|| {
1436 2,
1437 "no jj repository found in this directory or any parent; run `j init` here, `j clone URL`, or `j help`".to_string(),
1439 })?;
1440 let settings = user_settings(cfg);
1441 let workspace = Workspace::load(
1442 &settings,
1443 &root,
1444 &default_backend_factories(),
1445 &default_working_copy_factories(),
1447 .map_err(|e| (2, format!("cannot load the workspace: {}", e)))?;
1448 // only the default workspace is supported (§7.1)
1449 if workspace.workspace_name() != WorkspaceName::DEFAULT {
1450 return Err((
1451 2,
1452 format!(
1453 "this working copy belongs to workspace `{}`; only the default workspace is supported",
1454 workspace.workspace_name().as_str()
1455 ),
1456 ));
1458 // Snapshot and checkout both apply patterns saved by `jj sparse`,
1459 // independently of our SnapshotOptions. Accepting a partial working
1460 // copy would silently omit disk edits and skip files during checkout.
1461 let sparse = workspace.working_copy().sparse_patterns()
1462 .map_err(|e| (2, format!("cannot read the working copy's sparse patterns: {}", e)))?;
1463 if !sparse.iter().any(|path| path.is_root()) {
1464 return Err((
1465 2,
1466 "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
1467 ));
1469 let workspace_name = workspace.workspace_name().to_owned();
1470 let repo = block_on(workspace.repo_loader().load_at_head())
1471 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
1472 Ok(JjBackend {
1473 replay_cache: Rc::new(ReplayCache::default()),
1474 inner: Arc::new(JjInner {
1475 workspace_root: root,
1476 workspace: Mutex::new(workspace),
1477 repo: Mutex::new(repo),
1478 workspace_name,
1479 visible: Mutex::new(None),
1480 lock_guard: Mutex::new(None),
1481 pending: Mutex::new(None),
1482 folds_case: OnceLock::new(),
1483 long_names: Mutex::new(HashMap::new()),
1484 }),
1485 })
1488 fn current_repo(&self) -> Arc<ReadonlyRepo> {
1489 self.inner.repo.lock().unwrap().clone()
1492 pub fn take_lock(&self) {
1493 // an flock, not a lockfile: the OS releases it when the process dies,
1494 // so a killed j (SIGINT, §1.3) never bricks the repository
1495 let lock_path = self.inner.workspace_root.join(".jj").join("j.lock");
1496 let file = match std::fs::OpenOptions::new()
1497 .create(true)
1498 .write(true)
1499 .open(&lock_path)
1501 Ok(f) => f,
1502 Err(e) => {
1503 crate::terminal::report_error(format_args!("j: cannot create {}: {}", lock_path.display(), e));
1504 std::process::exit(2);
1506 };
1507 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
1508 if rc != 0 {
1509 crate::terminal::report_error(format_args!("j: another j is running in this repository"));
1510 std::process::exit(2);
1512 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513 put_back_left(&self.inner.workspace_root);
1516 pub fn build_interp(
1517 &self,
1518 cfg: &mut Config,
1519 _text: &str,
1520 snapshot: bool,
1521 ) -> Result<(Interp, Value, Value), OpenError> {
1522 // reload at head: a previous run (or another tool) may have advanced
1523 // the operation since `open`
1524 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
1525 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
1526 *self.inner.repo.lock().unwrap() = head_repo;
1527 let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
1528 let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
1530 let current = if snapshot {
1531 // §7.4: snapshot the working directory into the wc commit; the
1532 // snapshot operation is written but unpublished — a persisting
1533 // run folds it into its own operation (§7.7)
1534 let (post, pending) = block_on(self.snapshot_repo())?;
1535 match pending {
1536 // no working-copy change: the repo is the one already loaded,
1537 // so reuse its value instead of rebuilding it
1538 None => {
1539 *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
1540 loaded.clone()
1542 Some(pending) => {
1543 *self.inner.pending.lock().unwrap() = Some(pending);
1544 let vis = block_on(read_visible(&post))?;
1545 let cur = build_repo_value(&vis, &vis.wc_change_id, &self.replay_cache)?;
1546 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
1547 cur
1550 } else {
1551 *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
1552 loaded.clone()
1553 };
1555 let mut interp = Interp::new(
1556 Rc::new(self.clone()),
1557 cfg.shapes.clone(),
1558 crate::value::Env::empty(),
1559 );
1560 // §1.2.5: resolve the config's `@prefix` literals now that the visible
1561 // ids are known, and before anything evaluates a definition
1562 if let Some((def, prefix, _)) =
1563 crate::config::resolve_config_ids(cfg, &interp).into_iter().next()
1565 return Err((
1566 3,
1567 format!(
1568 "config.j: `@{}` in `{}` does not resolve to a unique commit",
1569 prefix, def
1570 ),
1571 ));
1573 // §7.2: if the focused commit is in the immutable set, the snapshot
1574 // cannot go into it; the focus is a new empty child of it holding the
1575 // snapshot, and persisting records that child
1576 match self.focus_if_immutable(&mut interp, cfg, &loaded, &current)? {
1577 Some((loaded, current)) => {
1578 // the snapshot's rewrite of the wc commit (and its rebase of
1579 // the descendants) is not the repository's: lookups answer
1580 // from the stored commits, and persisting leaves the wc
1581 // commit as stored rather than folding the snapshot into it
1582 *self.inner.visible.lock().unwrap() = Some(loaded_visible);
1583 if let Some(p) = self.inner.pending.lock().unwrap().as_mut() {
1584 p.fold_into_wc = false;
1586 Ok((interp, loaded, current))
1588 None => Ok((interp, loaded, current)),
1592 /// If the focus of `current` is immutable, return the loaded and current
1593 /// repos refocused on a new empty child of it (§7.2). Both are `loaded`
1594 /// with that one child added: the snapshot goes into neither the focus
1595 /// nor, through a rebase, its descendants. The child holds the focus's
1596 /// stored files in the loaded repo and the snapshot in the current one,
1597 /// so a dirty `j id` differs from the loaded value and records the child
1598 /// (§1.2 step 8).
1599 fn focus_if_immutable(
1600 &self,
1601 interp: &mut Interp,
1602 cfg: &Config,
1603 loaded: &Value,
1604 current: &Value,
1605 ) -> Result<Option<(Value, Value)>, OpenError> {
1606 // Evaluate the config to compute the immutable set. This uses the
1607 // interpreter the run will go on to use, rather than a throwaway one,
1608 // so that the `immutable` (and the `trunk` inside it) computed here is
1609 // reused when rendering or persisting asks for the same Repo value —
1610 // each is a full interpreted walk of the history.
1611 crate::config::eval_config(interp, cfg).map_err(|c| (3, c.msg))?;
1612 let immutable = crate::repo::compute_immutable(interp, current)
1613 .map_err(|c| (1, c.msg))?;
1614 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
1615 Ok(Value::Id(i)) => i.to_string(),
1616 _ => return Ok(None),
1617 };
1618 if !immutable.contains(&focus_id) {
1619 return Ok(None);
1621 // the focus's location in `loaded`, pre-snapshot; the child is its
1622 // last child, so every existing child sits to the left of it
1623 let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
1624 let root = field(loaded, "root")?;
1625 let frame = Value::record(&[
1626 ("left", field(loaded, "children")?),
1627 ("parent", root.clone()),
1628 ("right", Value::list(vec![])),
1629 ]);
1630 let mut frames = vec![frame];
1631 frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
1632 let context = Value::list(frames);
1633 let id = Value::Id(Rc::new(interp.mint_id()));
1634 let at_child = |files: Value| {
1635 Value::record(&[
1636 ("children", Value::list(vec![])),
1637 ("context", context.clone()),
1639 "root",
1640 Value::record(&[
1641 ("files", files),
1642 ("id", id.clone()),
1643 ("labels", Value::list(vec![])),
1644 ("message", Value::text("")),
1645 ]),
1646 ),
1647 ])
1648 };
1649 let current_at_child = at_child(field(&field(current, "root")?, "files")?);
1650 // with nothing snapshotted `current` is `loaded` itself: share one
1651 // value, so a run that changes nothing is a no-op without comparing
1652 // the whole repository
1653 let loaded_at_child = match (loaded, current) {
1654 (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b) => current_at_child.clone(),
1655 _ => at_child(field(&root, "files")?),
1656 };
1657 Ok(Some((loaded_at_child, current_at_child)))
1660 /// Snapshot the working directory into the working-copy commit (§7.4);
1661 /// return the repo at the snapshot's operation, written but not
1662 /// published, or the repo as loaded when there is nothing to record.
1663 async fn snapshot_repo(
1664 &self,
1665 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
1666 let mut ws_guard = self.inner.workspace.lock().unwrap();
1667 let root = ws_guard.workspace_root().to_owned();
1668 let saved = SavedState::of(&ws_guard);
1669 let mut locked_ws = ws_guard.start_working_copy_mutation()
1670 .await
1671 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1672 let old_tree = locked_ws.locked_wc().old_tree().clone();
1673 let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
1674 .await
1675 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1676 // the repo build_interp loaded at head from the workspace's own
1677 // loader, whose store the scanned tree lives in
1678 let head = self.current_repo();
1679 let wc_commit = self.wc_commit(&head).await?;
1680 let current = locked_ws.locked_wc().old_operation_id() == head.operation().id();
1681 let tree = match marker_style(head.settings()) {
1682 Ok(markers) => snapshot_tree(&root, markers, &wc_commit.tree(), &old_tree, current, &scanned).await,
1683 Err(e) => Err(e),
1685 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1686 if tree.tree_ids() == wc_commit.tree().tree_ids() {
1687 // nothing to record: the directory holds the commit's files, or,
1688 // where the state is stale, the state's, or changes to them the
1689 // commit already has, such as what a checkout cut short wrote.
1690 // What was scanned is saved, at the operation the state is
1691 // stale since where it still is.
1692 save_scan(locked_ws, &scanned, &wc_commit.tree(), head.operation().id())
1693 .await
1694 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1695 return Ok((head, None));
1697 // a directory come to hold the parent's files, as where it reverts
1698 // the commit's change, is recorded with the parent's tree (§7.4), as
1699 // persistence writes a commit with its parent's files (§7.5 step 4):
1700 // the snapshot builds on the commit's tree, and so could make
1701 // another tree for the parent's files, one jj takes for a change.
1702 // Not before the check above, or a commit already holding such a
1703 // tree would be rewritten by a directory that has not changed. A
1704 // merge is immutable, so never folded into (§7.2), and its parents'
1705 // trees are not merged for it
1706 let tree = match wc_commit.parent_ids() {
1707 [_] => parents_tree_if_same(head.as_ref(), wc_commit.parent_ids(), tree).await,
1708 _ => tree,
1709 };
1710 let mut tx = head.start_transaction();
1711 tx.set_is_snapshot(true);
1712 let new_wc = tx
1713 .repo_mut()
1714 .rewrite_commit(&wc_commit)
1715 .set_tree(tree)
1716 .write()
1717 .await
1718 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
1719 tx.repo_mut()
1720 .set_wc_commit(self.inner.workspace_name.clone(), new_wc.id().clone())
1721 .map_err(|e| (2, format!("cannot set the working-copy commit: {}", e)))?;
1722 tx.repo_mut()
1723 .rebase_descendants()
1724 .await
1725 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1726 // write but do NOT publish: a persisting run folds the snapshot into
1727 // its own operation (§7.7); a printing run discards it entirely
1728 let unpublished = tx
1729 .write("snapshot working copy")
1730 .await
1731 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
1732 let new_repo = unpublished.leave_unpublished();
1733 // Release the lock *without* finishing: `finish` saves the scanned
1734 // tree state to disk, which would record the working directory as
1735 // already snapshotted while the published head still has the old wc
1736 // tree. The next run's incremental snapshot would then find nothing
1737 // to do and build the Repo from the stale tree — and a persisting run
1738 // would check that stale tree back out, destroying the edits. The
1739 // state is advanced only by `persist`'s checkout, which first scans
1740 // the directory again and finishes against the operation that
1741 // actually recorded the tree (§7.4/§7.7).
1742 drop(locked_ws);
1743 let pending = PendingSnapshot {
1744 pre_repo: head,
1745 tree: new_wc.tree().clone(),
1746 fold_into_wc: true,
1747 scanned,
1748 };
1749 Ok((new_repo, Some(pending)))
1752 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
1753 let id = repo
1754 .view()
1755 .get_wc_commit_id(WorkspaceName::DEFAULT)
1756 .cloned()
1757 .ok_or_else(|| (2, "no working-copy commit".to_string()))?;
1758 repo.store()
1759 .get_commit_async(&id)
1760 .await
1761 .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))
1764 pub fn persist(
1765 &self,
1766 interp: &mut Interp,
1767 cfg: &Config,
1768 loaded: &Value,
1769 current: &Value,
1770 new: &Value,
1771 text: &str,
1772 ) -> Result<bool, Crash> {
1773 // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
1774 if crate::repo::same_repo(loaded, new)? {
1775 return Ok(false);
1777 *interp.old_repo.borrow_mut() = Some(loaded.clone());
1778 crate::repo::validate_repo(interp, new)?;
1780 // fold the snapshot into this operation (§1.2 step 8): start from the
1781 // pre-snapshot repo and reapply the snapshot's wc rewrite here
1782 let pending = self.inner.pending.lock().unwrap().take();
1783 let base = match &pending {
1784 Some(p) => p.pre_repo.clone(),
1785 None => self.current_repo(),
1786 };
1787 let store = base.store().clone();
1788 let user_sig = self.user_signature(cfg)?;
1790 let mut tx = base.start_transaction();
1791 // the snapshot's wc rewrite, folded into this operation (§1.2): the
1792 // walk below compares against its output, so the snapshot change and
1793 // the expression's edits become one rewrite
1794 let mut snapshot_wc: Option<Commit> = None;
1795 if let Some(p) = pending.as_ref().filter(|p| p.fold_into_wc) {
1796 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
1797 let c = block_on(
1798 tx.repo_mut()
1799 .rewrite_commit(&wc_commit)
1800 .set_tree(p.tree.clone())
1801 .write(),
1803 .map_err(|e| Crash::new(format!("cannot write the snapshot commit: {}", e)))?;
1804 snapshot_wc = Some(c);
1806 // compare against the stored commits of the base operation (the
1807 // snapshot's unpublished rewrite must not become a predecessor)
1808 let mut old_stored: Arc<VisibleRepo> =
1809 Arc::new(block_on(read_visible(&base)).map_err(|e| Crash::new(e.1))?);
1810 if let Some(c) = &snapshot_wc {
1811 let mut vis = (*old_stored).clone();
1812 let change = c.change_id().reverse_hex();
1813 if let Some(rec) = vis.commits.get_mut(&change) {
1814 rec.commit = c.clone();
1816 old_stored = Arc::new(vis);
1818 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
1819 // the stored trees the program's commits were loaded with
1820 let visible = self.inner.visible.lock().unwrap().clone();
1821 let loaded_trees = LoadedTrees::new(visible.as_deref(), &[current, new])?;
1822 // shared across the whole persist walk so unchanged blobs are inflated once
1823 let cache = BlobCache::default();
1824 let entries = EntryCache::default();
1826 // the root commit must be the top and unchanged (§7.5 step 4)
1827 let top = crate::repo::by_id(new, ROOT_ID)?
1828 .and_then(|loc| {
1829 loc.field("context")
1830 .ok()
1831 .and_then(|c| c.as_list().ok().map(|l| l.is_empty()))
1832 .and_then(|is_top| if is_top { Some(loc) } else { None })
1833 })
1834 .ok_or_else(|| Crash::new("persistence: the root commit must be the top"))?;
1835 let root_jj = block_on(store.get_commit_async(store.root_commit_id()))
1836 .map_err(|e| Crash::new(format!("cannot read the root commit: {}", e)))?;
1838 let root_v = top.field("root")?;
1839 let msg = root_v.field("message")?.as_text()?.to_string();
1840 if root_jj.description() != msg {
1841 return Err(Crash::new("persistence: the root commit cannot be changed"));
1843 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
1844 .map_err(|e| Crash::new(e.msg))?;
1845 if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
1846 return Err(Crash::new("persistence: the root commit cannot be changed"));
1849 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
1851 // walk `new` top-down (§7.5 step 4), parents before children
1852 // a deque, not a Vec: `remove(0)` shifts everything after it, which
1853 // is quadratic in the number of commits for a wide history
1854 let mut queue: std::collections::VecDeque<(Value, String)> =
1855 std::collections::VecDeque::new(); // (subtree value, parent change id)
1856 for kid in top.field("children")?.as_list()?.iter() {
1857 queue.push_back((kid.clone(), ROOT_ID.to_string()));
1859 while let Some((subtree, parent_change)) = queue.pop_front() {
1860 let commit_v = subtree.field("root")?;
1861 let id = commit_id_of(&commit_v)?;
1862 let parent_jj = written
1863 .get(&parent_change)
1864 .cloned()
1865 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
1866 // not read unless compared or written: most commits keep the
1867 // stored tree they were loaded with
1868 let files_v = crate::repo::files_of(&commit_v)?;
1869 let message = commit_v.field("message")?.as_text()?.to_string();
1870 let new_id = match old_stored.commits.get(&id) {
1871 None => {
1872 let tree =
1873 loaded_trees.tree_to_write(tx.repo(), &files_v, std::slice::from_ref(&parent_jj))?;
1874 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
1875 Crash::new(format!("persistence: `{}` is not a valid change id", id))
1876 })?;
1877 let c = block_on(
1878 tx.repo_mut()
1879 .new_commit(vec![parent_jj], tree)
1880 .set_change_id(change_id)
1881 .set_description(message)
1882 .set_author(user_sig.clone())
1883 .set_committer(user_sig.clone())
1884 .write(),
1886 .map_err(|e| Crash::new(format!("cannot create commit: {}", e)))?;
1887 c.id().clone()
1889 Some(stored) => {
1890 let stored_commit = &stored.commit;
1891 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
1892 let stored_tree = stored_commit.tree();
1893 // loaded from the stored tree, the files are unchanged:
1894 // reading every commit's to compare them made each
1895 // persist read the files of the whole history
1896 let files_changed = crate::repo::stored_tree(&files_v)
1897 != Some(tree_name(&stored_tree).as_str())
1898 && {
1899 let stored_files =
1900 block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
1901 // a tree has no order: entries listed differently
1902 // are not a change to rewrite (§7.3)
1903 !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
1904 };
1905 let msg_changed = stored_commit.description() != message;
1906 if parent_changed || files_changed || msg_changed {
1907 // files the stored tree lists are written as it is,
1908 // but where they are the parent's, as the files of a
1909 // commit moved onto another tree for them come to be,
1910 // with the parent's tree (§7.5 step 4)
1911 let parents = std::slice::from_ref(&parent_jj);
1912 let tree = if files_changed {
1913 loaded_trees.tree_to_write(tx.repo(), &files_v, parents)?
1914 } else {
1915 block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
1916 };
1917 let c = block_on(
1918 tx.repo_mut()
1919 .rewrite_commit(stored_commit)
1920 .set_parents(vec![parent_jj])
1921 .set_tree(tree)
1922 .set_description(message)
1923 .set_committer(user_sig.clone())
1924 .write(),
1926 .map_err(|e| Crash::new(format!("cannot rewrite commit: {}", e)))?;
1927 c.id().clone()
1928 } else {
1929 stored_commit.id().clone()
1932 };
1933 written.insert(id.clone(), new_id);
1934 for kid in subtree.field("children")?.as_list()?.iter() {
1935 queue.push_back((kid.clone(), id.clone()));
1939 // §7.5 step 5: abandon ids in old that are not in new. Children were
1940 // already re-placed explicitly by the top-down walk above, so record
1941 // the abandon with no rebase targets (repo.rs:1075); calling
1942 // record_abandoned_commit instead would rebase our new commits and
1943 // trip the "descendants not rebased" assert in Transaction::write.
1944 let new_ids = new_id_set(new)?;
1945 for (id, stored) in &old_stored.commits {
1946 if id == ROOT_ID || new_ids.contains(id) {
1947 continue;
1949 // abandon (§7.5 step 5): the walk has already re-placed every
1950 // surviving descendant, so jj's rebase machinery has nothing left
1951 // to do; the abandon is recorded for evolution bookkeeping
1952 tx.repo_mut().record_abandoned_commit_with_parents(
1953 stored.commit.id().clone(),
1954 [stored.commit.parent_ids()[0].clone()],
1955 );
1958 // §7.5 step 6: set the working-copy commit to the focus (validate_repo
1959 // has already checked mutability)
1960 let focus_id = commit_id_of(&new.field("root")?)?;
1961 let focus_jj = written
1962 .get(&focus_id)
1963 .cloned()
1964 .ok_or_else(|| Crash::new("persistence: internal: focus not written"))?;
1965 tx.repo_mut()
1966 .set_wc_commit(self.inner.workspace_name.clone(), focus_jj.clone())
1967 .map_err(|e| Crash::new(format!("cannot set the working-copy commit: {}", e)))?;
1969 // the walk rewrites commits; descendants already written must not be
1970 // left dangling (transaction invariant)
1971 // every descendant of a rewritten or abandoned commit was re-placed
1972 // by the walk itself, so jj's rebase machinery is a no-op here — but
1973 // the transaction requires the bookkeeping to be flushed
1974 block_on(tx.repo_mut().rebase_descendants())
1975 .map_err(|e| Crash::new(format!("cannot rebase descendants: {}", e)))?;
1976 let desc = truncate_chars(text, 200);
1977 let unpublished = block_on(tx.write(desc))
1978 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
1980 // §7.4/§7.5 step 7: publish the operation, then check the focus out
1981 let focus_commit = block_on(store.get_commit_async(&focus_jj))
1982 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
1983 // the checkout starts from what the working directory holds: the
1984 // tree the run's snapshot scanned, or, where it had nothing to
1985 // record, the tree the working-copy state records, as
1986 // `snapshot_repo` left it. That is the focus as loaded unless the
1987 // state is stale (§7.4), when the checkout writes the focus over it;
1988 // taking the directory to hold the focus instead, it would find it
1989 // did not, describe the focus without writing it (a reset), and the
1990 // next run would record the stale files into the focus.
1991 let on_disk = match pending {
1992 Some(p) => p.scanned,
1993 None => {
1994 let state = self.inner.workspace.lock().unwrap().working_copy().tree().cloned();
1995 // a state that cannot be read fails the checkout, which
1996 // reads it again when it locks
1997 state.or_else(|_| block_on(self.wc_commit(&base)).map(|c| c.tree()).map_err(|e| Crash::new(e.1)))?
1999 };
2000 block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
2001 Ok(true)
2004 /// Check `commit` out: write its files to the working directory and
2005 /// save the working copy's state as holding them (§7.4). `op`, the
2006 /// operation that makes it the working-copy commit, is published first,
2007 /// as jj does (§7.5 step 7); with none, `commit` is the head's own.
2008 /// `on_disk` is the tree the directory holds, which the checkout starts
2009 /// from: what a persisting run's snapshot scanned, or the tree the
2010 /// working-copy state records where it scanned nothing to record, or
2011 /// what `undo`'s look found; with none, the state as it is. A change
2012 /// made to the directory since `on_disk` was scanned, where the
2013 /// checkout writes, refuses the checkout before `op` is published
2014 /// (`written_over`); one made once it has begun is written over, as by
2015 /// jj. Where the commit adds a file, something `on_disk` does not track
2016 /// standing there, but for empty directories and what the checkout
2017 /// writes, refuses the checkout before `op` is published
2018 /// (`look_where_added`), and one put there since fails it after, as it
2019 /// makes jj skip the path. A checkout that fails, or is cut short, after
2020 /// `op` is published leaves `op` recorded and the state stale, recording
2021 /// `on_disk`: the next run takes what the checkout wrote for the
2022 /// commit's own and carries on (`snapshot_tree`), as the crash says,
2023 /// with `back` naming the way back; with no `back` it says nothing of
2024 /// the operation (a clone that fails removes it).
2025 async fn checkout(
2026 &self,
2027 commit: &Commit,
2028 on_disk: Option<&MergedTree>,
2029 op: Option<UnpublishedOperation>,
2030 back: Option<&str>,
2031 ) -> Result<(), Crash> {
2032 let mut ws_guard = self.inner.workspace.lock().unwrap();
2033 let root = ws_guard.workspace_root().to_owned();
2034 let saved = SavedState::of(&ws_guard);
2035 let mut locked_ws = ws_guard.start_working_copy_mutation()
2036 .await
2037 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
2038 // A persisting run's snapshot released its lock without saving what
2039 // it scanned, so the state just loaded may still describe the tree
2040 // recorded before the edits, and jj checks out by diffing that tree
2041 // against the new one: a path whose recorded content equals the
2042 // focus's would keep the user's edit, a file created since would
2043 // stay, and a file replaced by a directory (or back) would leave
2044 // state contradicting the tree. Scan the directory again first, as
2045 // the snapshot did, so the checkout diffs from what is actually on
2046 // disk (§7.4). A scan keeps what the state knows of each file where
2047 // a reset to the snapshot's tree would forget it: a conflict whose
2048 // sides hold marker-like lines is materialized with longer markers,
2049 // and a forgotten length makes the next snapshot read those markers
2050 // back at 7 characters, fail to parse them and record the edited
2051 // conflict as resolved text, markers and all (§7.4).
2052 if let Some(on_disk) = on_disk {
2053 let old_tree = locked_ws.locked_wc().old_tree().clone();
2054 let reset = match scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()).await {
2055 Ok(scanned) if scanned.tree_ids() == on_disk.tree_ids() => false,
2056 // the directory changed while the program ran: where the
2057 // checkout would write the focus over such a change, which
2058 // no snapshot holds and jj's checkout does not look for,
2059 // the run crashes before anything is recorded or written
2060 // (§7.5 step 7)
2061 Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
2062 Ok(None) => true,
2063 Ok(Some(path)) => {
2064 return Err(Crash::new(format!(
2065 "cannot check out the focus: `{}` changed in the working directory while the program ran, and the checkout would write over it; nothing was recorded, so run the command again",
2066 path.as_internal_file_string()
2067 )));
2069 Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2070 },
2071 // or it can no longer be scanned (an entry appeared,
2072 // vanished or cannot be read; a failed scan leaves the state
2073 // as it was)
2074 Err(_) => true,
2075 };
2076 // Otherwise the state describes `on_disk` instead. A reset
2077 // writes no file; the paths it changes are re-read by the next
2078 // run.
2079 if reset {
2080 locked_ws
2081 .locked_wc()
2082 .reset(&with_tree(commit, on_disk.clone()))
2083 .await
2084 .map_err(|e| Crash::new(format!("cannot reset the working copy: {}", e)))?;
2087 // Where the focus adds a file and something the directory does not
2088 // track stands (an ignored file, a symlink, a directory holding
2089 // one), jj's checkout would skip the path, and the next run read
2090 // what stands there into the focus: refuse before anything is
2091 // recorded or written, as git does (§7.5 step 7). The trees of
2092 // empty directories found there, and the files holding what the
2093 // checkout writes, are cleared once `op` is published.
2094 let markers = marker_style(self.current_repo().settings())
2095 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
2096 let clear = match on_disk {
2097 None => Vec::new(),
2098 Some(on_disk) => match look_where_added(&root, markers, on_disk, &commit.tree()).await {
2099 Ok(Ok(clear)) => clear,
2100 Ok(Err(in_the_way)) => {
2101 return Err(Crash::new(format!("cannot check out the focus: {}", in_the_way_of(&in_the_way))));
2103 Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2104 },
2105 };
2106 // from here until the state is saved, a signal to stop waits
2107 let _deferral = Deferral::start();
2108 let published = op.is_some();
2109 let op_id = match op {
2110 None => self.current_repo().operation().id().clone(),
2111 Some(op) => {
2112 let repo = op
2113 .publish()
2114 .await
2115 .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
2116 let op_id = repo.operation().id().clone();
2117 *self.inner.repo.lock().unwrap() = repo;
2118 op_id
2120 };
2121 // what a crash from here on says: the state is left stale, which the
2122 // next run carries on from (§7.5 step 7)
2123 let carry_on = |what: &str| match back {
2124 None => String::new(),
2125 Some(back) if published => {
2126 format!("; the operation is recorded, but {}: the next command carries on from there{}", what, back)
2128 Some(_) => format!("; {}: the next command carries on from there", what),
2129 };
2130 // A checkout cut short leaves the directory holding `on_disk` but
2131 // where it wrote, and the next run takes what it holds for changes
2132 // to the state (`snapshot_tree`). So the state records `on_disk`
2133 // first, at the operation it was saved at, which leaves it stale
2134 // until the checkout completes: were it left recording the tree
2135 // before the run's snapshot, the snapshot's changes, now in the
2136 // operation, would be taken for new ones and replayed onto the
2137 // focus too. It is saved only once `op` is published: a state
2138 // recording the snapshot, beside a head that does not, would lose
2139 // it (§7.4).
2140 if on_disk.is_some_and(|t| t.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids()) {
2141 let old_op = locked_ws.locked_wc().old_operation_id().clone();
2142 let saved = match locked_ws.finish(old_op).await {
2143 Ok(()) => ws_guard.start_working_copy_mutation().await.map_err(|e| e.to_string()),
2144 Err(e) => Err(e.to_string()),
2145 };
2146 locked_ws = match saved {
2147 Ok(locked) => locked,
2148 Err(e) => {
2149 let what = "the working directory was not updated";
2150 return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2152 };
2154 // what is moved aside is put back where the checkout does not write,
2155 // or cuts the write short, once it returns, however it returns
2156 let mut aside = clear_the_way(&root, markers, &commit.tree(), &clear).await;
2157 let from = locked_ws.locked_wc().old_tree().clone();
2158 let what = "the working directory was only partly updated";
2159 let stats = match locked_ws.locked_wc().check_out(commit).await {
2160 Ok(stats) => stats,
2161 Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}{}", e, carry_on(what)))),
2162 };
2163 // it cut no write short, so a start of a file moved aside, standing
2164 // at its path, was put there meanwhile
2165 aside.returned = true;
2166 // Something the directory does not track, put where the focus adds a
2167 // file since it was looked at, or a file holding what the checkout
2168 // writes there that could not be moved aside, made jj skip the
2169 // path, which it records as written. The state is left stale, as by
2170 // a checkout that fails, so that no run reads what stands there
2171 // (§7.5 step 7).
2172 if stats.skipped_files > 0 {
2173 let skipped = first_skipped(&root, markers, &from, &commit.tree()).await.ok().flatten();
2174 let why = match (skipped, &aside.stuck) {
2175 (Some(path), _) => format!(
2176 "`{}` was not written, as something untracked in the working directory was in its way (move it aside)",
2177 path.as_internal_file_string()
2178 ),
2179 (None, Some((path, e))) => format!(
2180 "`{}` was not written, as the untracked file there, holding what it writes, could not be moved out of its way: {}",
2181 path.as_internal_file_string(),
2183 ),
2184 (None, None) => format!(
2185 "{} of its files were not written, as something untracked in the working directory was in their way",
2186 stats.skipped_files
2187 ),
2188 };
2189 return Err(Crash::new(format!("cannot check out the focus: {}{}", why, carry_on(what))));
2191 // the state names the operation it was checked out at, so it is
2192 // saved only once that operation is published
2193 if let Err(e) = locked_ws.finish(op_id).await {
2194 let what = "the working copy's record of what the directory holds was not updated";
2195 return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2197 Ok(())
2200 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
2201 let (name, email) = validated_identity(cfg)
2202 .map_err(|(_, message)| Crash::new(message))?;
2203 Ok(Signature {
2204 name,
2205 email,
2206 timestamp: Timestamp::now(),
2207 })
2210 fn repo_loader(&self) -> jj_lib::repo::RepoLoader {
2211 self.inner.workspace.lock().unwrap().repo_loader().clone()
2214 fn head_repo(&self) -> Result<Arc<ReadonlyRepo>, OpenError> {
2215 block_on(self.repo_loader().load_at_head())
2216 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))
2219 async fn publish_tx(
2220 &self,
2221 tx: jj_lib::transaction::Transaction,
2222 description: &str,
2223 ) -> Result<(OperationId, Arc<ReadonlyRepo>), OpenError> {
2224 let unpublished = tx
2225 .write(description)
2226 .await
2227 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2228 let op_id = unpublished.operation().id().clone();
2229 let repo = unpublished
2230 .publish()
2231 .await
2232 .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2233 *self.inner.repo.lock().unwrap() = repo.clone();
2234 Ok((op_id, repo))
2237 fn git_subprocess_options(&self) -> Result<jj_lib::git::GitSubprocessOptions, OpenError> {
2238 jj_lib::git::GitSubprocessOptions::from_settings(self.current_repo().settings())
2239 .map_err(|e| (2, format!("cannot read git settings: {}", e)))
2242 fn git_import_options(&self) -> Result<jj_lib::git::GitImportOptions, OpenError> {
2243 let settings = jj_lib::git::GitSettings::from_settings(self.current_repo().settings())
2244 .map_err(|e| (2, format!("cannot read git settings: {}", e)))?;
2245 Ok(jj_lib::git::GitImportOptions {
2246 abandon_unreachable_commits: settings.abandon_unreachable_commits,
2247 record_synthetic_predecessors: settings.record_synthetic_predecessors,
2248 remote_auto_track_bookmarks: std::collections::HashMap::new(),
2249 })
2252 /// fetch all bookmarks/tags of `remote` into the git repo, via the git
2253 /// subprocess (jj-lib's GitFetch handles remote-name validation and the
2254 /// refspec retry loop); the fetched refs are imported by the caller
2255 fn git_fetch_refs(
2256 &self,
2257 mut_repo: &mut jj_lib::repo::MutableRepo,
2258 remote: &RemoteName,
2259 ) -> Result<(), OpenError> {
2260 use jj_lib::str_util::StringExpression;
2261 let expr = jj_lib::git::GitFetchRefExpression {
2262 bookmark: StringExpression::all(),
2263 tag: StringExpression::all(),
2264 };
2265 let expanded = jj_lib::git::expand_fetch_refspecs(remote, expr)
2266 .map_err(|e| (1, format!("cannot expand the fetch refspecs: {}", e)))?;
2267 let subprocess_options = self.git_subprocess_options()?;
2268 let import_options = jj_lib::git::GitImportOptions {
2269 abandon_unreachable_commits: true,
2270 record_synthetic_predecessors: true,
2271 remote_auto_track_bookmarks: std::collections::HashMap::new(),
2272 };
2273 let mut git_fetch = jj_lib::git::GitFetch::new(mut_repo, subprocess_options, &import_options)
2274 .map_err(|_| (2, "the repository does not have a git backend".to_string()))?;
2275 let mut callback = QuietGitCallback;
2276 match git_fetch.fetch(remote, expanded, &mut callback, None) {
2277 Ok(()) => Ok(()),
2278 Err(jj_lib::git::GitFetchError::NoSuchRemote(_)) => {
2279 // the backend's cached gix handle can be stale when the
2280 // remote was just written to .git/config in this same run;
2281 // fall back to a direct `git fetch` subprocess
2282 let store = self.current_repo().store().clone();
2283 let backend = git_backend(&store)?;
2284 if remote_has_url(backend, remote) {
2285 git_fetch_subprocess(backend.git_repo_path(), remote)
2286 } else {
2287 Err((1, format!("no remote {}", remote.as_str())))
2290 Err(other) => Err((1, format!("fetch failed: {}", other))),
2294 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295 let url = &origin_url(url)?;
2296 self.take_lock();
2297 let base = self.current_repo();
2298 let origin = RemoteName::new("origin");
2299 let exists = {
2300 let git_repo = jj_lib::git::get_git_repo(base.store())
2301 .map_err(|_| (2, "the repository does not have a git backend".to_string()))?;
2302 git_repo.try_find_remote(origin.as_str()).is_some()
2303 };
2304 if exists {
2305 jj_lib::git::set_remote_urls(base.store(), origin, Some(url), None)
2306 } else {
2307 let mut tx = base.start_transaction();
2308 jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
2310 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
2313 /// Run from the top of the workspace, as git runs from the top of its
2314 /// working tree: git resolves a relative local URL (one stored by git,
2315 /// or by `j` before §7.8 resolved them) against the directory it runs
2316 /// in, and jj-lib's git and ours inherit this process's
2317 fn enter_workspace_root(&self) -> Result<(), OpenError> {
2318 std::env::set_current_dir(&self.inner.workspace_root)
2319 .map_err(|e| (1, format!("cannot enter {}: {}", self.inner.workspace_root.display(), e)))
2322 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323 self.enter_workspace_root()?;
2324 let origin = RemoteName::new("origin");
2325 let base = self.head_repo()?;
2326 let mut tx = base.start_transaction();
2327 self.git_fetch_refs(tx.repo_mut(), origin)?;
2328 let import_options = self.git_import_options()?;
2329 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
2330 .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
2331 block_on(tx.repo_mut().rebase_descendants())
2332 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
2333 let _ = block_on(self.publish_tx(tx, "fetch"))?;
2334 Ok(())
2336 pub fn cmd_push(&self, cfg: &mut Config, expr_text: &str) -> Result<PushOutcome, OpenError> {
2337 self.enter_workspace_root()?;
2338 let origin = RemoteName::new("origin");
2339 let base = self.head_repo()?;
2340 let (value, vis, immutable) = self.eval_push_expr(cfg, expr_text, &base)?;
2341 let records = parse_push_records(&value, &vis, &base)?;
2342 if records.is_empty() {
2343 return Ok(PushOutcome::default());
2345 let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
2346 let mut outcome = PushOutcome::default();
2347 for (name, target) in &records {
2348 let before = base.view()
2349 .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2350 .target.as_resolved().cloned().unwrap_or(None);
2351 if &before == target {
2352 outcome.unchanged += 1;
2353 } else if target.is_none() {
2354 outcome.deleted += 1;
2355 } else {
2356 outcome.published += 1;
2359 let (_, clean) = self.inspect_working_copy(&base, false)?;
2360 if !clean {
2361 crate::terminal::report(format_args!(
2362 "j: warning: working copy has unrecorded changes; push sends recorded content only, so these edits will not be sent. Run `j id` to record them, then push again to include them."
2363 ));
2365 let description = format!("push {}", expr_text);
2366 let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
2367 Ok(outcome)
2370 /// evaluate `EXPR` against the recorded repository, without snapshotting
2371 /// (§1.1), apply a function result to the repo value once (§7.6); also
2372 /// return the immutable set of that repository (§7.5 step 3)
2373 fn eval_push_expr(
2374 &self,
2375 cfg: &mut Config,
2376 expr_text: &str,
2377 base: &Arc<ReadonlyRepo>,
2378 ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
2379 let outer = Rc::new(cfg.global_names.clone());
2380 let expr = crate::parse::parse_expr(expr_text, outer)
2381 .map_err(|p| (3, crate::diagnostics::format_source_error("expression", expr_text, p.line, &p.msg)))?;
2382 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
2383 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
2384 let (prefix, candidates) = &failures[0];
2385 if candidates.is_empty() {
2386 (1, format!("crash: `@{}` matches no commit", prefix))
2387 } else {
2388 (1, format!("crash: `@{}` is ambiguous", prefix))
2390 })?;
2391 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
2392 // the set a push may not move a bookmark out of (§7.6); build_interp
2393 // evaluated it for this value to place the focus, so it is cached
2394 let immutable = crate::repo::compute_immutable(&mut interp, &current)
2395 .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2396 let expression_error = |crash: Crash| {
2397 let mut message = format!("crash: {}", crash.msg);
2398 match &crash.def {
2399 Some(name) => message.push_str(&format!("\n in {}, from push {}", name, expr_text)),
2400 None => message.push_str(&format!("\n from push {}", expr_text)),
2402 if let Some(hint) = crate::diagnostics::push_expression_hint(expr_text, cfg, &crash) {
2403 message.push_str(&format!("\n {}", hint));
2405 (1, message)
2406 };
2407 let env = interp.global_env();
2408 let expr_rc = Rc::new(expr);
2409 let mut v = interp
2410 .eval(&expr_rc, &env)
2411 .map_err(&expression_error)?;
2412 if matches!(v, Value::Fun(_)) {
2413 v = interp
2414 .apply(v, current)
2415 .map_err(expression_error)?;
2417 let _ = base;
2418 let vis = self
2419 .inner
2420 .visible
2421 .lock()
2422 .unwrap()
2423 .clone()
2424 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
2425 Ok((v, vis, immutable))
2428 /// push the updates to `origin` and record the new remote-bookmark
2429 /// positions as one operation (§7.6); when `deletes_wait`, the deletes
2430 /// go in a second git push, sent only once the remote has accepted every
2431 /// other update
2432 fn push_to_origin(
2433 &self,
2434 base: &Arc<ReadonlyRepo>,
2435 origin: &RemoteName,
2436 records: &[(String, Option<CommitId>)],
2437 deletes_wait: bool,
2438 description: &str,
2439 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
2440 // Never hand an empty refspec list to git, whose push.default could
2441 // select branches the expression did not request.
2442 if records.is_empty() {
2443 return Ok(base.clone());
2445 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446 let mut deletes = jj_lib::git::GitPushRefTargets::default();
2447 for (name, after) in records {
2448 let before = base
2449 .view()
2450 .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2451 .target
2452 .as_resolved()
2453 .cloned()
2454 .unwrap_or(None);
2455 // check_push_records sets `deletes_wait` only when some record
2456 // sets a bookmark, so the first push is never empty
2457 let batch = if deletes_wait && after.is_none() { &mut deletes } else { &mut targets };
2458 batch
2459 .bookmarks
2460 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
2462 let mut tx = base.start_transaction();
2463 let subprocess_options = self.git_subprocess_options()?;
2464 let mut callback = QuietGitCallback;
2465 let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
2466 jj_lib::git::push_refs(
2467 tx.repo_mut(),
2468 subprocess_options.clone(),
2469 origin,
2470 targets,
2471 &mut callback,
2472 &jj_lib::git::GitPushOptions::default(),
2474 .map_err(|e| match e {
2475 jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2476 other => (1, format!("push failed: {}", other)),
2477 })
2478 };
2479 let mut stats = push(&mut tx, &targets)?;
2480 // git push is not atomic: the remote may accept some updates and
2481 // reject others. When only a bookmark this push sets keeps a deleted
2482 // bookmark's immutable commit reachable, the deletes go in a second
2483 // push once the remote has accepted every update, and are not sent
2484 // otherwise (§7.6)
2485 let mut not_sent: Vec<String> = Vec::new();
2486 let mut failed = None;
2487 if !deletes.bookmarks.is_empty() {
2488 if stats.all_ok() {
2489 match push(&mut tx, &deletes) {
2490 Ok(more) => {
2491 stats.pushed.extend(more.pushed);
2492 stats.rejected.extend(more.rejected);
2493 stats.remote_rejected.extend(more.remote_rejected);
2494 stats.unexported_bookmarks.extend(more.unexported_bookmarks);
2496 Err(e) => failed = Some(e),
2498 } else {
2499 for (name, _) in &deletes.bookmarks {
2500 not_sent.push(format!("refs/heads/{}", name.as_str()));
2504 // push_refs has set the accepted updates in `tx`, and they are on the
2505 // remote, so they are recorded before the rest is reported; a push
2506 // the remote refused entirely records nothing
2507 let mut repo = base.clone();
2508 if stats.all_ok() || stats.some_exported() {
2509 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
2510 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2511 repo = block_on(unpublished.publish())
2512 .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2513 *self.inner.repo.lock().unwrap() = repo.clone();
2515 if let Some((code, msg)) = failed {
2516 return Err((code, format!("{}; the other bookmarks were pushed and recorded", msg)));
2518 if !stats.all_ok() {
2519 let mut names: Vec<String> = Vec::new();
2520 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
2521 names.push(name.as_str().to_string());
2523 // pushed, but not exported to the colocated git repo, so jj-lib
2524 // left them out of `tx`
2525 for (symbol, _) in &stats.unexported_bookmarks {
2526 names.push(format!("{} (pushed but not recorded)", symbol.name.as_str()));
2528 let mut msg = format!("push rejected: {}", names.join(", "));
2529 if !not_sent.is_empty() {
2530 msg.push_str(&format!("; deletes not sent: {}", not_sent.join(", ")));
2532 if stats.some_exported() {
2533 msg.push_str("; the other bookmarks were pushed and recorded");
2535 return Err((1, msg));
2537 Ok(repo)
2540 pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
2541 let base = self.head_repo()?;
2542 let on_disk = self.check_wc_clean(&base)?;
2543 let loader = self.repo_loader();
2544 let mut cur = base.operation().clone();
2545 let target_op_id: OperationId;
2546 loop {
2547 let marker = op_marker(cur.metadata());
2548 if let Some(m) = marker.as_ref().filter(|m| m.restores_view) {
2549 // jj 0.45 names the operation whose view was restored.
2550 // Repeating the same direction follows that target; reversing
2551 // direction restores the marker's parent. The target may
2552 // itself be an undo or redo, not just a normal operation.
2553 if (m.kind == "redo") == redo {
2554 cur = block_on(loader.load_operation(&m.id))
2555 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2556 continue;
2558 let Some(parent) = cur.parent_ids().first().cloned() else {
2559 return Err((1, if redo { "nothing to redo" } else { "nothing to undo" }.to_string()));
2560 };
2561 target_op_id = parent;
2562 break;
2564 match (marker, redo) {
2565 (None, false) => {
2566 let Some(parent) = cur.parent_ids().first().cloned() else {
2567 return Err((1, "nothing to undo".to_string()));
2568 };
2569 target_op_id = parent;
2570 break;
2572 (None, true) => return Err((1, "nothing to redo".to_string())),
2573 (Some(m), false) if m.kind == "undo" => {
2574 if !m.authored_by_j {
2575 // degrade (§7.7): cannot follow the marker, so undo
2576 // this operation's own parent
2577 let Some(parent) = cur.parent_ids().first().cloned() else {
2578 return Err((1, "nothing to undo".to_string()));
2579 };
2580 target_op_id = parent;
2581 break;
2583 let Some(grandparent) = op_parent_id(&loader, &m.id)? else {
2584 return Err((1, "nothing to undo".to_string()));
2585 };
2586 cur = block_on(loader.load_operation(&grandparent))
2587 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2589 (Some(m), true) if m.kind == "undo" => {
2590 // An old j marker names the discarded normal operation,
2591 // but its parent has the same view and retains any earlier
2592 // undos. Modern redo markers must keep that navigation
2593 // point so another redo can continue across the upgrade.
2594 target_op_id = if m.authored_by_j {
2595 cur.parent_ids().first().cloned()
2596 .ok_or_else(|| (1, "nothing to redo".to_string()))?
2597 } else {
2598 m.id
2599 };
2600 break;
2602 (Some(m), false) => {
2603 // a redo: "move past the operation it reapplied" (§7.7).
2604 // The redo marker names the *undo* it reversed; the
2605 // operation that undo discarded is the one this redo put
2606 // back, so that is where the walk continues. Stopping at
2607 // the undo instead would make the loop treat it as an undo
2608 // to follow and step back one operation too far.
2609 if !m.authored_by_j {
2610 let Some(parent) = cur.parent_ids().first().cloned() else {
2611 return Err((1, "nothing to undo".to_string()));
2612 };
2613 target_op_id = parent;
2614 break;
2616 let undo_op = block_on(loader.load_operation(&m.id))
2617 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2618 cur = match op_marker(undo_op.metadata()) {
2619 Some(u) if u.kind == "undo" && u.authored_by_j => {
2620 block_on(loader.load_operation(&u.id))
2621 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?
2623 // cannot follow the marker: degrade to the undo itself
2624 _ => undo_op,
2625 };
2627 (Some(m), true) => {
2628 // a redo: the undo it reversed is already accounted for,
2629 // so keep looking outward from that undo's parent for an
2630 // undo that has not been reversed yet (§7.7: the head must
2631 // be an undo, "possibly reached through redos").
2632 if !m.authored_by_j {
2633 target_op_id = m.id;
2634 break;
2636 let Some(prev) = op_parent_id(&loader, &m.id)? else {
2637 return Err((1, "nothing to redo".to_string()));
2638 };
2639 cur = block_on(loader.load_operation(&prev))
2640 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2644 // before the operation init or clone recorded there was no
2645 // repository (§7.7); the view jj recorded then has an empty child of
2646 // the root as the working-copy commit, and checking it out would
2647 // delete every file of the head
2648 if !redo && op_created_repo(cur.metadata()) {
2649 return Err((1, "nothing to undo".to_string()));
2651 let mut target_op = block_on(loader.load_operation(&target_op_id))
2652 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2653 // jj also follows markers in the selected direction at the target:
2654 // undoing a redo whose parent is an undo restores that undo's target,
2655 // and conversely for redo. Keep the same canonical navigation point.
2656 while let Some(m) = op_marker(target_op.metadata())
2657 .filter(|m| m.restores_view && (m.kind == "redo") == redo)
2659 target_op = block_on(loader.load_operation(&m.id))
2660 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2662 let target_op_id = target_op.id().clone();
2663 // Report the expression whose state is being reversed or restored,
2664 // following restore markers left by repeated undo/redo commands.
2665 let affected = if redo { &target_op } else { &cur };
2666 let affected_description = restored_operation_description(&loader, affected);
2667 let target_view = block_on(
2668 loader
2669 .op_store()
2670 .read_view(target_op.view_id()),
2672 .map_err(|e| (2, format!("cannot load an operation view: {}", e)))?;
2673 // undoing the operation that established the working copy (e.g. the
2674 // very first operation after init) would restore a view with no
2675 // checked-out commit, leaving the repository unusable; there is
2676 // nothing meaningful to undo
2677 if !target_view
2678 .wc_commit_ids
2679 .contains_key(WorkspaceName::DEFAULT)
2681 return Err((
2682 1,
2683 if redo {
2684 "nothing to redo".to_string()
2685 } else {
2686 "nothing to undo".to_string()
2687 },
2688 ));
2690 // Use jj's current marker so either CLI can continue this history.
2691 // The older j and jj marker formats remain readable above.
2692 let marker = format!(
2693 "{}: restore to operation {}",
2694 if redo { "redo" } else { "undo" },
2695 target_op_id.hex()
2696 );
2697 let mut tx = base.start_transaction();
2698 tx.repo_mut().set_view(target_view.clone());
2699 let unpublished = block_on(tx.write(&marker))
2700 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2701 // the operation is published, then the restored focus checked out
2702 // from what the directory holds (§7.7)
2703 let wc_id = &target_view.wc_commit_ids[WorkspaceName::DEFAULT];
2704 let wc_commit = block_on(base.store().get_commit_async(wc_id))
2705 .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
2706 let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
2707 block_on(self.checkout(&wc_commit, Some(&on_disk), Some(unpublished), Some(back)))
2708 .map_err(|c| (1, c.msg))?;
2709 let focus_message = wc_commit.description().lines().next().unwrap_or("").trim();
2710 Ok(format!(
2711 "{} `{}`; restored focus @{} {}",
2712 if redo { "Redid" } else { "Undid" },
2713 affected_description,
2714 wc_commit.change_id().reverse_hex(),
2715 terminal_text(if focus_message.is_empty() { "(no description)" } else { focus_message }),
2716 ))
2719 /// Refuse, as `undo` and `redo` do before anything else (§7.7), when the
2720 /// working directory holds changes the working-copy commit does not:
2721 /// what a snapshot would record (§7.4), which their checkout would write
2722 /// over. Otherwise return the tree the directory holds, as scanned,
2723 /// which that checkout starts from: the commit's, but for the text
2724 /// written for a conflict the state does not record, as one lost and
2725 /// made again records none, or, where the working-copy state is stale,
2726 /// the state's with any changes to it the commit already has, such as
2727 /// what a checkout cut short wrote. Reserved commands never snapshot,
2728 /// so nothing is recorded either way.
2729 fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<MergedTree, OpenError> {
2730 let (scanned, clean) = self.inspect_working_copy(base, true)?;
2731 if !clean {
2732 return Err((1, "working copy has changes not in @; run `j id` to record them or discard them. Recording creates a new operation: the next `j undo` undoes that recording, not the operation you originally wanted to undo; a new recording also clears pending redos. Use `j ops` to inspect the history".to_string()));
2734 Ok(scanned)
2737 /// Compare a scan with the recorded focus without publishing a snapshot.
2738 /// Push only inspects: it must leave the saved working-copy state alone
2739 /// so a later `j id` still sees every unrecorded edit.
2740 fn inspect_working_copy(
2741 &self,
2742 base: &Arc<ReadonlyRepo>,
2743 refresh_clean_state: bool,
2744 ) -> Result<(MergedTree, bool), OpenError> {
2745 let wc_tree = block_on(self.wc_commit(base))?.tree();
2746 let mut ws_guard = self.inner.workspace.lock().unwrap();
2747 let root = ws_guard.workspace_root().to_owned();
2748 let saved = SavedState::of(&ws_guard);
2749 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
2750 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
2751 let old_tree = locked_ws.locked_wc().old_tree().clone();
2752 let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
2753 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2754 let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
2755 let recorded = match marker_style(base.settings()) {
2756 Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
2757 Err(e) => Err(e),
2759 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2760 if recorded.tree_ids() != wc_tree.tree_ids() {
2761 // drop the lock without saving, or the state would claim the
2762 // directory was snapshotted and hide the changes from the next
2763 // run (§7.7)
2764 return Ok((scanned, false));
2766 // saving the state only refreshes what it knows of each file, or
2767 // takes in what the commit already has
2768 if refresh_clean_state {
2769 block_on(save_scan(locked_ws, &scanned, &wc_tree, base.operation().id()))
2770 .map_err(|e| (2, format!("cannot finish: {}", e)))?;
2772 Ok((scanned, true))
2775 /// The operation log for `ops` (§7.7), one line per operation, newest
2776 /// first; the caller writes it to stdout.
2777 pub fn cmd_ops(&self) -> Result<String, OpenError> {
2778 let base = self.head_repo()?;
2779 let loader = self.repo_loader();
2780 let head = base.operation().clone();
2781 let mut out = String::new();
2782 let mut cur = Some(head);
2783 while let Some(op) = cur {
2784 let end = op.metadata().time.end.timestamp.0.div_euclid(1000);
2785 let age = crate::render::render_age(end);
2786 let mut desc = op.metadata().description.trim().to_string();
2787 let mut tag = "";
2788 match op_kind(op.metadata()) {
2789 OpKind::Normal => {}
2790 OpKind::Undo => tag = "undo",
2791 OpKind::Redo => tag = "redo",
2793 if desc.is_empty() {
2794 desc = "(no description)".to_string();
2796 let mark = if op.id() == base.operation().id() {
2797 "*"
2798 } else {
2799 " "
2800 };
2801 if tag.is_empty() {
2802 out.push_str(&format!("{} {:>4} {}\n", mark, age, desc));
2803 } else {
2804 out.push_str(&format!("{} {:>4} {} ({})\n", mark, age, desc, tag));
2806 cur = match op.parent_ids().first() {
2807 Some(p) => Some(
2808 block_on(loader.load_operation(p))
2809 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?,
2810 ),
2811 None => None,
2812 };
2814 Ok(out)
2818pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
2819 let settings = user_settings_strict(cfg)?;
2820 let cwd = std::env::current_dir()
2821 .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
2822 for dir in cwd.ancestors() {
2823 if dir.join(".jj").is_dir() {
2824 return Err((
2825 2,
2826 format!("already inside a jj repository at {}", dir.display()),
2827 ));
2830 let git_dir = cwd.join(".git");
2831 if git_dir.exists() && !git_dir.is_dir() {
2832 return Err((
2833 2,
2834 ".git exists but is not a directory (a git worktree or submodule)".to_string(),
2835 ));
2837 let had_git = git_dir.is_dir();
2838 let (workspace, repo) = if had_git {
2839 // adopt the existing git repository (§7.8); init_colocated_git
2840 // refuses to init over one
2841 block_on(Workspace::init_external_git(&settings, &cwd, &git_dir))
2842 } else {
2843 block_on(Workspace::init_colocated_git(
2844 &settings,
2845 &cwd,
2846 gix::hash::Kind::Sha1,
2847 ))
2849 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
2850 let backend = JjBackend {
2851 replay_cache: Rc::new(ReplayCache::default()),
2852 inner: Arc::new(JjInner {
2853 workspace_root: cwd.clone(),
2854 workspace: Mutex::new(workspace),
2855 repo: Mutex::new(repo),
2856 workspace_name: WorkspaceName::DEFAULT.to_owned(),
2857 visible: Mutex::new(None),
2858 lock_guard: Mutex::new(None),
2859 pending: Mutex::new(None),
2860 folds_case: OnceLock::new(),
2861 long_names: Mutex::new(HashMap::new()),
2862 }),
2863 };
2864 // the import and the working-copy commit are one operation, the one
2865 // undo never undoes (§7.7)
2866 let mut tx = backend.current_repo().start_transaction();
2867 if had_git {
2868 let import_options = backend.git_import_options()?;
2869 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
2870 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2871 // HEAD too: it is the head the working-copy commit starts on, and a
2872 // detached HEAD's commits are reachable from no ref
2873 block_on(jj_lib::git::import_head(
2874 tx.repo_mut(),
2875 &backend.inner.workspace_name,
2876 &cwd,
2877 ))
2878 .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
2879 block_on(tx.repo_mut().rebase_descendants())
2880 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
2882 // §7.8: the current head is git's HEAD (none on an unborn branch)
2883 let head = tx
2884 .repo()
2885 .view()
2886 .git_head(&backend.inner.workspace_name)
2887 .as_resolved()
2888 .cloned()
2889 .flatten();
2890 create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
2891 Ok(())
2894pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
2895 let settings = user_settings_strict(cfg)?;
2896 let url = &origin_url(url)?;
2897 let dir_path = PathBuf::from(dir);
2898 if dir_path.join(".jj").is_dir() {
2899 return Err((2, format!("{} already contains a jj repository", dir)));
2901 if dir_path.is_dir() {
2902 let mut entries = std::fs::read_dir(&dir_path)
2903 .map_err(|e| (2, format!("cannot read {}: {}", dir, e)))?;
2904 if entries.next().is_some() {
2905 return Err((2, format!("{} already exists and is not empty", dir)));
2907 } else if dir_path.exists() {
2908 return Err((2, format!("{} already exists and is not empty", dir)));
2910 let existed = dir_path.is_dir();
2911 // the directory an existing DIR leads to, through a symlink DIR may be:
2912 // a clone that fails empties it only while DIR still leads there (§7.8)
2913 let found = if existed { dir_identity(&dir_path, true) } else { None };
2914 // the directories this run creates, in order, each with its device and
2915 // inode: a clone that fails removes DIR and those of its parents left
2916 // empty, or an existing DIR's entries, and nothing else (§7.8).
2917 // Each is made by `create_dir`, whose success proves it new. The checks
2918 // above read DIR as written, before its missing parents exist; once
2919 // they do, `new/..` or `new/../keep` names a directory that was there,
2920 // so DIR's own `create_dir` failing, AlreadyExists included, refuses it
2921 // as `git clone` does
2922 let mut created: Vec<(&std::path::Path, Option<(u64, u64)>)> = Vec::new();
2923 // those of them a failed clone leaves, as their paths no longer lead
2924 // to them
2925 let mut left: Vec<&std::path::Path> = Vec::new();
2926 // from here until the clone has removed what it made or completed, a
2927 // signal to stop waits, and fails a clone that has not yet made its
2928 // working-copy commit (`unless_interrupted`, §1.3)
2929 let _held = Deferral::start();
2930 if !existed {
2931 // DIR and its missing parents, DIR first ("" is the current
2932 // directory, which exists)
2933 let missing: Vec<&std::path::Path> = dir_path
2934 .ancestors()
2935 .enumerate()
2936 .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
2937 .map(|(_, p)| p)
2938 .collect();
2939 for p in missing.into_iter().rev() {
2940 match std::fs::create_dir(p) {
2941 Ok(()) => created.push((p, dir_identity(p, false))),
2942 // a parent reached again through `..`
2943 Err(e)
2944 if e.kind() == std::io::ErrorKind::AlreadyExists
2945 && p != dir_path
2946 && p.is_dir() => {}
2947 Err(e) => {
2948 for &(p, id) in created.iter().rev() {
2949 if !remove_created_dir(p, id, false) {
2950 left.push(p);
2953 let msg = format!("cannot create {}: {}", dir, e);
2954 return Err((2, msg + &left_note(&left)));
2959 let cloned = clone_into(cfg, &settings, url, &dir_path);
2960 let Err((code, msg)) = cloned else {
2961 return Ok(());
2962 };
2963 // like `git clone`, a clone that fails leaves nothing behind: its
2964 // working-copy commit would claim files the checkout never wrote, and a
2965 // later clone would refuse the directory (§7.8)
2966 if !existed {
2967 // DIR, the last one created, goes whole; a parent holds nothing of
2968 // the clone's but DIR, so it goes only if left empty: what else was
2969 // put there meanwhile, a sibling clone say, stays
2970 for (i, &(p, id)) in created.iter().enumerate().rev() {
2971 if !remove_created_dir(p, id, i + 1 == created.len()) {
2972 left.push(p);
2975 Err((code, msg + &left_note(&left)))
2976 } else if found.is_some() && dir_identity(&dir_path, true) == found {
2977 if let Ok(entries) = std::fs::read_dir(&dir_path) {
2978 for entry in entries.flatten() {
2979 let _ = match entry.file_type() {
2980 Ok(t) if t.is_dir() => std::fs::remove_dir_all(entry.path()),
2981 _ => std::fs::remove_file(entry.path()),
2982 };
2985 Err((code, msg))
2986 } else if dir_path.symlink_metadata().is_ok() {
2987 // a symlink put in place of DIR, or of a directory above it, would
2988 // have `read_dir` list what that names
2989 Err((
2990 code,
2991 format!(
2992 "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
2993 msg, dir
2994 ),
2995 ))
2996 } else {
2997 Err((code, msg))
3001/// The device and inode of the directory at `path`, following a symlink
3002/// there only when `follow` says so; `None` when `path` holds no directory
3003fn dir_identity(path: &std::path::Path, follow: bool) -> Option<(u64, u64)> {
3004 use std::os::unix::fs::MetadataExt;
3005 let meta = if follow {
3006 std::fs::metadata(path)
3007 } else {
3008 std::fs::symlink_metadata(path)
3009 };
3010 meta.ok().filter(|m| m.is_dir()).map(|m| (m.dev(), m.ino()))
3013/// Remove the directory a failed clone created at `path`, `whole` or only
3014/// if empty, while `path` still leads to it: the directory whose device and
3015/// inode `id` were read on creating it (§7.8). A symlink put in its place,
3016/// or in place of a directory above it, would have the removal follow it
3017/// and delete what that names, so a path leading anywhere else is left, and
3018/// the result is false. One leading nowhere holds nothing to remove.
3019fn remove_created_dir(path: &std::path::Path, id: Option<(u64, u64)>, whole: bool) -> bool {
3020 if id.is_some() && dir_identity(path, false) == id {
3021 let _ = if whole {
3022 std::fs::remove_dir_all(path)
3023 } else {
3024 std::fs::remove_dir(path)
3025 };
3026 true
3027 } else {
3028 path.symlink_metadata().is_err()
3032/// What a failed clone's error adds for the directories it created and
3033/// `left` (§7.8): nothing when it left none
3034fn left_note(left: &[&std::path::Path]) -> String {
3035 let named = |ps: &[&std::path::Path]| {
3036 ps.iter().map(|p| format!("`{}`", p.display())).collect::<Vec<_>>().join(", ")
3037 };
3038 match left {
3039 [] => String::new(),
3040 [one] => format!(
3041 "; left {}, as it is no longer the directory this clone created",
3042 named(&[one])
3043 ),
3044 [init @ .., last] => format!(
3045 "; left {} and {}, as they are no longer the directories this clone created",
3046 named(init),
3047 named(&[last])
3048 ),
3052/// `url` as `origin` holds it (§7.8): a local path resolved against the
3053/// current directory, as `jj git clone` stores it, for git resolves a
3054/// relative one against the directory each later `fetch` or `push` runs
3055/// in; one under a home directory (`~/…`, `~user/…`) and any other URL, a
3056/// `file://` one included, as given. One that does not parse, or a path
3057/// that does not resolve to UTF-8, is a usage error.
3058fn origin_url(url: &str) -> Result<String, OpenError> {
3059 let mut parsed = gix::url::parse(url).map_err(|e| (2, format!("invalid URL `{}`: {}", url, e)))?;
3060 // a bare path is a file location in the alternative form
3061 if parsed.scheme != gix::url::Scheme::File || !parsed.serialize_alternative_form {
3062 return Ok(url.to_string());
3064 // git expands a leading `~` to the home directory on every fetch and
3065 // push, from wherever it runs; resolved here, it would name a directory
3066 // called `~` (`./~/…` still does)
3067 if parsed.path.starts_with(b"~") {
3068 return Ok(url.to_string());
3070 let cwd = std::env::current_dir()
3071 .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
3072 parsed
3073 .canonicalize(&cwd)
3074 .map_err(|e| (2, format!("cannot resolve `{}`: {}", url, e)))?;
3075 // `add_remote` takes UTF-8, which only the current directory's name or
3076 // a symlink's target can break; stored as given, the path would reach
3077 // the remote only from where it was typed
3078 String::from_utf8(parsed.to_bstring().into())
3079 .map_err(|_| (2, format!("cannot resolve `{}`: the path it names is not UTF-8", url)))
3082/// `clone`'s work once `dir_path` exists and is empty (§7.8)
3083fn clone_into(
3084 cfg: &Config,
3085 settings: &UserSettings,
3086 url: &str,
3087 dir_path: &std::path::Path,
3088) -> Result<(), OpenError> {
3089 let (workspace, repo) = block_on(Workspace::init_colocated_git(
3090 settings,
3091 dir_path,
3092 gix::hash::Kind::Sha1,
3093 ))
3094 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
3095 let backend = JjBackend {
3096 replay_cache: Rc::new(ReplayCache::default()),
3097 inner: Arc::new(JjInner {
3098 workspace_root: dir_path.to_path_buf(),
3099 workspace: Mutex::new(workspace),
3100 repo: Mutex::new(repo),
3101 workspace_name: WorkspaceName::DEFAULT.to_owned(),
3102 visible: Mutex::new(None),
3103 lock_guard: Mutex::new(None),
3104 pending: Mutex::new(None),
3105 folds_case: OnceLock::new(),
3106 long_names: Mutex::new(HashMap::new()),
3107 }),
3108 };
3109 let origin = RemoteName::new("origin");
3110 let mut tx = backend.current_repo().start_transaction();
3111 // a signal to stop fails each step before the working-copy commit's
3112 // (§1.3)
3113 unless_interrupted(
3114 jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3115 .map_err(|e| (1, format!("cannot set the remote: {}", e))),
3116 )?;
3117 unless_interrupted(backend.git_fetch_refs(tx.repo_mut(), origin))?;
3118 let default_branch = unless_interrupted(git_default_branch(
3119 git_backend(tx.repo().store())?.git_repo_path(),
3120 origin,
3121 ))?;
3122 let import_options = backend.git_import_options()?;
3123 unless_interrupted(
3124 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3125 .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
3126 )?;
3127 unless_interrupted(
3128 block_on(tx.repo_mut().rebase_descendants())
3129 .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
3130 )?;
3131 // §7.8: the default bookmark's target (none when the remote's HEAD names
3132 // no branch, or a branch it does not have)
3133 let head = default_branch.and_then(|name| {
3134 tx.repo()
3135 .view()
3136 .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3137 .target
3138 .as_resolved()
3139 .cloned()
3140 .flatten()
3141 });
3142 // the fetch and the working-copy commit are one operation, the one undo
3143 // never undoes (§7.7)
3144 create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
3145 Ok(())
3148/// §7.8: after init/clone, create a working-copy commit as a child of
3149/// `head` (the root commit when there is none), record it in `tx` with
3150/// what the command has imported, and check it out. The commit holds its
3151/// parent's files less any a checkout cannot create (`checkout_tree`), so
3152/// it is empty unless the parent has one. `adopt` says the working
3153/// directory already holds a checkout of the parent (init over an existing
3154/// git repository): the working copy is then reset to the commit instead of
3155/// written, so no file is touched and what the directory holds beyond the
3156/// parent is the next snapshot's change. `back` is what a checkout that
3157/// fails says (`JjBackend::checkout`): none for a clone, which then removes
3158/// what it made.
3159fn create_initial_wc_commit(
3160 backend: &JjBackend,
3161 cfg: &Config,
3162 mut tx: jj_lib::transaction::Transaction,
3163 description: &str,
3164 head: Option<CommitId>,
3165 adopt: bool,
3166 back: Option<&str>,
3167) -> Result<(), OpenError> {
3168 let store = tx.base_repo().store().clone();
3169 let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
3170 // jj's own init already created a working-copy commit; if it is already
3171 // a child of the target parent, keep it and just check it out
3172 let mut to_abandon: Option<CommitId> = None;
3173 let existing = tx.repo().view().get_wc_commit_id(&backend.inner.workspace_name).cloned();
3174 if let Some(existing) = existing.and_then(|id| block_on(store.get_commit_async(&id)).ok()) {
3175 if existing.parent_ids().first() == Some(&parent) {
3176 // jj's commit is an empty child of the root; record only what
3177 // was imported, if anything
3178 let op = if tx.repo().has_changes() {
3179 Some(
3180 block_on(tx.write(truncate_chars(description, 200)))
3181 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
3183 } else {
3184 None
3185 };
3186 block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
3187 return Ok(());
3189 if existing.id() != store.root_commit_id() {
3190 to_abandon = Some(existing.id().clone());
3193 let parent_tree = block_on(store.get_commit_async(&parent))
3194 .map_err(|e| (1, format!("cannot read the parent commit: {}", e)))?
3195 .tree();
3196 let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
3197 let user_sig = backend
3198 .user_signature(cfg)
3199 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3200 if let Some(old_wc) = &to_abandon {
3201 tx.repo_mut().record_abandoned_commit_with_parents(
3202 old_wc.clone(),
3203 [store.root_commit_id().clone()],
3204 );
3206 let wc = block_on(
3207 tx.repo_mut()
3208 .new_commit(vec![parent], wc_tree)
3209 .set_author(user_sig.clone())
3210 .set_committer(user_sig)
3211 .write(),
3213 .map_err(|e| (1, format!("cannot create the working-copy commit: {}", e)))?;
3214 tx.repo_mut()
3215 .set_wc_commit(backend.inner.workspace_name.clone(), wc.id().clone())
3216 .map_err(|e| (1, format!("cannot set the working-copy commit: {}", e)))?;
3217 block_on(tx.repo_mut().rebase_descendants())
3218 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3219 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
3220 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
3221 // an adopted directory is reset to the commit, not checked out over:
3222 // checking out from the empty tree jj's init recorded would recreate
3223 // the files the user deleted there. Anything else is written, once the
3224 // operation is published (§7.5 step 7).
3225 let on_disk = if adopt { Some(wc.tree()) } else { None };
3226 block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
3227 Ok(())
3230/// `tree` without the files at a path a checkout cannot create (§7.5 step
3231/// 1), which a git branch can hold: a `.jj` directory committed by mistake,
3232/// a name the workspace's filesystem does not hold. jj's checkout stops at
3233/// the first such path, failing the whole clone, so the working-copy commit
3234/// init or clone starts on leaves them out, and their removal is its change
3235/// (§7.8).
3236async fn checkout_tree(tree: MergedTree, backend: &JjBackend) -> Result<MergedTree, OpenError> {
3237 let folds_case = backend.folds_case();
3238 let refused: Vec<RepoPathBuf> = tree
3239 .entries()
3240 .map(|(path, _)| path)
3241 .filter(|path| {
3242 path.components().any(|c| {
3243 crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3244 })
3245 })
3246 .collect();
3247 if refused.is_empty() {
3248 return Ok(tree);
3250 let mut builder = MergedTreeBuilder::new(tree);
3251 for path in refused {
3252 builder.set_or_remove(path, Merge::absent());
3254 builder
3255 .write_tree()
3256 .await
3257 .map_err(|e| (1, format!("cannot write a tree: {}", e)))
3260/// user settings for init/clone: a missing or malformed `user` is a
3261/// configuration error (exit 3) (§7.9)
3262fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3263 let (name, email) = validated_identity(cfg)?;
3264 let mut config = StackedConfig::with_defaults();
3265 let text = format!(
3266 "[user]\nname = {}\nemail = {}\n",
3267 toml_string(&name),
3268 toml_string(&email)
3269 );
3270 let layer =
3271 ConfigLayer::parse(ConfigSource::User, &text).expect("generated user config should parse");
3272 config.add_layer(layer);
3273 UserSettings::from_config(config)
3274 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
3277/// The identity shown during repository setup. Validation is deliberately
3278/// confined to authoring boundaries so inspection and recovery still work
3279/// with an unchanged reference configuration.
3280pub fn configured_identity(cfg: &Config) -> Result<String, OpenError> {
3281 let (name, email) = validated_identity(cfg)?;
3282 Ok(format!("{} <{}>", terminal_text(&name), terminal_text(&email)))
3285fn terminal_text(text: &str) -> String {
3286 text.chars().map(|c| {
3287 if c.is_control() { c.escape_debug().to_string() } else { c.to_string() }
3288 }).collect()
3291fn validated_identity(cfg: &Config) -> Result<(String, String), OpenError> {
3292 let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3293 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3294 check_identity(&name, &email)?;
3295 Ok((name, email))
3298fn check_identity(name: &str, email: &str) -> Result<(), OpenError> {
3299 if name.trim().is_empty() || email.trim().is_empty()
3300 || name.trim() == "Your Name" || email.trim().eq_ignore_ascii_case("you@example.com")
3302 return Err((3, format!(
3303 "config.j: user identity is not configured ({} <{}>); set `user.name` and `user.email` in the `user` record before creating or recording commits",
3304 terminal_text(name), terminal_text(email),
3305 )));
3307 Ok(())
3310// ----------------------------------------------------------------------
3311// reserved-command helpers
3312// ----------------------------------------------------------------------
3314/// discards git subprocess sideband/progress output
3315struct QuietGitCallback;
3317impl jj_lib::git::GitSubprocessCallback for QuietGitCallback {
3318 fn needs_progress(&self) -> bool {
3319 false
3321 fn progress(&mut self, _progress: &jj_lib::git::GitProgress) -> std::io::Result<()> {
3322 Ok(())
3324 fn local_sideband(
3325 &mut self,
3326 _message: &[u8],
3327 _term: Option<jj_lib::git::GitSidebandLineTerminator>,
3328 ) -> std::io::Result<()> {
3329 Ok(())
3331 fn remote_sideband(
3332 &mut self,
3333 _message: &[u8],
3334 _term: Option<jj_lib::git::GitSidebandLineTerminator>,
3335 ) -> std::io::Result<()> {
3336 Ok(())
3340fn git_backend(store: &Store) -> Result<&jj_lib::git_backend::GitBackend, OpenError> {
3341 jj_lib::git::get_git_backend(store)
3342 .map_err(|_| (2, "the repository does not have a git backend".to_string()))
3345/// whether the remote is configured in .git/config, seen through a fresh
3346/// gix handle (the backend's cached handle may predate a just-written config)
3347fn remote_has_url(backend: &jj_lib::git_backend::GitBackend, remote: &RemoteName) -> bool {
3348 let Ok(fresh) = gix::open(backend.git_repo_path()) else {
3349 return false;
3350 };
3351 match fresh.try_find_remote(remote.as_str()) {
3352 Some(Ok(r)) => {
3353 r.url(gix::remote::Direction::Fetch).is_some()
3354 || r.url(gix::remote::Direction::Push).is_some()
3356 _ => false,
3360/// `git fetch <remote>` + prune, with the backend's configured git binary
3361fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Result<(), OpenError> {
3362 let exe = std::env::var("J_GIT").unwrap_or_else(|_| "git".to_string());
3363 let out = std::process::Command::new(exe)
3364 .arg("--git-dir")
3365 .arg(git_dir)
3366 .args(["fetch", "--prune", remote.as_str()])
3367 .stdin(std::process::Stdio::null())
3368 .output()
3369 .map_err(|e| (1, format!("cannot run git fetch: {}", e)))?;
3370 if out.status.success() {
3371 Ok(())
3372 } else {
3373 Err((1, format!("fetch failed: {}", git_failure(&out))))
3377/// What a git that failed said, or how it ended when it said nothing (a
3378/// Ctrl-C stops it silently)
3379fn git_failure(out: &std::process::Output) -> String {
3380 use std::os::unix::process::ExitStatusExt;
3381 let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
3382 match (said.is_empty(), out.status.signal()) {
3383 (false, _) => said,
3384 (true, Some(sig)) => format!("git was stopped by signal {}", sig),
3385 (true, None) => format!("git exited with status {}", out.status.code().unwrap_or(-1)),
3389/// the branch the remote's HEAD names, its default bookmark (§7.8), asked of
3390/// the remote with `git ls-remote --symref` (the git `git_fetch_subprocess`
3391/// runs); `None` when HEAD names no branch (a detached HEAD)
3392fn git_default_branch(
3393 git_dir: &std::path::Path,
3394 remote: &RemoteName,
3395) -> Result<Option<String>, OpenError> {
3396 let exe = std::env::var("J_GIT").unwrap_or_else(|_| "git".to_string());
3397 let out = std::process::Command::new(exe)
3398 .arg("--git-dir")
3399 .arg(git_dir)
3400 .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401 .stdin(std::process::Stdio::null())
3402 .output()
3403 .map_err(|e| (1, format!("cannot run git ls-remote: {}", e)))?;
3404 if !out.status.success() {
3405 return Err((1, format!("cannot read the remote's default branch: {}", git_failure(&out))));
3407 // `ref: refs/heads/<branch>\tHEAD`
3408 Ok(String::from_utf8_lossy(&out.stdout).lines().find_map(|line| {
3409 let branch = line.strip_prefix("ref: refs/heads/")?.strip_suffix("\tHEAD")?;
3410 Some(branch.to_string())
3411 }))
3414enum OpKind {
3415 Normal,
3416 Undo,
3417 Redo,
3420/// a marker parsed from an operation description (§7.7 kinds)
3421struct OpMarker {
3422 kind: &'static str,
3423 id: OperationId,
3424 /// jj 0.45 names the operation whose view was restored, rather than
3425 /// the operation undone or the undo reversed.
3426 restores_view: bool,
3427 /// Whether a legacy marker is j's exact `undo: <id>` / `redo: <id>`
3428 /// format, whose discarded-operation links the walk can follow.
3429 authored_by_j: bool,
3432/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
3433/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
3434/// `redo operation <id>` descriptions remain readable too.
3435fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
3436 match op_marker(meta) {
3437 Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
3438 Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
3439 _ => OpKind::Normal,
3443fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
3444 let desc = meta.description.trim();
3445 for kind in ["undo", "redo"] {
3446 if let Some(rest) = desc.strip_prefix(&format!("{}: restore to operation ", kind)) {
3447 if let Some(id) = parse_op_id(rest.trim()) {
3448 return Some(OpMarker { kind, id, restores_view: true, authored_by_j: false });
3451 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
3452 if let Some(id) = parse_op_id(rest.trim()) {
3453 return Some(OpMarker { kind, id, restores_view: false, authored_by_j: true });
3456 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
3457 if let Some(id) = parse_op_id(rest.trim()) {
3458 return Some(OpMarker { kind, id, restores_view: false, authored_by_j: false });
3462 None
3465/// Whether `meta` is the operation `init` or `clone` recorded (§7.7), by its
3466/// description, `init` or `clone URL`: no expression is described so, both
3467/// words being reserved (§1.1).
3468fn op_created_repo(meta: &jj_lib::op_store::OperationMetadata) -> bool {
3469 let desc = meta.description.trim();
3470 desc == "init" || desc.starts_with("clone ")
3473/// a full-length (or prefix) hex operation id
3474fn parse_op_id(s: &str) -> Option<OperationId> {
3475 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
3476 return None;
3478 hex::decode(pad_hex(s)).ok().map(OperationId::new)
3481fn pad_hex(s: &str) -> String {
3482 if s.len() % 2 == 0 {
3483 s.to_string()
3484 } else {
3485 format!("0{}", s)
3489fn op_parent_id(
3490 loader: &jj_lib::repo::RepoLoader,
3491 op_id: &OperationId,
3492) -> Result<Option<OperationId>, OpenError> {
3493 let op = block_on(loader.load_operation(op_id))
3494 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
3495 Ok(op.parent_ids().first().cloned())
3498/// A restore operation represents an earlier view. Follow that view to a
3499/// normal operation for useful confirmation text without changing undo's
3500/// navigation or its jj-compatible metadata.
3501fn restored_operation_description(
3502 loader: &jj_lib::repo::RepoLoader,
3503 op: &jj_lib::operation::Operation,
3504) -> String {
3505 let mut represented = op.clone();
3506 let mut seen = BTreeSet::new();
3507 while seen.insert(represented.id().clone()) {
3508 let Some(marker) = op_marker(represented.metadata()) else { break };
3509 let target = if marker.restores_view {
3510 Some(marker.id)
3511 } else if marker.authored_by_j {
3512 // A legacy undo names what it discarded; a legacy redo names
3513 // the undo it reversed. Either restored its target's parent.
3514 op_parent_id(loader, &marker.id).ok().flatten()
3515 } else {
3516 // Unknown legacy jj history is best identified by its actual
3517 // description instead of guessing which operation it applied.
3518 None
3519 };
3520 let Some(target) = target else { break };
3521 // Missing historical metadata must not prevent an otherwise valid
3522 // recovery operation merely because its summary cannot be expanded.
3523 let Ok(target_op) = block_on(loader.load_operation(&target)) else { break };
3524 represented = target_op;
3526 let description = represented.metadata().description.lines().next().unwrap_or("").trim();
3527 if description.is_empty() {
3528 format!("operation {}", truncate_chars(&represented.id().hex(), 12))
3529 } else {
3530 terminal_text(description)
3534/// parse the push-record list (§7.6): records of shape {id, name} set a
3535/// bookmark, records of shape {delete} remove one; duplicates merge, the same
3536/// name with different effects crashes (exit 1)
3537fn parse_push_records(
3538 value: &Value,
3539 vis: &VisibleRepo,
3540 repo: &Arc<ReadonlyRepo>,
3541) -> Result<Vec<(String, Option<CommitId>)>, OpenError> {
3542 let items = value
3543 .as_list()
3544 .map_err(|_| (1, "push: the expression must evaluate to a list of push records".to_string()))?;
3545 let mut effects: BTreeMap<String, Option<String>> = BTreeMap::new();
3546 for item in items {
3547 let fields = item.field_set().ok_or_else(|| {
3548 (1, "push: every element must be a record of shape {id, name} or {delete}".to_string())
3549 })?;
3550 if fields.len() == 1 && fields.contains("delete") {
3551 let name = item
3552 .field("delete")
3553 .and_then(|v| v.as_text().map(|t| t.to_string()))
3554 .map_err(|_| (1, "push: `delete` must be a Text".to_string()))?;
3555 match effects.get(&name) {
3556 Some(None) => {}
3557 Some(Some(_)) => {
3558 return Err((1, format!("crash: two push records for `{}` disagree", name)))
3560 None => {
3561 effects.insert(name, None);
3564 } else if fields.len() == 2 && fields.contains("id") && fields.contains("name") {
3565 let name = item
3566 .field("name")
3567 .and_then(|v| v.as_text().map(|t| t.to_string()))
3568 .map_err(|_| (1, "push: `name` must be a Text".to_string()))?;
3569 let id = match item.field("id") {
3570 Ok(Value::Id(i)) => i.to_string(),
3571 _ => return Err((1, "push: `id` must be an Id".to_string())),
3572 };
3573 match effects.get(&name) {
3574 Some(Some(prev)) if prev == &id => {}
3575 Some(_) => {
3576 return Err((1, format!("crash: two push records for `{}` disagree", name)))
3578 None => {
3579 effects.insert(name, Some(id));
3582 } else {
3583 return Err((
3584 1,
3585 "push: every element must be a record of shape {id, name} or {delete}".to_string(),
3586 ));
3589 let origin = RemoteName::new("origin");
3590 let mut out = Vec::new();
3591 for (name, effect) in effects {
3592 if !is_valid_git_branch_name(&name) {
3593 return Err((1, format!("push: `{}` is not a valid git branch name", name)));
3595 match effect {
3596 Some(change_id) => {
3597 let rec = vis.commits.get(&change_id).ok_or_else(|| {
3598 (1, format!("push: `@{}` is not a visible commit", change_id))
3599 })?;
3600 // jj's root has no Git object: its all-zero commit id is
3601 // interpreted by `git push` as a deletion, not a target.
3602 if rec.commit.id() == repo.store().root_commit_id() {
3603 return Err((1, "push: the root commit cannot be pushed".to_string()));
3605 out.push((name, Some(rec.commit.id().clone())));
3607 None => {
3608 let remote_ref = repo
3609 .view()
3610 .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin));
3611 if remote_ref.target.is_absent() {
3612 return Err((
3613 1,
3614 format!("push: the remote does not have a bookmark `{}`", name),
3615 ));
3617 out.push((name, None));
3621 Ok(out)
3624/// §7.6: refuse to move a bookmark whose current target is immutable to a
3625/// commit that does not descend from that target, or to delete it while no
3626/// bookmark left on `origin` after the push reaches that target; and refuse
3627/// to send commits with unresolved files or empty descriptions. Returns
3628/// whether such a delete is allowed only because a record's new target
3629/// reaches it, so the deletes must wait for the updates (push_to_origin),
3630/// and refuses a push whose deletes would wait for a bookmark git cannot
3631/// create while one of them is still there
3632fn check_push_records(
3633 records: &[(String, Option<CommitId>)],
3634 repo: &Arc<ReadonlyRepo>,
3635 vis: &VisibleRepo,
3636 immutable: &BTreeSet<String>,
3637) -> Result<bool, OpenError> {
3638 let store = repo.store().clone();
3639 let origin = RemoteName::new("origin");
3640 // the records' new targets, which the push sends and leaves on `origin`
3641 let new_targets: Vec<CommitId> = records.iter().filter_map(|(_, t)| t.clone()).collect();
3642 // the targets of the other bookmarks left on `origin` after the push:
3643 // those no record names, where they are as of the last fetch or push;
3644 // computed for the first delete that needs them
3645 let mut unnamed: Option<Vec<CommitId>> = None;
3646 // the deletes that only records' new targets keep reached
3647 let mut waiting: Vec<&str> = Vec::new();
3648 for (name, target) in records {
3649 let Some(current) = repo
3650 .view()
3651 .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
3652 .target
3653 .as_resolved()
3654 .cloned()
3655 .unwrap_or(None)
3656 else {
3657 continue;
3658 };
3659 let change_id = block_on(store.get_commit_async(&current))
3660 .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
3661 .change_id()
3662 .reverse_hex();
3663 // the immutable set holds visible commits: a target rewritten here
3664 // since the last fetch or push is hidden, and not in it although the
3665 // visible rewrite carries its change id (and its label)
3666 let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == &current);
3667 if !visible || !immutable.contains(&change_id) {
3668 continue;
3670 match target {
3671 None => {
3672 // a delete loses no history while another bookmark still
3673 // reaches the commit: a merged feature, a second name. When
3674 // only a bookmark this push sets reaches it (`rename`), the
3675 // remote may reject that update and accept the delete, so
3676 // the delete waits for the update to be accepted
3677 let unnamed = unnamed.get_or_insert_with(|| {
3678 let named: BTreeSet<&str> = records.iter().map(|(n, _)| n.as_str()).collect();
3679 repo.view()
3680 .remote_bookmarks(origin)
3681 .filter(|(n, _)| !named.contains(n.as_str()))
3682 .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3683 .cloned()
3684 .collect()
3685 });
3686 let reached = |heads: &[CommitId]| {
3687 ResolvedRevsetExpression::commits(vec![current.clone()])
3688 .intersection(&ResolvedRevsetExpression::commits(heads.to_vec()).ancestors())
3689 .evaluate(repo.as_ref())
3690 .and_then(|r| r.is_empty())
3691 .map(|empty| !empty)
3692 .map_err(|e| (2, format!("cannot read the index: {}", e)))
3693 };
3694 if !reached(unnamed)? {
3695 if !reached(&new_targets)? {
3696 return Err((
3697 1,
3698 format!(
3699 "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3700 name, change_id
3701 ),
3702 ));
3704 waiting.push(name);
3707 Some(new) => {
3708 let descends = block_on(repo.index().is_ancestor(&current, new))
3709 .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3710 if !descends {
3711 return Err((
3712 1,
3713 format!(
3714 "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
3715 name, change_id
3716 ),
3717 ));
3722 // git cannot hold a bookmark and one under its name at once (`master`
3723 // and `master/legacy`), so while the deletes wait, a first push that
3724 // creates a name nesting with a deleted one is rejected and the deletes
3725 // are never sent. With a bookmark no record names on the waiting
3726 // delete's commit, nothing waits and it is one git push (§7.6)
3727 let deletes_wait = !waiting.is_empty();
3728 if deletes_wait {
3729 let nests = |a: &str, b: &str| b.strip_prefix(a).is_some_and(|rest| rest.starts_with('/'));
3730 for (deleted, _) in records.iter().filter(|(_, t)| t.is_none()) {
3731 for (set, _) in records.iter().filter(|(_, t)| t.is_some()) {
3732 if nests(deleted, set) || nests(set, deleted) {
3733 let keep = if waiting.contains(&deleted.as_str()) { deleted.as_str() } else { waiting[0] };
3734 let literal = crate::show::text_literal(keep);
3735 // the temporary label: the first of `tmp`, `tmp-1`, ...
3736 // that is no bookmark, here or on origin, no record
3737 // names, and nests with none of those, so pushing and
3738 // deleting it moves none of the user's bookmarks, and
3739 // this push, which names nothing nesting with it, no
3740 // longer waits on `keep`'s commit
3741 let taken: Vec<&str> = repo
3742 .view()
3743 .bookmarks()
3744 .map(|(n, _)| n.as_str())
3745 .chain(records.iter().map(|(n, _)| n.as_str()))
3746 .collect();
3747 let tmp = std::iter::once("tmp".to_string())
3748 .chain((1..).map(|i| format!("tmp-{i}")))
3749 .find(|t| !taken.iter().any(|n| n == t || nests(n, t) || nests(t, n)))
3750 .expect("finitely many names are taken");
3751 let tmp = crate::show::text_literal(&tmp);
3752 return Err((
3753 1,
3754 format!(
3755 "push: `{deleted}` must stay on origin until `{set}` is accepted, and git cannot create `{set}` while `{deleted}` is there; first push a temporary label on `{keep}`'s commit, `push (label {tmp} (labelled {literal}))`, then this push, then `push (unlabel {tmp})`"
3756 ),
3757 ));
3762 // the commits that would be sent: ancestors of the new targets that no
3763 // bookmark on the remote already reaches; a delete sends none
3764 if new_targets.is_empty() {
3765 return Ok(deletes_wait);
3767 let on_remote: Vec<CommitId> = repo
3768 .view()
3769 .remote_bookmarks(origin)
3770 .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3771 .cloned()
3772 .collect();
3773 let sent = ResolvedRevsetExpression::commits(on_remote)
3774 .range(&ResolvedRevsetExpression::commits(new_targets))
3775 .evaluate(repo.as_ref())
3776 .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3777 let sent: Vec<Commit> = block_on(sent.stream().commits(&store).try_collect())
3778 .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3779 for commit in &sent {
3780 let change_id = commit.change_id().reverse_hex();
3781 let mut unresolved_paths = Vec::new();
3782 for (path, value) in commit.tree().entries() {
3783 let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3784 if !value.is_resolved() {
3785 unresolved_paths.push(path.as_internal_file_string().to_string());
3788 if !unresolved_paths.is_empty() {
3789 let mut paths = unresolved_paths.iter().take(5)
3790 .map(|path| format!("`{}`", terminal_text(path)))
3791 .collect::<Vec<_>>().join(", ");
3792 if unresolved_paths.len() > 5 {
3793 paths.push_str(&format!(" and {} more", unresolved_paths.len() - 5));
3795 return Err((1, format!(
3796 "push: commit `@{change_id}` has unresolved files: {paths}; run `j conflicts` to inspect conflicts, then `j by @{change_id}` to edit this commit. Resolve the listed files, run `j id` to record the resolution, and check `j conflicts` before pushing again"
3797 )));
3799 if commit.description().trim().is_empty() && commit.id() != store.root_commit_id() {
3800 return Err((1, format!("push: commit `@{}` has an empty description", change_id)));
3803 Ok(deletes_wait)
3806/// change-id closure of jj-parents from the seeds (all jj parents, not just
3807/// the first), for the immutable set (§7.5)
3808fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
3809 let mut seen = BTreeSet::new();
3810 let mut stack: Vec<String> = seeds.to_vec();
3811 while let Some(id) = stack.pop() {
3812 if !seen.insert(id.clone()) {
3813 continue;
3815 if let Some(rec) = vis.commits.get(&id) {
3816 for pid in rec.all_parents.iter() {
3817 if !pid.is_empty() {
3818 stack.push(pid.clone());
3823 seen
3826/// git-check-ref-format --branch rules
3827fn is_valid_git_branch_name(name: &str) -> bool {
3828 if name.starts_with('-') {
3829 return false;
3831 let full_name = format!("refs/heads/{name}");
3832 gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
3835// ----------------------------------------------------------------------
3836// Backend impl: metadata lookups and replay (§7.2, §7.3)
3837// ----------------------------------------------------------------------
3839impl Backend for JjBackend {
3840 fn validate_authoring(&self) -> Result<(), Crash> {
3841 let repo = self.current_repo();
3842 check_identity(repo.settings().user_name(), repo.settings().user_email())
3843 .map_err(|(_, message)| Crash::new(message))
3846 fn visible_ids(&self) -> Vec<String> {
3847 match self.inner.visible.lock().unwrap().as_ref() {
3848 Some(v) => v.commits.keys().cloned().collect(),
3849 None => Vec::new(),
3853 fn meta(&self, id: &str) -> Result<MetaInfo, Crash> {
3854 let vis = self.inner.visible.lock().unwrap().clone();
3855 let vis = vis.ok_or_else(|| Crash::new("meta: repository not loaded"))?;
3856 let rec = vis
3857 .commits
3858 .get(id)
3859 .ok_or_else(|| Crash::new("meta: no stored commit for this id"))?;
3860 Ok(MetaInfo {
3861 hash: rec.commit.id().hex(),
3862 author: rec.commit.author().name.clone(),
3863 email: rec.commit.author().email.clone(),
3864 time: rec.commit.committer().timestamp.timestamp.0.div_euclid(1000),
3865 })
3868 fn is_merge(&self, id: &str) -> bool {
3869 let vis = self.inner.visible.lock().unwrap().clone();
3870 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.parent_ids().len() >= 2))
3871 .unwrap_or(false)
3874 fn has_conflict(&self, id: &str) -> Option<bool> {
3875 let vis = self.inner.visible.lock().unwrap().clone();
3876 // O(1): a commit is conflicted iff its tree id is an unresolved merge
3877 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
3880 fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
3881 let vis = self.inner.visible.lock().unwrap().clone();
3882 // O(1): empty iff the tree id equals the parent's tree id
3883 vis.and_then(|v| {
3884 let rec = v.commits.get(id)?;
3885 let parent = v.commits.get(parent)?;
3886 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3887 })
3890 fn folds_case(&self) -> bool {
3891 // jj's checkout refuses a name whose file identity is that of
3892 // `.git` or `.jj` in the same directory (local_working_copy.rs), so
3893 // where `.JJ` is the workspace's own `.jj` it refuses `.GIT` too.
3894 // Where each spelling of a name gets its own inode number
3895 // (exfat-fuse) the identities differ and the checkout writes
3896 // `.GIT/x` into `.git`, so folding is told by how names resolve:
3897 // `.JJ` resolves, but the directory does not list it under that
3898 // spelling. A directory that cannot be listed is taken to fold
3899 // case, the answer that refuses more.
3900 *self.inner.folds_case.get_or_init(|| {
3901 use jj_lib::file_util::FileIdentity;
3902 let root = &self.inner.workspace_root;
3903 let Ok(upper) = FileIdentity::from_symlink_path(root.join(".JJ")) else {
3904 return false;
3905 };
3906 FileIdentity::from_symlink_path(root.join(".jj")).is_ok_and(|lower| lower == upper)
3907 || std::fs::read_dir(root).map_or(true, |mut entries| {
3908 !entries.any(|e| e.is_ok_and(|e| e.file_name() == ".JJ"))
3909 })
3910 })
3913 fn name_fits(&self, name: &str) -> bool {
3914 if name.len() <= crate::repo::NAME_MAX {
3915 return true;
3917 // a component, never a path: one with `/` would be created elsewhere
3918 if name.contains(['/', '\0']) {
3919 return false;
3921 // no call tells whether the limit counts bytes (ext4) or UTF-16
3922 // units or characters (exFAT, NTFS, APFS), so the name is tried:
3923 // created in a directory of its own under `.jj`, which is then
3924 // removed, once per name and run
3925 let mut known = self.inner.long_names.lock().unwrap();
3926 if let Some(fits) = known.get(name) {
3927 return *fits;
3929 let fits = match tempfile::Builder::new()
3930 .prefix("name-")
3931 .tempdir_in(self.inner.workspace_root.join(".jj"))
3933 Ok(dir) => {
3934 let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3935 drop(dir);
3936 fits
3938 Err(_) => false,
3939 };
3940 known.insert(name.to_string(), fits);
3941 fits
3944 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
3945 let vis = self.inner.visible.lock().unwrap().clone();
3946 let Some(vis) = vis else {
3947 return BTreeSet::new();
3948 };
3949 // every jj parent, not just the first: rewriting a commit reached
3950 // only through a merge's second parent rewrites the merge too
3951 // (§7.5 step 3)
3952 let seeds: Vec<String> = ids.iter().cloned().collect();
3953 ancestors_of_change_ids(&vis, &seeds)
3956 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
3957 self.replay_snapshot(
3958 &Value::list(onto.to_vec()),
3959 &Value::list(from.to_vec()),
3960 &Value::list(to.to_vec()),
3961 )?.as_list().map(<[Value]>::to_vec)
3964 fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
3965 let store = self.current_repo().store().clone();
3966 block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
3970// ----------------------------------------------------------------------
3971// jj three-way tree merge for `replay` (§7.3)
3972// ----------------------------------------------------------------------
3974/// Replay sees the same lists repeatedly: a parent's old and new files
3975/// become each child's bases. Retain their validated trees by list identity,
3976/// including its offset, without keeping discarded snapshots alive.
3977#[derive(Default)]
3978struct ReplayCache {
3979 trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980 empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981 next_sweep: Cell<usize>,
3982 blobs: BlobCache,
3983 entries: EntryCache,
3986struct ReplayTree {
3987 storage: Weak<Vec<Value>>,
3988 tree: MergedTree,
3989 /// A list read from a resolved tree, already in canonical path order.
3990 canonical: bool,
3993impl ReplayCache {
3994 fn remember(&self, list: &ListVal, tree: MergedTree, canonical: bool) {
3995 let mut trees = self.trees.borrow_mut();
3996 trees.insert(list.identity(), ReplayTree {
3997 storage: list.weak_storage(),
3998 tree,
3999 canonical,
4000 });
4001 if trees.len() >= self.next_sweep.get().max(256) {
4002 trees.retain(|_, t| t.storage.strong_count() != 0);
4003 self.next_sweep.set((trees.len() * 2).max(256));
4007 async fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<(MergedTree, bool), Crash> {
4008 let Value::List(list) = files else {
4009 files.as_list()?;
4010 unreachable!();
4011 };
4012 if let Some(cached) = self.trees.borrow().get(&list.identity()) {
4013 // The weak reference keeps its allocation reserved, so another
4014 // list cannot reuse this address while the key is cached.
4015 if cached.storage.strong_count() != 0 {
4016 return Ok((cached.tree.clone(), cached.canonical));
4019 // Validate every new list before caching it, even when from == to:
4020 // an unchanged change must still reject malformed snapshots.
4021 let tree = build_tree(store, files).await?;
4022 self.remember(list, tree.clone(), false);
4023 Ok((tree, false))
4026 async fn files_of(&self, store: &Arc<Store>, tree: &MergedTree) -> Result<Value, Crash> {
4027 let (files, reusable) = tree_to_files_inner(store, tree, &self.blobs, &self.entries).await?;
4028 let list = ListVal::new(files);
4029 if reusable && !self.has_empty_subtrees(tree).await? {
4030 self.remember(&list, tree.clone(), true);
4032 Ok(Value::List(list))
4035 /// Flattening a Git tree omits empty directories. Reusing such a tree
4036 /// would let a replay conflict with a directory absent from its input
4037 /// snapshot. Inspect directories once per tree, without rebuilding files.
4038 async fn has_empty_subtrees(&self, tree: &MergedTree) -> Result<bool, Crash> {
4039 let Some(root) = tree.tree_ids().as_resolved() else { return Ok(true) };
4040 if let Some(found) = self.empty_subtrees.borrow().get(root) {
4041 return Ok(*found);
4043 let store = tree.store();
4044 let mut pending = vec![(RepoPathBuf::root(), root.clone())];
4045 let mut visited = BTreeSet::new();
4046 while let Some((dir, id)) = pending.pop() {
4047 if !visited.insert(id.clone()) {
4048 continue;
4050 let known = self.empty_subtrees.borrow().get(&id).copied();
4051 if let Some(found) = known {
4052 if found {
4053 self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4054 return Ok(true);
4056 continue;
4058 let loaded = store.get_tree(dir.clone(), &id).await
4059 .map_err(|e| Crash::new(format!("cannot read a tree: {}", e)))?;
4060 for entry in loaded.entries_non_recursive() {
4061 if let TreeValue::Tree(child) = entry.value() {
4062 if child == store.empty_tree_id() {
4063 self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4064 return Ok(true);
4066 pending.push((dir.join(entry.name()), child.clone()));
4070 // Every subtree visited was nonempty all the way down. Sharing this
4071 // answer also avoids rescanning unchanged directories of later trees.
4072 self.empty_subtrees.borrow_mut().extend(visited.into_iter().map(|id| (id, false)));
4073 Ok(false)
4077async fn jj_replay(
4078 store: &Arc<Store>,
4079 cache: &ReplayCache,
4080 onto: &Value,
4081 from: &Value,
4082 to: &Value,
4083) -> Result<Value, Crash> {
4084 let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
4085 let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
4086 let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
4087 // any of the three may itself be conflicted (a snapshot with unresolved
4088 // blobs, §7.3), so merge the trees the way jj's rebase does:
4089 // MergedTree::merge flattens conflicted inputs into one merge, simplifies
4090 // it, and resolves file contents line-level with tree_merge::merge_trees,
4091 // keeping unresolvable paths as conflicts. Empty labels leave the result
4092 // unlabeled; the labels would not survive tree_to_files anyway.
4093 let merged = MergedTree::merge(Merge::from_removes_adds(
4094 vec![(from_tree.clone(), String::new())],
4095 vec![(onto_tree.clone(), String::new()), (to_tree.clone(), String::new())],
4096 ))
4097 .await
4098 .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4099 refuse_file_directory_clash(&merged, &onto_tree, &from_tree, &to_tree).await?;
4100 // Reusing a resolved tree's list preserves replay's sorted output. A
4101 // constructed list may have another order, and conflicts may simplify
4102 // on reading, so neither can take this shortcut.
4103 for (files, tree, canonical) in [
4104 (onto, &onto_tree, onto_canonical),
4105 (to, &to_tree, to_canonical),
4106 (from, &from_tree, from_canonical),
4107 ] {
4108 if canonical && merged.tree_ids() == tree.tree_ids() {
4109 return Ok(files.clone());
4112 // conflicts stay as unresolved blobs (§7.3)
4113 cache.files_of(store, &merged).await
4116/// Crash if the merge left a conflict with a file on one side and, on
4117/// another, a directory `onto` or `to` holds at that path: one entry cannot
4118/// list that directory's entries (§7.3). A conflict that `onto`, `from` or
4119/// `to` already holds at the path is not made here: its sides, a directory
4120/// side jj made earlier included, came in as an unresolved blob, and replay
4121/// carries them beside the others, as when the child resolving that conflict
4122/// is squashed into it.
4123async fn refuse_file_directory_clash(
4124 merged: &MergedTree,
4125 onto: &MergedTree,
4126 from: &MergedTree,
4127 to: &MergedTree,
4128) -> Result<(), Crash> {
4129 let value_at = async |tree: &MergedTree, path: &RepoPath| {
4130 tree.path_value(path).await.map_err(|e| Crash::new(format!("replay: {}", e)))
4131 };
4132 // walks only the unresolved part of the tree: nothing when it is resolved
4133 for (path, value) in merged.conflicts() {
4134 let value = value.map_err(|e| Crash::new(format!("replay: {}", e)))?;
4135 // sides that cancel are no sides
4136 let value = value.simplify();
4137 let has = |dir: bool| {
4138 value
4139 .adds()
4140 .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4141 };
4142 if !has(false) || !has(true) {
4143 continue;
4145 let held = [
4146 value_at(onto, &path).await?,
4147 value_at(from, &path).await?,
4148 value_at(to, &path).await?,
4149 ];
4150 // an input's conflict at the path, as opposed to one inside the
4151 // directory there, which leaves the path a directory in the input
4152 if held.iter().any(|h| !h.is_resolved() && !h.is_tree()) {
4153 continue;
4155 let side = |dir: &MergedTreeValueT| {
4156 dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
4157 };
4158 if side(&held[0]) || side(&held[2]) {
4159 let comps: Vec<String> =
4160 path.components().map(|c| c.as_internal_str().to_string()).collect();
4161 return Err(crate::domain::file_directory_clash(&comps));
4164 Ok(())
4167// ----------------------------------------------------------------------
4168// values -> jj trees
4169// ----------------------------------------------------------------------
4171/// A merged tree value: Merge<Option<TreeValue>> (jj_lib::backend::MergedTreeValue)
4172type MergedTreeValueT = Merge<Option<TreeValue>>;
4174struct ConflictTreeValue;
4176impl ConflictTreeValue {
4177 fn resolved(v: TreeValue) -> MergedTreeValueT {
4178 Merge::resolved(Some(v))
4180 fn new(adds: Vec<Option<TreeValue>>, removes: Vec<Option<TreeValue>>) -> MergedTreeValueT {
4181 Merge::from_removes_adds(removes, adds)
4185/// The stored trees a program's commits may still hold the files of, to
4186/// write each such commit with the tree itself (§7.5 step 4). A list holds
4187/// each conflict simplified (`merged_value_to_blob`), and the tree it was
4188/// loaded from need not, so a tree built from the list could differ from
4189/// the stored one where the files do not: a child `new` made would not be
4190/// empty, as jj's own `new` leaves one.
4191struct LoadedTrees<'a> {
4192 /// the stored commits the program's repo was loaded from
4193 visible: Option<&'a VisibleRepo>,
4194 /// the lists the lazy `files` of the program's commits have loaded, by
4195 /// identity: `c.files` takes the list out, so a commit given it, as
4196 /// `new` gives its child its parent's, holds the list itself
4197 lists: HashMap<(*const Vec<Value>, usize), Rc<ThunkVal>>,
4200impl<'a> LoadedTrees<'a> {
4201 /// The trees `repos`' commits were loaded with; nothing is read
4202 fn new(visible: Option<&'a VisibleRepo>, repos: &[&Value]) -> Result<LoadedTrees<'a>, Crash> {
4203 let mut lists = HashMap::new();
4204 for repo in repos {
4205 for commit in crate::repo::all_commits(repo)? {
4206 if let Value::Thunk(t) = crate::repo::files_of(&commit)? {
4207 if let (Some(_), Some(Value::List(list))) = (t.tree(), t.peek()) {
4208 lists.insert(list.identity(), t);
4213 Ok(LoadedTrees { visible, lists })
4216 /// The tree to write for a commit on `parents` whose files are `files`:
4217 /// where they are still a list loaded from a stored tree, that tree as
4218 /// jj stored it, and otherwise the one `build_tree` makes of them; but
4219 /// where either lists the parents' files, the parents' tree
4220 fn tree_to_write(&self, repo: &dyn Repo, files: &Value, parents: &[CommitId]) -> Result<MergedTree, Crash> {
4221 let tree = self.tree_of(repo.store(), files)?;
4222 Ok(block_on(parents_tree_if_same(repo, parents, tree)))
4225 fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4226 let loaded = match files {
4227 Value::Thunk(t) => Some(t),
4228 Value::List(list) => self.lists.get(&list.identity()),
4229 _ => None,
4230 };
4231 if let Some(t) = loaded {
4232 let stored = t.origin().zip(self.visible).and_then(|(id, vis)| vis.commits.get(id));
4233 if let Some(tree) = stored.map(|rec| rec.commit.tree()) {
4234 if t.tree() == Some(tree_name(&tree).as_str()) {
4235 return Ok(tree);
4239 block_on(build_tree(store, &files.forced()?))
4243/// The tree of `parents`, merged where there are several, as jj's emptiness
4244/// takes it, where it lists the files `tree` lists, and otherwise `tree`
4245/// (§7.5 step 4). A list holds each conflict less the sides that cancel
4246/// (§7.3), and a tree built from it holds no labels, where a tree jj merged
4247/// keeps both; so a commit left with its parent's files by `squash`,
4248/// `rebase` or `abandon`, which build the files anew, got another tree for
4249/// them: jj took it for a change at each conflicted path, the tree's empty
4250/// mark (`is_empty`) did not show, and the checkout wrote the conflicts
4251/// again with generic labels. So did a commit moved onto another tree for
4252/// its files, which kept its own, and a snapshot that reverted the commit's
4253/// change, which builds on the commit's tree (§7.4). Files with no conflict
4254/// make one tree, so a resolved parents' tree is not compared, nor one of
4255/// the same ids, and the comparison stops at the first path where the two
4256/// differ. It only chooses between two trees for the same files, so where
4257/// the parents' cannot be read, it is `tree`.
4258async fn parents_tree_if_same(repo: &dyn Repo, parents: &[CommitId], tree: MergedTree) -> MergedTree {
4259 let mut commits = Vec::with_capacity(parents.len());
4260 for id in parents {
4261 match repo.store().get_commit_async(id).await {
4262 Ok(commit) => commits.push(commit),
4263 Err(_) => return tree,
4266 let Ok(parents_tree) = jj_lib::rewrite::merge_commit_trees(repo, &commits).await else {
4267 return tree;
4268 };
4269 // the same terms: the parents' tree, with jj's labels for them
4270 if parents_tree.tree_ids() == tree.tree_ids() {
4271 return parents_tree;
4273 if parents_tree.tree_ids().is_resolved() {
4274 return tree;
4276 let mut diff = parents_tree.diff_stream(&tree, &EverythingMatcher);
4277 while let Some(entry) = diff.next().await {
4278 match entry.values {
4279 Ok(Diff { before, after }) if value_as_read(&before) == value_as_read(&after) => {}
4280 _ => return tree,
4283 parents_tree
4286async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4287 let entries = files
4288 .as_list()
4289 .map_err(|c| Crash::new(format!("persistence: {}", c.msg)))?;
4290 let map = crate::domain::snapshot_map(entries)?;
4291 let mut builder = MergedTreeBuilder::new(store.empty_merged_tree());
4292 for (path, content) in &map {
4293 let repo_path = to_repo_path(path)?;
4294 let value = blob_to_tree_value(store, content).await?;
4295 builder.set_or_remove(repo_path, value);
4297 builder
4298 .write_tree()
4299 .await
4300 .map_err(|e| Crash::new(format!("cannot write a tree: {}", e)))
4303async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<MergedTreeValueT, Crash> {
4304 let Value::Blob(b) = content else {
4305 return Err(Crash::new("persistence: content is not a Blob"));
4306 };
4307 match &b.content {
4308 // an unchanged blob loaded from this same store: reuse its file id
4309 // instead of inflating and rewriting the bytes
4310 BlobContent::Lazy(l) => {
4311 let id = FileId::try_from_hex(&l.id)
4312 .ok_or_else(|| Crash::new("persistence: bad blob id".to_string()))?;
4313 Ok(ConflictTreeValue::resolved(TreeValue::File {
4314 id,
4315 executable: b.kind == BlobKind::Executable,
4316 copy_id: CopyId::placeholder(),
4317 }))
4319 BlobContent::Resolved(bytes) if b.kind == BlobKind::Symlink => {
4320 let target = std::str::from_utf8(bytes)
4321 .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4322 let id = store
4323 .write_symlink(&RepoPathBuf::root(), target)
4324 .await
4325 .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4326 Ok(ConflictTreeValue::resolved(TreeValue::Symlink(id)))
4328 BlobContent::Resolved(bytes) => {
4329 let mut slice: &[u8] = bytes.as_ref();
4330 let id = store
4331 .write_file(&RepoPathBuf::root(), &mut slice)
4332 .await
4333 .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4334 Ok(ConflictTreeValue::resolved(TreeValue::File {
4335 id,
4336 executable: b.kind == BlobKind::Executable,
4337 copy_id: CopyId::placeholder(),
4338 }))
4340 BlobContent::Conflict(sides) => {
4341 if sides.len() % 2 != 1 {
4342 return Err(Crash::new("persistence: malformed conflict blob"));
4344 // jj order: [add, (remove, add)*] → Merge with interleaved adds
4345 // and removes; each side keeps its absence, its own file type or
4346 // its directory (§7.3), so the tree written is the one the
4347 // conflict was read from
4348 let mut values: Vec<Option<TreeValue>> = Vec::new();
4349 for side in sides {
4350 let Some(side) = side else {
4351 values.push(None);
4352 continue;
4353 };
4354 let value = if let Some(hex) = &side.tree {
4355 TreeValue::Tree(
4356 TreeId::try_from_hex(hex)
4357 .ok_or_else(|| Crash::new("persistence: bad tree id"))?,
4359 } else if side.kind == BlobKind::Symlink {
4360 let target = std::str::from_utf8(&side.bytes)
4361 .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4362 let id = store
4363 .write_symlink(&RepoPathBuf::root(), target)
4364 .await
4365 .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366 TreeValue::Symlink(id)
4367 } else {
4368 let mut slice: &[u8] = side.bytes.as_ref();
4369 let id = store
4370 .write_file(&RepoPathBuf::root(), &mut slice)
4371 .await
4372 .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4373 TreeValue::File {
4374 id,
4375 executable: side.kind == BlobKind::Executable,
4376 copy_id: CopyId::placeholder(),
4378 };
4379 values.push(Some(value));
4381 let mut adds = Vec::new();
4382 let mut removes = Vec::new();
4383 for (i, v) in values.into_iter().enumerate() {
4384 if i % 2 == 0 {
4385 adds.push(v);
4386 } else {
4387 removes.push(v);
4390 Ok(ConflictTreeValue::new(adds, removes))
4395fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
4396 let mut p = RepoPathBuf::root();
4397 for comp in path {
4398 let c = RepoPathComponentBuf::new(comp.clone())
4399 .map_err(|_| Crash::new(format!("invalid path component `{}`", comp)))?;
4400 p = p.join(&c);
4402 Ok(p)
4405// ----------------------------------------------------------------------
4406// jj trees -> values
4407// ----------------------------------------------------------------------
4409/// Blob contents keyed by file id, shared while snapshots hold their bytes.
4410/// Weak entries let discarded replay results release their conflict sides.
4411#[derive(Clone, Default)]
4412struct BlobCache {
4413 bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
4414 next_sweep: Rc<Cell<usize>>,
4417impl BlobCache {
4418 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
4419 self.bytes.borrow().get(id).and_then(Weak::upgrade)
4421 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
4422 let mut cached = self.bytes.borrow_mut();
4423 cached.insert(id, Rc::downgrade(&bytes));
4424 if cached.len() >= self.next_sweep.get().max(256) {
4425 cached.retain(|_, bytes| bytes.strong_count() != 0);
4426 self.next_sweep.set((cached.len() * 2).max(256));
4431/// Whole file entries keyed by (path, blob identity), shared by loaded
4432/// snapshots and replays. Weak references let transient replay results go
4433/// away while unchanged entries still held by a snapshot are reused.
4434#[derive(Clone, Default)]
4435struct EntryCache {
4436 entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437 next_sweep: Rc<Cell<usize>>,
4440impl EntryCache {
4441 fn get(&self, key: &str) -> Option<Value> {
4442 self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
4444 fn insert(&self, key: String, entry: Value) {
4445 let Value::Record(record) = entry else { return };
4446 let mut entries = self.entries.borrow_mut();
4447 entries.insert(key, Rc::downgrade(&record));
4448 if entries.len() >= self.next_sweep.get().max(256) {
4449 entries.retain(|_, entry| entry.strong_count() != 0);
4450 self.next_sweep.set((entries.len() * 2).max(256));
4455async fn read_file_bytes(
4456 store: &Arc<Store>,
4457 path: &RepoPath,
4458 id: &FileId,
4459 cache: &BlobCache,
4460) -> Result<Rc<Vec<u8>>, Crash> {
4461 if let Some(bytes) = cache.get(id) {
4462 return Ok(bytes);
4464 use futures::AsyncReadExt;
4465 let mut reader = store
4466 .read_file(path, id)
4467 .await
4468 .map_err(|e| Crash::new(format!("cannot read a file: {}", e)))?;
4469 let mut buf = Vec::new();
4470 reader
4471 .read_to_end(&mut buf)
4472 .await
4473 .map_err(|e| Crash::new(format!("cannot read a file: {}", e)))?;
4474 let buf = Rc::new(buf);
4475 cache.insert(id.clone(), buf.clone());
4476 Ok(buf)
4479/// one side of a conflict as jj stores it: absent (`None`, a deletion
4480/// conflict), or content with that side's own file type (§7.3)
4481async fn conflict_side(
4482 store: &Arc<Store>,
4483 path: &RepoPath,
4484 v: &Option<TreeValue>,
4485 cache: &BlobCache,
4486) -> Result<Option<ConflictSide>, Crash> {
4487 match v {
4488 None => Ok(None),
4489 Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490 kind: if *executable {
4491 BlobKind::Executable
4492 } else {
4493 BlobKind::Regular
4494 },
4495 bytes: read_file_bytes(store, path, id, cache).await?,
4496 tree: None,
4497 })),
4498 Some(TreeValue::Symlink(id)) => {
4499 let target = store
4500 .read_symlink(path, id)
4501 .await
4502 .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
4503 Ok(Some(ConflictSide {
4504 kind: BlobKind::Symlink,
4505 bytes: Rc::new(target.into_bytes()),
4506 tree: None,
4507 }))
4509 // a directory side, in a conflict between a file and a directory,
4510 // reads as empty content but keeps its tree, so writing the conflict
4511 // back cannot put an empty file where the directory was
4512 Some(TreeValue::Tree(id)) => Ok(Some(ConflictSide {
4513 kind: BlobKind::Regular,
4514 bytes: Rc::new(Vec::new()),
4515 tree: Some(id.hex()),
4516 })),
4517 // a submodule side reads as an empty file, as a resolved one does in
4518 // tree_value_to_blob (§12)
4519 Some(TreeValue::GitSubmodule(_)) => Ok(Some(ConflictSide::regular(&[]))),
4523/// The value a list reads where a tree holds `value` (§7.3): a conflict less
4524/// each pair of sides that cancel, one added and one removed the same, as jj
4525/// simplifies a file conflict to write it out. jj keeps every term of a
4526/// merge it cannot resolve, pairs that cancel included, so a replayed
4527/// commit's conflict inherited from its parent was not the parent's, and
4528/// each commit a rebase replayed above took two more sides for it. Where
4529/// only directory sides would be left, the value is kept whole: jj keeps it
4530/// as one conflict at the path, and the simplified one would be a conflict
4531/// between directories, which jj merges entry by entry. Two values read
4532/// the same are the same entry of a list.
4533fn value_as_read(value: &MergedTreeValueT) -> std::borrow::Cow<'_, MergedTreeValueT> {
4534 match (!value.is_resolved()).then(|| value.simplify()).filter(|v| !v.is_tree()) {
4535 Some(simplified) => std::borrow::Cow::Owned(simplified),
4536 None => std::borrow::Cow::Borrowed(value),
4540async fn merged_value_to_blob(
4541 store: &Arc<Store>,
4542 path: &RepoPath,
4543 value: &jj_lib::backend::MergedTreeValue,
4544 cache: &BlobCache,
4545) -> Result<Value, Crash> {
4546 // the conflict less the sides that cancel (§7.3)
4547 let read = value_as_read(value);
4548 let value = read.as_ref();
4549 if let Some(v) = value.as_resolved() {
4550 return match v {
4551 Some(tv) => tree_value_to_blob(store, path, tv).await,
4552 None => Ok(Value::Blob(Rc::new(BlobVal {
4553 kind: BlobKind::Regular,
4554 content: BlobContent::Resolved(Rc::new(Vec::new())),
4555 }))),
4556 };
4558 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
4559 let mut sides: Vec<Option<ConflictSide>> = Vec::new();
4560 for (i, add) in value.adds().enumerate() {
4561 sides.push(conflict_side(store, path, add, cache).await?);
4562 if let Some(remove) = value.get_remove(i) {
4563 sides.push(conflict_side(store, path, remove, cache).await?);
4566 Ok(Value::Blob(Rc::new(BlobVal {
4567 kind: BlobKind::Regular,
4568 content: BlobContent::Conflict(sides),
4569 })))
4572async fn tree_value_to_blob(
4573 store: &Arc<Store>,
4574 path: &RepoPath,
4575 value: &TreeValue,
4576) -> Result<Value, Crash> {
4577 match value {
4578 TreeValue::File { id, executable, .. } => {
4579 // lazy: the bytes are read from the store only if an expression
4580 // actually looks at them; equality and persistence use `id`
4581 let id_hex = id.hex();
4582 let store2 = store.clone();
4583 let path2 = path.to_owned();
4584 let id2 = id.clone();
4585 let lazy = LazyBlob::new(id_hex, move || {
4586 let mut reader = block_on(store2.read_file(&path2, &id2))
4587 .map_err(|e| Crash::new(format!("cannot read a file: {}", e)))?;
4588 let mut buf = Vec::new();
4590 use futures::AsyncReadExt;
4591 block_on(reader.read_to_end(&mut buf))
4592 .map_err(|e| Crash::new(format!("cannot read a file: {}", e)))?;
4594 Ok(Rc::new(buf))
4595 });
4596 Ok(Value::Blob(Rc::new(BlobVal {
4597 kind: if *executable {
4598 BlobKind::Executable
4599 } else {
4600 BlobKind::Regular
4601 },
4602 content: BlobContent::Lazy(Rc::new(lazy)),
4603 })))
4605 TreeValue::Symlink(id) => {
4606 let target = store
4607 .read_symlink(path, id)
4608 .await
4609 .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
4610 Ok(Value::Blob(Rc::new(BlobVal {
4611 kind: BlobKind::Symlink,
4612 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
4613 })))
4615 // MergedTree::entries recurses into a resolved directory, and a
4616 // directory side of a conflict goes through conflict_side, so this is
4617 // a submodule, which §12 puts out of scope
4618 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
4619 kind: BlobKind::Regular,
4620 content: BlobContent::Resolved(Rc::new(Vec::new())),
4621 }))),
4625async fn tree_to_files(
4626 store: &Arc<Store>,
4627 tree: &MergedTree,
4628 cache: &BlobCache,
4629 entries: &EntryCache,
4630) -> Result<Vec<Value>, Crash> {
4631 tree_to_files_inner(store, tree, cache, entries).await.map(|(files, _)| files)
4634/// Whether these files can reuse the original tree in replay. Reading a
4635/// conflict simplifies it, and an unsupported git submodule reads as an
4636/// empty file, so their original trees cannot represent the resulting list.
4637async fn tree_to_files_inner(
4638 store: &Arc<Store>,
4639 tree: &MergedTree,
4640 cache: &BlobCache,
4641 entries: &EntryCache,
4642) -> Result<(Vec<Value>, bool), Crash> {
4643 let mut out = Vec::new();
4644 let mut reusable = tree.tree_ids().is_resolved();
4645 for (path, value) in tree.entries() {
4646 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4647 reusable &= match value.as_resolved() {
4648 Some(Some(TreeValue::File { copy_id, .. })) => *copy_id == CopyId::placeholder(),
4649 Some(Some(TreeValue::Symlink(_))) => true,
4650 _ => false,
4651 };
4652 // cache key: the entry is fully determined by its path and the
4653 // identity of its (resolved) content; conflicts are not cached
4654 let key = match value.as_resolved() {
4655 Some(Some(TreeValue::File { id, executable, .. })) => Some(format!(
4656 "F\u{0}{}\u{0}{}\u{0}{}",
4657 path.as_internal_file_string(),
4658 id.hex(),
4659 executable
4660 )),
4661 // symlinks and directories are cheap; conflicts are rare — neither cached
4662 _ => None,
4663 };
4664 let entry = match key.as_ref().and_then(|k| entries.get(k)) {
4665 Some(e) => e,
4666 None => {
4667 let comps: Vec<String> = path
4668 .components()
4669 .map(|c| c.as_internal_str().to_string())
4670 .collect();
4671 let content = merged_value_to_blob(store, &path, &value, cache).await?;
4672 let p = Value::list(comps.into_iter().map(Value::text).collect());
4673 let e = Value::record(&[("content", content), ("path", p)]);
4674 if let Some(k) = key {
4675 entries.insert(k, e.clone());
4679 };
4680 out.push(entry);
4682 Ok((out, reusable))
4685// ----------------------------------------------------------------------
4686// value helpers
4687// ----------------------------------------------------------------------
4689/// The name of a stored tree that a commit's lazy `files` carry
4690/// (`ThunkVal::tree`): its tree ids, one per term of a conflicted tree. The
4691/// list `tree_to_files` makes depends on nothing else, so two trees of one
4692/// name list the same entries.
4693fn tree_name(tree: &MergedTree) -> String {
4694 let ids: Vec<String> = tree.tree_ids().iter().map(|id| id.hex()).collect();
4695 ids.join(",")
4698fn commit_id_of(commit: &Value) -> Result<String, Crash> {
4699 match commit.field("id")? {
4700 Value::Id(i) => Ok(i.to_string()),
4701 v => Err(Crash::new(format!("expected an Id, got a {}", v.kind_name()))),
4705fn hex_of(id: Option<&CommitId>) -> String {
4706 id.map(|i| i.hex()).unwrap_or_default()
4709fn truncate_chars(s: &str, n: usize) -> String {
4710 s.chars().take(n).collect()
4713fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
4714 let mut out = BTreeSet::new();
4715 for c in crate::repo::all_commits(repo)? {
4716 out.insert(commit_id_of(&c)?);
4718 Ok(out)
4721// ----------------------------------------------------------------------
4722// building the Repo value from the visible map (§7.2)
4723// ----------------------------------------------------------------------
4725fn build_repo_value(vis: &VisibleRepo, focus_change: &str, replay_cache: &Rc<ReplayCache>) -> Result<Value, OpenError> {
4726 let root_rec = vis
4727 .commits
4728 .get(ROOT_ID)
4729 .ok_or_else(|| (2, "no root commit".to_string()))?;
4730 let store = root_rec.commit.store().clone();
4731 let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
4732 sort_subtree(&mut subtree, vis);
4733 let top = Value::record(&[
4734 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
4735 ("context", Value::list(vec![])),
4736 ("root", subtree.field("root").map_err(|e| (2, e.msg))?),
4737 ]);
4738 match crate::repo::by_id(&top, focus_change).map_err(|c| (2, c.msg))? {
4739 Some(v) => Ok(v),
4740 None => Err((2, "the working-copy commit is not visible".to_string())),
4744fn build_subtree(
4745 vis: &VisibleRepo,
4746 rec: &StoredCommit,
4747 store: &Arc<Store>,
4748 replay_cache: &Rc<ReplayCache>,
4749) -> Result<Value, OpenError> {
4750 // the files list is lazy (§7.2): most commits are never inspected, so
4751 // their file entries are materialized only on first `field("files")`.
4752 // Tagged with the change id, it tells the renderer when the O(1)
4753 // `has_conflict`/`is_empty` answers still describe the commit's files;
4754 // named by its tree, it lets persistence compare it unread (§7.5)
4755 let files_thunk = {
4756 let store = store.clone();
4757 let tree = rec.commit.tree();
4758 let name = Some(tree_name(&tree));
4759 let replay_cache = replay_cache.clone();
4760 Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
4761 block_on(replay_cache.files_of(&store, &tree))
4762 })))
4763 };
4764 let labels: Vec<Value> = vis
4765 .labels
4766 .get(&rec.change_id)
4767 .cloned()
4768 .unwrap_or_default()
4769 .into_iter()
4770 .map(Value::text)
4771 .collect();
4772 let commit_v = Value::record(&[
4773 ("files", files_thunk),
4774 ("id", Value::Id(Rc::new(rec.change_id.clone()))),
4775 ("labels", Value::list(labels)),
4776 ("message", Value::text(rec.commit.description())),
4777 ]);
4778 let mut children = Vec::new();
4779 if let Some(kids) = vis.children.get(&rec.change_id) {
4780 for kid in kids {
4781 if let Some(krec) = vis.commits.get(kid) {
4782 children.push(build_subtree(vis, krec, store, replay_cache)?);
4786 Ok(Value::record(&[("children", Value::list(children)), ("root", commit_v)]))
4789/// children ordered by committer timestamp asc, ties by change id asc (§7.2)
4790fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
4791 let mut kids: Vec<Value> = subtree
4792 .field("children")
4793 .and_then(|v| v.as_list().map(|l| l.to_vec()))
4794 .unwrap_or_default();
4795 for kid in kids.iter_mut() {
4796 sort_subtree(kid, vis);
4798 kids.sort_by(|a, b| {
4799 let key = |v: &Value| -> (i64, String) {
4800 let id = v
4801 .field("root")
4802 .and_then(|r| r.field("id"))
4803 .ok()
4804 .and_then(|i| match i {
4805 Value::Id(s) => Some(s.to_string()),
4806 _ => None,
4807 })
4808 .unwrap_or_default();
4809 let t = vis.commits.get(&id).map(|r| r.committer_millis).unwrap_or(0);
4810 (t, id)
4811 };
4812 key(a).cmp(&key(b))
4813 });
4814 let root = subtree.field("root").expect("subtree has a root");
4815 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
4818#[cfg(test)]
4819mod tests {
4820 use super::*;
4821 use jj_lib::backend::SymlinkId;
4822 use jj_lib::conflict_labels::ConflictLabels;
4824 fn replay_store() -> (tempfile::TempDir, Arc<Store>) {
4825 let dir = tempfile::tempdir().unwrap();
4826 let (_, repo) = block_on(Workspace::init_colocated_git(
4827 &user_settings(None), dir.path(), gix::hash::Kind::Sha1,
4828 )).unwrap();
4829 (dir, repo.store().clone())
4832 fn file(path: &str, content: &str) -> Value {
4833 Value::record(&[
4834 ("path", Value::list(vec![Value::text(path)])),
4835 ("content", BlobVal::text_blob(content)),
4836 ])
4839 fn loaded_files(store: &Arc<Store>, cache: &ReplayCache, files: Vec<Value>) -> Value {
4840 let tree = block_on(build_tree(store, &Value::list(files))).unwrap();
4841 block_on(cache.files_of(store, &tree)).unwrap()
4844 #[test]
4845 fn replay_reuses_loaded_snapshots_and_unchanged_entries() {
4846 let (_dir, store) = replay_store();
4847 let cache = ReplayCache::default();
4848 let base = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "base b")]);
4849 let same = block_on(jj_replay(&store, &cache, &base, &base, &base)).unwrap();
4850 let (Value::List(before), Value::List(after)) = (&base, &same) else { panic!() };
4851 assert_eq!(before.identity(), after.identity(), "no-op replay must retain the snapshot");
4853 let onto = loaded_files(&store, &cache, vec![file("a", "onto"), file("b", "base b")]);
4854 let to = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "to")]);
4855 let merged = block_on(jj_replay(&store, &cache, &onto, &base, &to)).unwrap();
4856 let expected = Value::list(vec![file("a", "onto"), file("b", "to")]);
4857 assert!(crate::value::value_eq(&merged, &expected).unwrap());
4858 for (result, input) in [
4859 (&merged.as_list().unwrap()[0], &onto.as_list().unwrap()[0]),
4860 (&merged.as_list().unwrap()[1], &to.as_list().unwrap()[1]),
4861 ] {
4862 let (Value::Record(result), Value::Record(input)) = (result, input) else { panic!() };
4863 assert!(Rc::ptr_eq(result, input), "unchanged file entries must be shared");
4867 #[test]
4868 fn replay_cache_preserves_validation_ordering_and_list_offsets() {
4869 let (_dir, store) = replay_store();
4870 let cache = ReplayCache::default();
4871 let unsorted = Value::list(vec![file("b", "b"), file("a", "a")]);
4872 let empty = Value::list(vec![]);
4873 let sorted = block_on(jj_replay(&store, &cache, &unsorted, &empty, &empty)).unwrap();
4874 assert!(crate::value::value_eq(&sorted, &Value::list(vec![file("a", "a"), file("b", "b")])).unwrap());
4876 let Value::List(list) = &sorted else { panic!() };
4877 let tail = Value::List(list.skip(1));
4878 let replayed_tail = block_on(jj_replay(&store, &cache, &tail, &empty, &empty)).unwrap();
4879 assert!(crate::value::value_eq(&tail, &replayed_tail).unwrap());
4880 assert_eq!(replayed_tail.as_list().unwrap().len(), 1);
4882 let malformed = Value::list(vec![file("a", "one"), file("a", "two")]);
4883 let err = block_on(jj_replay(&store, &cache, &sorted, &malformed, &malformed)).unwrap_err();
4884 assert!(err.msg.contains("duplicate paths"), "{}", err.msg);
4887 #[test]
4888 fn replay_ignores_empty_directories_not_represented_by_snapshots() {
4889 let (_dir, store) = replay_store();
4890 let cache = ReplayCache::default();
4891 let entry = |id| jj_lib::backend::Tree::from_sorted_entries(vec![
4892 (RepoPathComponentBuf::new("d".to_string()).unwrap(), TreeValue::Tree(id)),
4893 ]);
4894 let direct = block_on(store.write_tree(RepoPath::root(), entry(store.empty_tree_id().clone()))).unwrap();
4895 let nested = block_on(store.write_tree(RepoPath::root(), entry(direct.id().clone()))).unwrap();
4896 let empty = Value::list(vec![]);
4897 let to = Value::list(vec![file("d", "file")]);
4898 for tree in [direct, nested] {
4899 let tree = MergedTree::resolved(store.clone(), tree.id().clone());
4900 let onto = block_on(cache.files_of(&store, &tree)).unwrap();
4901 assert!(onto.as_list().unwrap().is_empty());
4902 let result = block_on(jj_replay(&store, &cache, &onto, &empty, &to)).unwrap();
4903 assert!(crate::value::value_eq(&result, &to).unwrap());
4907 #[test]
4908 fn replay_cache_does_not_keep_discarded_snapshots_alive() {
4909 let (_dir, store) = replay_store();
4910 let cache = ReplayCache::default();
4911 let files = loaded_files(&store, &cache, vec![file("a", "a")]);
4912 let Value::List(list) = &files else { panic!() };
4913 let storage = list.weak_storage();
4914 let Value::Record(entry) = &files.as_list().unwrap()[0] else { panic!() };
4915 let entry = Rc::downgrade(entry);
4916 drop(files);
4917 assert!(storage.upgrade().is_none());
4918 assert!(entry.upgrade().is_none());
4920 let bytes = Rc::new(vec![1, 2, 3]);
4921 let weak_bytes = Rc::downgrade(&bytes);
4922 let id = FileId::new(vec![1; 20]);
4923 cache.blobs.insert(id.clone(), bytes.clone());
4924 assert!(Rc::ptr_eq(&cache.blobs.get(&id).unwrap(), &bytes));
4925 drop(bytes);
4926 assert!(weak_bytes.upgrade().is_none());
4927 assert!(cache.blobs.get(&id).is_none());
4930 #[test]
4931 fn user_settings_preserve_every_ascii_control_character() {
4932 let identity: String = (0u8..=127).map(char::from).collect();
4933 let text = format!(
4934 "[user]\nname = {}\nemail = {}\n",
4935 toml_string(&identity),
4936 toml_string(&identity)
4937 );
4938 let mut config = StackedConfig::with_defaults();
4939 config.add_layer(ConfigLayer::parse(ConfigSource::User, &text).unwrap());
4940 let settings = UserSettings::from_config(config).unwrap();
4941 assert_eq!(settings.user_name(), identity);
4942 assert_eq!(settings.user_email(), identity);
4945 #[test]
4946 fn a_description_names_its_terms_whatever_their_labels_and_the_sides_that_cancel() {
4947 // what a checkout writes for a symlink/file conflict of five terms,
4948 // and for the three it simplifies to, which a snapshot may write
4949 // without writing the file again (§7.4)
4950 let link = Some(TreeValue::Symlink(SymlinkId::new(vec![1; 20])));
4951 let file = |n: u8| {
4952 let id = FileId::new(vec![n; 20]);
4953 Some(TreeValue::File { id, executable: false, copy_id: CopyId::placeholder() })
4954 };
4955 let five = Merge::from_vec(vec![link.clone(), link.clone(), file(2), file(1), link.clone()]);
4956 let three = five.simplify();
4957 assert_eq!(three.num_sides(), 2);
4958 let unlabeled = ConflictLabels::unlabeled();
4959 let written = five.describe(&unlabeled);
4960 let simpler = three.describe(&unlabeled);
4961 assert_ne!(written, simpler);
4962 let terms = described_terms(simpler.as_bytes());
4963 assert!(terms.is_some());
4964 assert_eq!(described_terms(written.as_bytes()), terms);
4965 let names = ["left (one)", "base", "middle", "base 2", "right"];
4966 let labels = ConflictLabels::from_vec(names.iter().map(|n| n.to_string()).collect());
4967 let labelled = five.describe(&labels);
4968 assert!(labelled.contains(" (left (one))\n"), "{}", labelled);
4969 assert_eq!(described_terms(labelled.as_bytes()), terms);
4970 // other sides, and text that is no description, are no such conflict
4971 let other = Merge::from_vec(vec![link.clone(), file(1), file(3)]).describe(&unlabeled);
4972 assert!(described_terms(other.as_bytes()).is_some_and(|t| Some(t) != terms));
4973 assert_eq!(described_terms(b"resolved\n"), None);
4974 assert_eq!(described_terms(written.replace("Conflict:", "CONFLICT:").as_bytes()), None);
4975 assert_eq!(described_terms(written.trim_end().as_bytes()), None);
4976 assert_eq!(described_terms(format!("{} Adding file with id 0101 and more\n", written).as_bytes()), None);