@@ -6,33 +6,38 @@
6use crate::config::Config;
6use crate::config::Config;
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
8use crate::eval::Interp;
8use crate::eval::Interp;
9−use crate::value::{BlobContent, BlobKind, BlobVal, Crash, LazyBlob, Value};
9+use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, LazyBlob, ListVal, ThunkVal, Value};
10+use futures::{StreamExt, TryStreamExt};
10use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
11use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
11use jj_lib::commit::Commit;
12use jj_lib::commit::Commit;
12use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
13use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14+use jj_lib::conflicts::ConflictMarkerStyle;
13use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
15use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
14use jj_lib::gitignore::GitIgnoreFile;
16use jj_lib::gitignore::GitIgnoreFile;
17+use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
15−use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
18+use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
16use jj_lib::merge::{Diff, Merge};
19use jj_lib::merge::{Diff, Merge};
17use jj_lib::merged_tree::MergedTree;
20use jj_lib::merged_tree::MergedTree;
18use jj_lib::merged_tree_builder::MergedTreeBuilder;
21use jj_lib::merged_tree_builder::MergedTreeBuilder;
19−use jj_lib::backend::{CommitId, FileId};
22+use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
20use jj_lib::object_id::ObjectId;
23use jj_lib::object_id::ObjectId;
21use jj_lib::op_store::OperationId;
24use jj_lib::op_store::OperationId;
22use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
25use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
23use jj_lib::repo::{ReadonlyRepo, Repo};
26use jj_lib::repo::{ReadonlyRepo, Repo};
24−use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponentBuf};
27+use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent, RepoPathComponentBuf};
28+use jj_lib::revset::{ResolvedRevsetExpression, RevsetStreamExt};
25use jj_lib::settings::UserSettings;
29use jj_lib::settings::UserSettings;
26use jj_lib::store::Store;
30use jj_lib::store::Store;
31+use jj_lib::transaction::UnpublishedOperation;
27−use jj_lib::working_copy::SnapshotOptions;
32+use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
28use jj_lib::workspace::Workspace;
33use jj_lib::workspace::Workspace;
29−use std::cell::RefCell;
34+use std::cell::{Cell, RefCell};
30use std::collections::{BTreeMap, BTreeSet, HashMap};
35use std::collections::{BTreeMap, BTreeSet, HashMap};
31use std::future::Future;
36use std::future::Future;
32use std::os::unix::io::AsRawFd;
37use std::os::unix::io::AsRawFd;
33use std::path::PathBuf;
38use std::path::PathBuf;
34−use std::rc::Rc;
39+use std::rc::{Rc, Weak};
35−use std::sync::{Arc, Mutex};
40+use std::sync::{Arc, Mutex, OnceLock};
37fn block_on<F: Future>(f: F) -> F::Output {
42fn block_on<F: Future>(f: F) -> F::Output {
38 pollster::block_on(f)
43 pollster::block_on(f)
@@ -40,6 +45,15 @@ fn block_on<F: Future>(f: F) -> F::Output {
41type OpenError = (u8, String);
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)]
51+pub struct PushOutcome {
52+ pub published: usize,
54+ pub unchanged: usize,
43// ----------------------------------------------------------------------
57// ----------------------------------------------------------------------
44// locating and opening the repository (§7.1)
58// locating and opening the repository (§7.1)
45// ----------------------------------------------------------------------
59// ----------------------------------------------------------------------
@@ -64,6 +78,7 @@ fn toml_string(s: &str) -> String {
64 '\n' => out.push_str("\\n"),
78 '\n' => out.push_str("\\n"),
65 '\r' => out.push_str("\\r"),
79 '\r' => out.push_str("\\r"),
66 '\t' => out.push_str("\\t"),
80 '\t' => out.push_str("\\t"),
81+ c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
67 c => out.push(c),
82 c => out.push(c),
@@ -252,31 +267,1166 @@ pub struct JjInner {
252 repo: Mutex<Arc<ReadonlyRepo>>,
267 repo: Mutex<Arc<ReadonlyRepo>>,
253 workspace_name: WorkspaceNameBuf,
268 workspace_name: WorkspaceNameBuf,
254 /// visible map backing the `Backend` lookups during evaluation; filled by
269 /// visible map backing the `Backend` lookups during evaluation; filled by
255− /// build_interp with the post-snapshot map
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)
256 visible: Mutex<Option<Arc<VisibleRepo>>>,
272 visible: Mutex<Option<Arc<VisibleRepo>>>,
257 lock_guard: Mutex<Option<FileLock>>,
273 lock_guard: Mutex<Option<FileLock>>,
258− /// the snapshot transaction and working-copy lock, held between
274+ /// the snapshot's unpublished rewrite, kept between build_interp and
259− /// build_interp and persist so the snapshot and the expression's edits
275+ /// persist so the snapshot and the expression's edits form one jj
260− /// form one jj operation (§1.2); dropped uncommitted by printing runs
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
261 pending: Mutex<Option<PendingSnapshot>>,
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>>,
264/// holding the file keeps the flock
289/// holding the file keeps the flock
265pub struct FileLock(#[allow(dead_code)] std::fs::File);
290pub struct FileLock(#[allow(dead_code)] std::fs::File);
268struct PendingSnapshot {
292struct PendingSnapshot {
269 /// repo as loaded at head, before the snapshot
293 /// repo as loaded at head, before the snapshot
270 pre_repo: Arc<ReadonlyRepo>,
294 pre_repo: Arc<ReadonlyRepo>,
271− /// the snapshot's rewritten wc commit and tree, to be folded into the
295+ /// the snapshot's tree for the wc commit, to be folded into the
272 /// persisting operation (§1.2 step 8, §7.7)
296 /// persisting operation (§1.2 step 8, §7.7)
273− new_wc_id: CommitId,
274 tree: MergedTree,
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
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.
312+fn snapshot_options() -> SnapshotOptions<'static> {
314+ base_ignores: GitIgnoreFile::empty(),
316+ start_tracking_matcher: &EverythingMatcher,
317+ force_tracking_matcher: &NothingMatcher,
318+ max_new_file_size: u64::MAX,
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):
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.
340+ wc: &mut dyn LockedWorkingCopy,
341+ root: &std::path::Path,
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?;
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() {
368+ "cannot record the file `{}` in place of a directory holding a conflict",
369+ path.as_internal_file_string()
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.
386+ wc: &mut dyn LockedWorkingCopy,
387+ root: &std::path::Path,
389+ options: &SnapshotOptions<'_>,
390+) -> Result<MergedTree, String> {
391+ let err = match wc.snapshot(options).await {
392+ Ok((scanned, _stats)) => return Ok(scanned),
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());
399+ if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400+ return Err(err.to_string());
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 {
409+ if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410+ gone.push((path, Merge::absent()));
413+ if gone.is_empty() {
414+ return Err(err.to_string());
416+ reset_paths(wc, state, gone).await?;
417+ let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
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.
425+async fn reset_paths(
426+ wc: &mut dyn LockedWorkingCopy,
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);
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())?;
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
443+struct 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
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.
462+ settings: UserSettings,
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"),
474+ SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
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))
489+/// A modification time in milliseconds since the epoch, as jj's working
491+fn 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),
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.
503+fn 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.
516+fn 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') {
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),
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(')')) {
537+ removes.retain(|term| match adds.iter().position(|t| t == term) {
539+ adds.swap_remove(n);
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.
556+fn files_on_conflicts(
557+ root: &std::path::Path,
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,
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());
580+ let is_dir = meta.is_some_and(|m| m.is_dir());
581+ dirs.insert(dir.to_owned(), is_dir);
585+ let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
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))
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),
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))
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))
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 {
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);
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`).
654+async fn snapshot_tree(
655+ root: &std::path::Path,
656+ markers: ConflictMarkerStyle,
660+ scanned: &MergedTree,
661+) -> Result<MergedTree, String> {
662+ if state.tree_ids() == wc.tree_ids() {
663+ return Ok(scanned.clone());
666+ return conflicts_written(root, markers, wc, state, scanned).await;
668+ if scanned.tree_ids() == state.tree_ids() {
669+ return Ok(wc.clone());
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);
680+ // the base: the state, but for what the directory changed as the
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);
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())],
696+ (wc.clone(), "working-copy commit".to_string()),
697+ (scanned.clone(), "working directory".to_string()),
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)? {
720+ kept.set_or_remove(path, held);
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.
740+async fn conflicts_written(
741+ root: &std::path::Path,
742+ markers: ConflictMarkerStyle,
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
757+ let Ok(disk) = path.to_fs_path(root) else {
760+ if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761+ tree.set_or_remove(path, value);
766+ return Ok(scanned.clone());
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
778+async fn holds_written(
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() })
792+ let written = conflicts::materialize_tree_value(store, path, value, labels)
794+ .map_err(|e| e.to_string())?;
796+ MaterializedTreeValue::Symlink { target, .. } => {
797+ Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
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))
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(),
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))
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))
822+/// How the working copy writes a conflict, as `settings` say
823+fn 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`.
834+ root: &std::path::Path,
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,
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);
850+ if !stands.is_some_and(|t| t.is_dir()) {
851+ return Some((dir.to_owned(), stands));
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.
864+async fn written_over(
865+ on_disk: &MergedTree,
866+ rescanned: &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));
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
888+ holder: RepoPathBuf,
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)
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 {
902+ "`{}` (a directory holding untracked files, such as `{}`)",
903+ self.path.as_internal_file_string(),
904+ self.holder.as_internal_file_string()
907+ format!("`{}`", self.holder.as_internal_file_string())
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
915+fn in_the_way_of(in_the_way: &[InTheWay]) -> String {
916+ const LISTED: usize = 10;
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()
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()
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),
935+ "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
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`.
961+async fn look_where_added(
962+ root: &std::path::Path,
963+ markers: ConflictMarkerStyle,
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(_))) {
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();
987+ in_the_way.push(InTheWay { path, holder: dir });
992+ let Ok(disk) = path.to_fs_path(root) else {
995+ let Ok(meta) = disk.symlink_metadata() else {
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? {
1006+ Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007+ None => clear.push(path),
1010+ if in_the_way.is_empty() {
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.
1022+fn 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 {
1029+ for entry in entries {
1030+ let Ok(entry) = entry else {
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 {
1038+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039+ dirs.push((entry.path(), below));
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()
1046+ if !tracked.contains(&below) {
1047+ return Some(below);
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.
1064+async fn clear_the_way(
1065+ root: &std::path::Path,
1066+ markers: ConflictMarkerStyle,
1068+ paths: &[RepoPathBuf],
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() {
1086+ let Ok(disk) = path.to_fs_path(root) else {
1089+ let Ok(meta) = disk.symlink_metadata() else {
1096+ let Ok(value) = to.path_value(path).await else {
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));
1108+/// The start of the name of each directory in `.jj` that `Aside` moves
1110+const 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`).
1127+ root: std::path::PathBuf,
1128+ dir: Option<tempfile::TempDir>,
1129+ stuck: Option<(RepoPathBuf, std::io::Error)>,
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 {
1139+ none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
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)
1149+impl 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
1165+fn 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)
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()) {
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(),
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 {
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()) {
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
1212+fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1214+ use std::os::unix::fs::OpenOptionsExt;
1215+ let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1218+ if !stands.is_file() {
1221+ if copy.is_symlink() {
1222+ return stands.len() == 0;
1224+ if !copy.is_file() || stands.len() >= copy.len() {
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 {
1231+ let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232+ let mut rest = stands.len();
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];
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.
1251+fn put_back_left(root: &std::path::Path) {
1252+ let Ok(entries) = std::fs::read_dir(root.join(".jj")) else {
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.
1270+async fn first_skipped(
1271+ root: &std::path::Path,
1272+ markers: ConflictMarkerStyle,
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(_))) {
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));
1290+ let Ok(disk) = path.to_fs_path(root) else {
1293+ let Ok(meta) = disk.symlink_metadata() else {
1297+ || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1299+ return Ok(Some(path));
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)
1312+ mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313+ scanned: &MergedTree,
1315+ head: &OperationId,
1316+) -> Result<(), String> {
1317+ let op_id = if scanned.tree_ids() == wc.tree_ids() {
1320+ locked_ws.locked_wc().old_operation_id().clone()
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
1327+static DEFERRED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1329+extern "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
1344+ previous: Vec<(libc::c_int, libc::sigaction)>,
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
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
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 }
1373+impl 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
1386+pub fn raise_deferred() {
1387+ let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
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.
1403+fn 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()))
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.
1413+fn with_tree(commit: &Commit, tree: MergedTree) -> Commit {
1414+ let (tree_ids, labels) = tree.into_tree_ids_and_labels();
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()
277#[derive(Clone)]
1426#[derive(Clone)]
278pub struct JjBackend {
1427pub struct JjBackend {
279 inner: Arc<JjInner>,
1428 inner: Arc<JjInner>,
1429+ replay_cache: Rc<ReplayCache>,
282impl JjBackend {
1432impl JjBackend {
@@ -284,7 +1434,7 @@ impl JjBackend {
284 let root = find_repo_root().ok_or_else(|| {
1434 let root = find_repo_root().ok_or_else(|| {
287− "no jj repository found in this directory or any parent".to_string(),
1437+ "no jj repository found in this directory or any parent; run `j init` here, `j clone URL`, or `j help`".to_string(),
290 let settings = user_settings(cfg);
1440 let settings = user_settings(cfg);
@@ -305,10 +1455,22 @@ impl JjBackend {
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()) {
1466+ "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
308 let workspace_name = workspace.workspace_name().to_owned();
1469 let workspace_name = workspace.workspace_name().to_owned();
309 let repo = block_on(workspace.repo_loader().load_at_head())
1470 let repo = block_on(workspace.repo_loader().load_at_head())
310 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
1471 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
311 Ok(JjBackend {
1472 Ok(JjBackend {
1473+ replay_cache: Rc::new(ReplayCache::default()),
312 inner: Arc::new(JjInner {
1474 inner: Arc::new(JjInner {
313 workspace_root: root,
1475 workspace_root: root,
314 workspace: Mutex::new(workspace),
1476 workspace: Mutex::new(workspace),
@@ -317,6 +1479,8 @@ impl JjBackend {
317 visible: Mutex::new(None),
1479 visible: Mutex::new(None),
318 lock_guard: Mutex::new(None),
1480 lock_guard: Mutex::new(None),
319 pending: Mutex::new(None),
1481 pending: Mutex::new(None),
1482+ folds_case: OnceLock::new(),
1483+ long_names: Mutex::new(HashMap::new()),
@@ -336,21 +1500,22 @@ impl JjBackend {
337 Ok(f) => f,
1501 Ok(f) => f,
338 Err(e) => {
1502 Err(e) => {
339− eprintln!("j: cannot create {}: {}", lock_path.display(), e);
1503+ crate::terminal::report_error(format_args!("j: cannot create {}: {}", lock_path.display(), e));
340 std::process::exit(2);
1504 std::process::exit(2);
343 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
1507 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
344 if rc != 0 {
1508 if rc != 0 {
345− eprintln!("j: another j is running in this repository");
1509+ crate::terminal::report_error(format_args!("j: another j is running in this repository"));
346 std::process::exit(2);
1510 std::process::exit(2);
348 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1512 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513+ put_back_left(&self.inner.workspace_root);
351 pub fn build_interp(
1516 pub fn build_interp(
353− cfg: &Config,
1518+ cfg: &mut Config,
354 _text: &str,
1519 _text: &str,
355 snapshot: bool,
1520 snapshot: bool,
356 ) -> Result<(Interp, Value, Value), OpenError> {
1521 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -359,8 +1524,8 @@ impl JjBackend {
359 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
1524 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
360 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
1525 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
361 *self.inner.repo.lock().unwrap() = head_repo;
1526 *self.inner.repo.lock().unwrap() = head_repo;
362− let loaded_visible = block_on(read_visible(&self.current_repo()))?;
1527+ let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
363− let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id)?;
1528+ let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
365 let current = if snapshot {
1530 let current = if snapshot {
366 // §7.4: snapshot the working directory into the wc commit; the
1531 // §7.4: snapshot the working directory into the wc commit; the
@@ -371,19 +1536,19 @@ impl JjBackend {
371 // no working-copy change: the repo is the one already loaded,
1536 // no working-copy change: the repo is the one already loaded,
372 // so reuse its value instead of rebuilding it
1537 // so reuse its value instead of rebuilding it
373 None => {
1538 None => {
374− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1539+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
375 loaded.clone()
1540 loaded.clone()
377 Some(pending) => {
1542 Some(pending) => {
378 *self.inner.pending.lock().unwrap() = Some(pending);
1543 *self.inner.pending.lock().unwrap() = Some(pending);
379 let vis = block_on(read_visible(&post))?;
1544 let vis = block_on(read_visible(&post))?;
380− let cur = build_repo_value(&vis, &vis.wc_change_id)?;
1545+ let cur = build_repo_value(&vis, &vis.wc_change_id, &self.replay_cache)?;
381 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
1546 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
385 } else {
1550 } else {
386− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1551+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
387 loaded.clone()
1552 loaded.clone()
@@ -392,35 +1557,59 @@ impl JjBackend {
392 cfg.shapes.clone(),
1557 cfg.shapes.clone(),
393 crate::value::Env::empty(),
1558 crate::value::Env::empty(),
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()
1568+ "config.j: `@{}` in `{}` does not resolve to a unique commit",
395 // §7.2: if the focused commit is in the immutable set, the snapshot
1573 // §7.2: if the focused commit is in the immutable set, the snapshot
396 // cannot go into it; the focus is a new empty child of it holding the
1574 // cannot go into it; the focus is a new empty child of it holding the
397 // snapshot, and persisting records that child
1575 // snapshot, and persisting records that child
398− match self.focus_if_immutable(&mut interp, cfg, ¤t)? {
1576+ match self.focus_if_immutable(&mut interp, cfg, &loaded, ¤t)? {
399− Some(adjusted) => {
1577+ Some((loaded, current)) => {
400− // the persisted child is part of the loaded value too, so a
1578+ // the snapshot's rewrite of the wc commit (and its rebase of
401− // run that changes nothing is a no-op
1579+ // the descendants) is not the repository's: lookups answer
402− Ok((interp, adjusted.clone(), adjusted))
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))
404 None => Ok((interp, loaded, current)),
1588 None => Ok((interp, loaded, current)),
408− /// If the focus of `current` is immutable, return a repo refocused on a
1592+ /// If the focus of `current` is immutable, return the loaded and current
409− /// new empty child holding the focus's files (§7.2).
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
410 fn focus_if_immutable(
1599 fn focus_if_immutable(
412 interp: &mut Interp,
1601 interp: &mut Interp,
413 cfg: &Config,
1602 cfg: &Config,
414 current: &Value,
1604 current: &Value,
415− ) -> Result<Option<Value>, OpenError> {
1605+ ) -> Result<Option<(Value, Value)>, OpenError> {
416− // evaluate the config to compute the immutable set
1606+ // Evaluate the config to compute the immutable set. This uses the
417− let mut probe = Interp::new(
1607+ // interpreter the run will go on to use, rather than a throwaway one,
418− Rc::new(self.clone()),
1608+ // so that the `immutable` (and the `trunk` inside it) computed here is
419− cfg.shapes.clone(),
1609+ // reused when rendering or persisting asks for the same Repo value —
420− crate::value::Env::empty(),
1610+ // each is a full interpreted walk of the history.
422− crate::config::eval_config(&mut probe, cfg).map_err(|c| (3, c.msg))?;
1611+ crate::config::eval_config(interp, cfg).map_err(|c| (3, c.msg))?;
423− let immutable = crate::repo::compute_immutable(&mut probe, current)
1612+ let immutable = crate::repo::compute_immutable(interp, current)
424 .map_err(|c| (1, c.msg))?;
1613 .map_err(|c| (1, c.msg))?;
425 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
1614 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
426 Ok(Value::Id(i)) => i.to_string(),
1615 Ok(Value::Id(i)) => i.to_string(),
@@ -429,90 +1618,101 @@ impl JjBackend {
429 if !immutable.contains(&focus_id) {
1618 if !immutable.contains(&focus_id) {
430 return Ok(None);
1619 return Ok(None);
432− // build: new empty child of the focus with the focus's files, minted id
1621+ // the focus's location in `loaded`, pre-snapshot; the child is its
433− let _ = interp;
1622+ // last child, so every existing child sits to the left of it
434− let root = current.field("root").map_err(|c| (1, c.msg))?;
1623+ let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
435− let files = root.field("files").map_err(|c| (1, c.msg))?;
1624+ let root = field(loaded, "root")?;
436− let child = crate::value::Value::record(&[
1625+ let frame = Value::record(&[
437− ("files", files),
1626+ ("left", field(loaded, "children")?),
438− ("message", crate::value::Value::text("")),
439− ("labels", crate::value::Value::list(vec![])),
442− crate::value::Value::Id(Rc::new(probe.mint_id())),
445− let child_subtree = crate::value::Value::record(&[
446− ("root", child.clone()),
1627+ ("parent", root.clone()),
447− ("children", crate::value::Value::list(vec![])),
1628+ ("right", Value::list(vec![])),
449− let mut new_children: Vec<Value> = current
1630+ let mut frames = vec![frame];
450− .field("children")
1631+ frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
451− .map_err(|c| (1, c.msg))?
1632+ let context = Value::list(frames);
452− .as_list()
1633+ let id = Value::Id(Rc::new(interp.mint_id()));
453− .map_err(|c| (1, c.msg))?
1634+ let at_child = |files: Value| {
454− .to_vec();
1635+ Value::record(&[
455− new_children.push(child_subtree);
1636+ ("children", Value::list(vec![])),
456− let parent = crate::value::Value::record(&[
1637+ ("context", context.clone()),
457− ("children", crate::value::Value::list(new_children)),
1638+ (
458− ("context", current.field("context").map_err(|c| (1, c.msg))?),
1639+ "root",
459− ("root", root),
1640+ Value::record(&[
460− ]);
1641+ ("files", files),
461− // refocus onto the child (it is the last child)
1642+ ("id", id.clone()),
462− let child_id = match child.field("id").map_err(|c| (1, c.msg))? {
1643+ ("labels", Value::list(vec![])),
463− Value::Id(i) => i.to_string(),
1644+ ("message", Value::text("")),
464− _ => unreachable!(),
1645+ ]),
466− crate::repo::by_id(&parent, &child_id)
1649+ let current_at_child = at_child(field(&field(current, "root")?, "files")?);
467− .map_err(|c| (1, c.msg))?
1650+ // with nothing snapshotted `current` is `loaded` itself: share one
468− .ok_or_else(|| (1, "internal: cannot refocus".to_string()))
1651+ // value, so a run that changes nothing is a no-op without comparing
469− .map(Some)
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")?),
1657+ Ok(Some((loaded_at_child, current_at_child)))
472− /// Snapshot the working copy into the wc commit; returns the repo at the
1660+ /// Snapshot the working directory into the working-copy commit (§7.4);
473− /// snapshot operation (unpublished but written), or the pre-snapshot repo
1661+ /// return the repo at the snapshot's operation, written but not
474− /// if nothing changed.
1662+ /// published, or the repo as loaded when there is nothing to record.
475 async fn snapshot_repo(
1663 async fn snapshot_repo(
477 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
1665 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
478 let mut ws_guard = self.inner.workspace.lock().unwrap();
1666 let mut ws_guard = self.inner.workspace.lock().unwrap();
479− let loader = ws_guard.repo_loader().clone();
1667+ let root = ws_guard.workspace_root().to_owned();
1668+ let saved = SavedState::of(&ws_guard);
480 let mut locked_ws = ws_guard.start_working_copy_mutation()
1669 let mut locked_ws = ws_guard.start_working_copy_mutation()
482 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1671 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
483− let options = SnapshotOptions {
1672+ let old_tree = locked_ws.locked_wc().old_tree().clone();
484− base_ignores: GitIgnoreFile::empty(),
1673+ let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
486− start_tracking_matcher: &EverythingMatcher,
487− force_tracking_matcher: &NothingMatcher,
488− max_new_file_size: u64::MAX,
490− let (new_tree, _stats) = locked_ws
494 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1675 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
495− let changed = new_tree.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids();
1676+ // the repo build_interp loaded at head from the workspace's own
496− if !changed {
1677+ // loader, whose store the scanned tree lives in
497− locked_ws
1678+ let head = self.current_repo();
498− .finish(self.current_repo().operation().id().clone())
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,
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())
500 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1694 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
501− return Ok((self.current_repo(), None));
1695+ return Ok((head, None));
503− // the snapshotted tree lives in the workspace's store; drive the
1697+ // a directory come to hold the parent's files, as where it reverts
504− // transaction from the workspace's repo loader so stores match
1698+ // the commit's change, is recorded with the parent's tree (§7.4), as
505− let ws_repo = loader
1699+ // persistence writes a commit with its parent's files (§7.5 step 4):
506− .load_at_head()
1700+ // the snapshot builds on the commit's tree, and so could make
507− .await
1701+ // another tree for the parent's files, one jj takes for a change.
508− .map_err(|e| (2, format!("cannot reload the repository: {}", e)))?;
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,
509− let mut tx = ws_repo.start_transaction();
1710+ let mut tx = head.start_transaction();
510 tx.set_is_snapshot(true);
1711 tx.set_is_snapshot(true);
511− let wc_commit = self.wc_commit(&ws_repo).await?;
512 let new_wc = tx
1712 let new_wc = tx
513 .repo_mut()
1713 .repo_mut()
514 .rewrite_commit(&wc_commit)
1714 .rewrite_commit(&wc_commit)
515− .set_tree(new_tree)
1715+ .set_tree(tree)
516 .write()
1716 .write()
518 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
1718 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
@@ -529,25 +1729,26 @@ impl JjBackend {
529 .write("snapshot working copy")
1729 .write("snapshot working copy")
531 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
1731 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
532− let op_id = unpublished.operation().id().clone();
533 let new_repo = unpublished.leave_unpublished();
1732 let new_repo = unpublished.leave_unpublished();
534− // the working-copy state tracks the snapshot tree, against the
1733+ // Release the lock *without* finishing: `finish` saves the scanned
535− // current (published) head; persist will finish against its own op
1734+ // tree state to disk, which would record the working directory as
536− locked_ws
1735+ // already snapshotted while the published head still has the old wc
537− .finish(self.current_repo().operation().id().clone())
1736+ // tree. The next run's incremental snapshot would then find nothing
538− .await
1737+ // to do and build the Repo from the stale tree — and a persisting run
539− .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1738+ // would check that stale tree back out, destroying the edits. The
540− let _ = op_id;
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).
541 let pending = PendingSnapshot {
1743 let pending = PendingSnapshot {
542− pre_repo: self.current_repo(),
1744+ pre_repo: head,
543− new_wc_id: new_wc.id().clone(),
544 tree: new_wc.tree().clone(),
1745 tree: new_wc.tree().clone(),
1746+ fold_into_wc: true,
546 Ok((new_repo, Some(pending)))
1749 Ok((new_repo, Some(pending)))
551 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
1752 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
552 let id = repo
1753 let id = repo
@@ -565,12 +1766,13 @@ impl JjBackend {
565 interp: &mut Interp,
1766 interp: &mut Interp,
566 cfg: &Config,
1767 cfg: &Config,
567 loaded: &Value,
1768 loaded: &Value,
568− _current: &Value,
1769+ current: &Value,
569 new: &Value,
1770 new: &Value,
570 text: &str,
1771 text: &str,
571− ) -> Result<(), Crash> {
1772+ ) -> Result<bool, Crash> {
1773+ // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
572− if crate::value::value_eq(loaded, new)? {
1774+ if crate::repo::same_repo(loaded, new)? {
573− return Ok(());
1775+ return Ok(false);
575 *interp.old_repo.borrow_mut() = Some(loaded.clone());
1777 *interp.old_repo.borrow_mut() = Some(loaded.clone());
576 crate::repo::validate_repo(interp, new)?;
1778 crate::repo::validate_repo(interp, new)?;
@@ -590,7 +1792,7 @@ impl JjBackend {
590 // walk below compares against its output, so the snapshot change and
1792 // walk below compares against its output, so the snapshot change and
591 // the expression's edits become one rewrite
1793 // the expression's edits become one rewrite
592 let mut snapshot_wc: Option<Commit> = None;
1794 let mut snapshot_wc: Option<Commit> = None;
593− if let Some(p) = &pending {
1795+ if let Some(p) = pending.as_ref().filter(|p| p.fold_into_wc) {
594 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
1796 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
595 let c = block_on(
1797 let c = block_on(
596 tx.repo_mut()
1798 tx.repo_mut()
@@ -614,6 +1816,9 @@ impl JjBackend {
614 old_stored = Arc::new(vis);
1816 old_stored = Arc::new(vis);
616 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
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])?;
617 // shared across the whole persist walk so unchanged blobs are inflated once
1822 // shared across the whole persist walk so unchanged blobs are inflated once
618 let cache = BlobCache::default();
1823 let cache = BlobCache::default();
619 let entries = EntryCache::default();
1824 let entries = EntryCache::default();
@@ -637,33 +1842,35 @@ impl JjBackend {
638 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
1843 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
639 .map_err(|e| Crash::new(e.msg))?;
1844 .map_err(|e| Crash::new(e.msg))?;
640− if !crate::value::value_eq(
641− &Value::list(stored_files),
1845+ if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
642− &Value::list(root_v.field("files")?.as_list()?.to_vec()),
644 return Err(Crash::new("persistence: the root commit cannot be changed"));
1846 return Err(Crash::new("persistence: the root commit cannot be changed"));
647 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
1849 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
649 // walk `new` top-down (§7.5 step 4), parents before children
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)> =
650− let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
1855+ std::collections::VecDeque::new(); // (subtree value, parent change id)
651 for kid in top.field("children")?.as_list()?.iter() {
1856 for kid in top.field("children")?.as_list()?.iter() {
652− queue.push((kid.clone(), ROOT_ID.to_string()));
1857+ queue.push_back((kid.clone(), ROOT_ID.to_string()));
654− while !queue.is_empty() {
655− let (subtree, parent_change) = queue.remove(0);
1859+ while let Some((subtree, parent_change)) = queue.pop_front() {
656 let commit_v = subtree.field("root")?;
1860 let commit_v = subtree.field("root")?;
657 let id = commit_id_of(&commit_v)?;
1861 let id = commit_id_of(&commit_v)?;
658 let parent_jj = written
1862 let parent_jj = written
659 .get(&parent_change)
1863 .get(&parent_change)
660 .cloned()
1864 .cloned()
661 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
1865 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
662− let files_v = commit_v.field("files")?;
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)?;
663 let message = commit_v.field("message")?.as_text()?.to_string();
1869 let message = commit_v.field("message")?.as_text()?.to_string();
664 let new_id = match old_stored.commits.get(&id) {
1870 let new_id = match old_stored.commits.get(&id) {
665 None => {
1871 None => {
666− let tree = block_on(build_tree(&store, &files_v))?;
1872+ let tree =
1873+ loaded_trees.tree_to_write(tx.repo(), &files_v, std::slice::from_ref(&parent_jj))?;
667 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
1874 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
668 Crash::new(format!("persistence: `{}` is not a valid change id", id))
1875 Crash::new(format!("persistence: `{}` is not a valid change id", id))
@@ -682,17 +1889,31 @@ impl JjBackend {
682 Some(stored) => {
1889 Some(stored) => {
683 let stored_commit = &stored.commit;
1890 let stored_commit = &stored.commit;
684 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
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
685− let files_changed = {
1896+ let files_changed = crate::repo::stored_tree(&files_v)
1897+ != Some(tree_name(&stored_tree).as_str())
686− let stored_files =
1899+ let stored_files =
687− block_on(tree_to_files(&store, &stored_commit.tree(), &cache, &entries))?;
1900+ block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
688− !crate::value::value_eq(
1901+ // a tree has no order: entries listed differently
1902+ // are not a change to rewrite (§7.3)
689− &Value::list(stored_files),
1903+ !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
690− &Value::list(files_v.as_list()?.to_vec()),
693 let msg_changed = stored_commit.description() != message;
1905 let msg_changed = stored_commit.description() != message;
694 if parent_changed || files_changed || msg_changed {
1906 if parent_changed || files_changed || msg_changed {
695− let tree = block_on(build_tree(&store, &files_v))?;
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)?
1915+ block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
696 let c = block_on(
1917 let c = block_on(
697 tx.repo_mut()
1918 tx.repo_mut()
698 .rewrite_commit(stored_commit)
1919 .rewrite_commit(stored_commit)
@@ -711,7 +1932,7 @@ impl JjBackend {
712 written.insert(id.clone(), new_id);
1933 written.insert(id.clone(), new_id);
713 for kid in subtree.field("children")?.as_list()?.iter() {
1934 for kid in subtree.field("children")?.as_list()?.iter() {
714− queue.push((kid.clone(), id.clone()));
1935+ queue.push_back((kid.clone(), id.clone()));
@@ -755,37 +1976,230 @@ impl JjBackend {
755 let desc = truncate_chars(text, 200);
1976 let desc = truncate_chars(text, 200);
756 let unpublished = block_on(tx.write(desc))
1977 let unpublished = block_on(tx.write(desc))
757 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
1978 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
758− let op_id = unpublished.operation().id().clone();
759− let _new_repo = block_on(unpublished.publish())
760− .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
762− // §7.4/§7.5 step 7: check out the focus to the working directory
1980+ // §7.4/§7.5 step 7: publish the operation, then check the focus out
763 let focus_commit = block_on(store.get_commit_async(&focus_jj))
1981 let focus_commit = block_on(store.get_commit_async(&focus_jj))
764 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
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,
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)))?
765− block_on(self.checkout(&focus_commit, op_id))?;
2000+ block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
767− Ok(())
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).
770− async fn checkout(&self, commit: &Commit, op_id: OperationId) -> Result<(), Crash> {
2025+ async fn checkout(
2028+ on_disk: Option<&MergedTree>,
2029+ op: Option<UnpublishedOperation>,
2030+ back: Option<&str>,
2031+ ) -> Result<(), Crash> {
771 let mut ws_guard = self.inner.workspace.lock().unwrap();
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);
772 let mut locked_ws = ws_guard.start_working_copy_mutation()
2035 let mut locked_ws = ws_guard.start_working_copy_mutation()
774 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
2037 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
775− locked_ws
2038+ // A persisting run's snapshot released its lock without saving what
776− .locked_wc()
2039+ // it scanned, so the state just loaded may still describe the tree
777− .check_out(commit)
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
2061+ Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
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()
2069+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2071+ // or it can no longer be scanned (an entry appeared,
2072+ // vanished or cannot be read; a failed scan leaves the state
2076+ // Otherwise the state describes `on_disk` instead. A reset
2077+ // writes no file; the paths it changes are re-read by the next
2082+ .reset(&with_tree(commit, on_disk.clone()))
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())
779 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
2095 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
780− locked_ws
2096+ let clear = match on_disk {
781− .finish(op_id)
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))),
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(),
783− .map_err(|e| Crash::new(format!("cannot finish the checkout: {}", e)))?;
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;
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),
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
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()),
2146+ locked_ws = match saved {
2147+ Ok(locked) => locked,
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))));
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)))),
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
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()
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(),
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
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))));
787 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
2200 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
788− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)?;
2201+ let (name, email) = validated_identity(cfg)
2202+ .map_err(|(_, message)| Crash::new(message))?;
789 Ok(Signature {
2203 Ok(Signature {
@@ -878,6 +2292,7 @@ impl JjBackend {
880 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2294 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295+ let url = &origin_url(url)?;
881 self.take_lock();
2296 self.take_lock();
882 let base = self.current_repo();
2297 let base = self.current_repo();
883 let origin = RemoteName::new("origin");
2298 let origin = RemoteName::new("origin");
@@ -895,7 +2310,17 @@ impl JjBackend {
895 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
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)))
898 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2322 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323+ self.enter_workspace_root()?;
899 let origin = RemoteName::new("origin");
2324 let origin = RemoteName::new("origin");
900 let base = self.head_repo()?;
2325 let base = self.head_repo()?;
901 let mut tx = base.start_transaction();
2326 let mut tx = base.start_transaction();
@@ -908,27 +2333,52 @@ impl JjBackend {
908 let _ = block_on(self.publish_tx(tx, "fetch"))?;
2333 let _ = block_on(self.publish_tx(tx, "fetch"))?;
911− pub fn cmd_push(&self, cfg: &Config, expr_text: &str) -> Result<(), OpenError> {
2336+ pub fn cmd_push(&self, cfg: &mut Config, expr_text: &str) -> Result<PushOutcome, OpenError> {
2337+ self.enter_workspace_root()?;
912 let origin = RemoteName::new("origin");
2338 let origin = RemoteName::new("origin");
913 let base = self.head_repo()?;
2339 let base = self.head_repo()?;
914− let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
2340+ let (value, vis, immutable) = self.eval_push_expr(cfg, expr_text, &base)?;
915 let records = parse_push_records(&value, &vis, &base)?;
2341 let records = parse_push_records(&value, &vis, &base)?;
2342+ if records.is_empty() {
2343+ return Ok(PushOutcome::default());
916− check_push_records(&records, &vis, &base)?;
2345+ let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
917− let _ = self.push_to_origin(&base, origin, &records, expr_text)?;
2346+ let mut outcome = PushOutcome::default();
918− Ok(())
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;
2356+ outcome.published += 1;
2359+ let (_, clean) = self.inspect_working_copy(&base, false)?;
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."
2365+ let description = format!("push {}", expr_text);
2366+ let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
921 /// evaluate `EXPR` against the recorded repository, without snapshotting
2370 /// evaluate `EXPR` against the recorded repository, without snapshotting
922− /// (§1.1), apply a function result to the repo value once (§7.6)
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)
923 fn eval_push_expr(
2373 fn eval_push_expr(
925− cfg: &Config,
2375+ cfg: &mut Config,
926 expr_text: &str,
2376 expr_text: &str,
927 base: &Arc<ReadonlyRepo>,
2377 base: &Arc<ReadonlyRepo>,
928− ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
2378+ ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
929 let outer = Rc::new(cfg.global_names.clone());
2379 let outer = Rc::new(cfg.global_names.clone());
930 let expr = crate::parse::parse_expr(expr_text, outer)
2380 let expr = crate::parse::parse_expr(expr_text, outer)
931− .map_err(|p| (3, format!("line {}: {}", p.line, p.msg)))?;
2381+ .map_err(|p| (3, crate::diagnostics::format_source_error("expression", expr_text, p.line, &p.msg)))?;
932 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
2382 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
933 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
2383 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
934 let (prefix, candidates) = &failures[0];
2384 let (prefix, candidates) = &failures[0];
@@ -939,15 +2389,30 @@ impl JjBackend {
941 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
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, ¤t)
2395+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2396+ let expression_error = |crash: Crash| {
2397+ let mut message = format!("crash: {}", crash.msg);
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));
942 let env = interp.global_env();
2407 let env = interp.global_env();
943 let expr_rc = Rc::new(expr);
2408 let expr_rc = Rc::new(expr);
944 let mut v = interp
2409 let mut v = interp
945 .eval(&expr_rc, &env)
2410 .eval(&expr_rc, &env)
946− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2411+ .map_err(&expression_error)?;
947 if matches!(v, Value::Fun(_)) {
2412 if matches!(v, Value::Fun(_)) {
948 v = interp
2413 v = interp
949 .apply(v, current)
2414 .apply(v, current)
950− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2415+ .map_err(expression_error)?;
952 let _ = base;
2417 let _ = base;
953 let vis = self
2418 let vis = self
@@ -957,19 +2422,28 @@ impl JjBackend {
957 .unwrap()
2422 .unwrap()
958 .clone()
2423 .clone()
959 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
2424 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
960− Ok((v, vis))
2425+ Ok((v, vis, immutable))
963 /// push the updates to `origin` and record the new remote-bookmark
2428 /// push the updates to `origin` and record the new remote-bookmark
964− /// positions as one operation (§7.6)
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
965 fn push_to_origin(
2432 fn push_to_origin(
967 base: &Arc<ReadonlyRepo>,
2434 base: &Arc<ReadonlyRepo>,
968 origin: &RemoteName,
2435 origin: &RemoteName,
969 records: &[(String, Option<CommitId>)],
2436 records: &[(String, Option<CommitId>)],
2437+ deletes_wait: bool,
970 description: &str,
2438 description: &str,
971 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
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());
972 let mut targets = jj_lib::git::GitPushRefTargets::default();
2445 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446+ let mut deletes = jj_lib::git::GitPushRefTargets::default();
973 for (name, after) in records {
2447 for (name, after) in records {
974 let before = base
2448 let before = base
@@ -978,48 +2452,116 @@ impl JjBackend {
978 .as_resolved()
2452 .as_resolved()
979 .cloned()
2453 .cloned()
980 .unwrap_or(None);
2454 .unwrap_or(None);
981− targets
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 };
982 .bookmarks
2459 .bookmarks
983 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
2460 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
985 let mut tx = base.start_transaction();
2462 let mut tx = base.start_transaction();
986 let subprocess_options = self.git_subprocess_options()?;
2463 let subprocess_options = self.git_subprocess_options()?;
987 let mut callback = QuietGitCallback;
2464 let mut callback = QuietGitCallback;
2465+ let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
988− let stats = jj_lib::git::push_refs(
2466+ jj_lib::git::push_refs(
989− tx.repo_mut(),
2467+ tx.repo_mut(),
990− subprocess_options,
2468+ subprocess_options.clone(),
991− origin,
2469+ origin,
992− &targets,
2470+ targets,
993− &mut callback,
2471+ &mut callback,
994− &jj_lib::git::GitPushOptions::default(),
2472+ &jj_lib::git::GitPushOptions::default(),
996− .map_err(|e| match e {
2474+ .map_err(|e| match e {
997− jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2475+ jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
998− other => (1, format!("push failed: {}", other)),
2476+ other => (1, format!("push failed: {}", other)),
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) {
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),
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)));
1000 if !stats.all_ok() {
2518 if !stats.all_ok() {
1001 let mut names: Vec<String> = Vec::new();
2519 let mut names: Vec<String> = Vec::new();
1002 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
2520 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
1003 names.push(name.as_str().to_string());
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");
1005− return Err((1, format!("push rejected: {}", names.join(", "))));
2535+ return Err((1, msg));
1007− let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1008− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1009− let repo = block_on(unpublished.publish())
1010− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1011− *self.inner.repo.lock().unwrap() = repo.clone();
1012 Ok(repo)
2537 Ok(repo)
1015− pub fn cmd_undo(&self, redo: bool) -> Result<(), OpenError> {
2540+ pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
1016 let base = self.head_repo()?;
2541 let base = self.head_repo()?;
1017− self.check_wc_clean(&base)?;
2542+ let on_disk = self.check_wc_clean(&base)?;
1018 let loader = self.repo_loader();
2543 let loader = self.repo_loader();
1019 let mut cur = base.operation().clone();
2544 let mut cur = base.operation().clone();
1020 let target_op_id: OperationId;
2545 let target_op_id: OperationId;
1022− match (op_marker(cur.metadata()), redo) {
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)))?;
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()));
2561+ target_op_id = parent;
2564+ match (marker, redo) {
1023 (None, false) => {
2565 (None, false) => {
1024 let Some(parent) = cur.parent_ids().first().cloned() else {
2566 let Some(parent) = cur.parent_ids().first().cloned() else {
1025 return Err((1, "nothing to undo".to_string()));
2567 return Err((1, "nothing to undo".to_string()));
@@ -1045,11 +2587,25 @@ impl JjBackend {
1045 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2587 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1047 (Some(m), true) if m.kind == "undo" => {
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.
1048− target_op_id = m.id;
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()))?
1051 (Some(m), false) => {
2602 (Some(m), false) => {
1052− // a redo: move past the undo it reversed
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.
1053 if !m.authored_by_j {
2609 if !m.authored_by_j {
1054 let Some(parent) = cur.parent_ids().first().cloned() else {
2610 let Some(parent) = cur.parent_ids().first().cloned() else {
1055 return Err((1, "nothing to undo".to_string()));
2611 return Err((1, "nothing to undo".to_string()));
@@ -1057,23 +2613,57 @@ impl JjBackend {
1057 target_op_id = parent;
2613 target_op_id = parent;
1060− cur = block_on(loader.load_operation(&m.id))
2616+ let undo_op = block_on(loader.load_operation(&m.id))
1061 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
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
1063 (Some(m), true) => {
2627 (Some(m), true) => {
1064− // a redo: reverse the undo it reversed, following further
2628+ // a redo: the undo it reversed is already accounted for,
1065− // redos to the operation ultimately reapplied
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").
1066− target_op_id = if m.authored_by_j {
2632+ if !m.authored_by_j {
1067− block_on(chain_through_redos(&loader, &m.id))?
2633+ target_op_id = m.id;
1068− } else {
2634+ break;
2636+ let Some(prev) = op_parent_id(&loader, &m.id)? else {
2637+ return Err((1, "nothing to redo".to_string()));
1071− break;
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()));
1075− let target_op = block_on(loader.load_operation(&target_op_id))
2651+ let mut target_op = block_on(loader.load_operation(&target_op_id))
1076 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
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);
1077 let target_view = block_on(
2667 let target_view = block_on(
1079 .op_store()
2669 .op_store()
@@ -1097,56 +2687,94 @@ impl JjBackend {
2690+ // Use jj's current marker so either CLI can continue this history.
2691+ // The older j and jj marker formats remain readable above.
1100− let marker = if redo {
2692+ let marker = format!(
1101− format!("redo: {}", cur.id().hex())
2693+ "{}: restore to operation {}",
1102− } else {
2694+ if redo { "redo" } else { "undo" },
1103− format!("undo: {}", cur.id().hex())
2695+ target_op_id.hex()
1105 let mut tx = base.start_transaction();
2697 let mut tx = base.start_transaction();
1106 tx.repo_mut().set_view(target_view.clone());
2698 tx.repo_mut().set_view(target_view.clone());
1107 let unpublished = block_on(tx.write(&marker))
2699 let unpublished = block_on(tx.write(&marker))
1108 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2700 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1109− let op_id = unpublished.operation().id().clone();
2701+ // the operation is published, then the restored focus checked out
1110− let repo = block_on(unpublished.publish())
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))
1111− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2705+ .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
1112− *self.inner.repo.lock().unwrap() = repo.clone();
2706+ let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
1113− let wc_commit = block_on(self.wc_commit(&repo))?;
1114− block_on(self.checkout(&wc_commit, op_id)).map_err(|c| (1, c.msg))?;
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();
1115− Ok(())
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 }),
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)?;
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()));
1118− /// refuse if the working directory differs from the focused commit's
2737+ /// Compare a scan with the recorded focus without publishing a snapshot.
1119− /// files (§7.7): reserved commands never snapshot, so compare the locked
2738+ /// Push only inspects: it must leave the saved working-copy state alone
1120− /// working copy's recorded tree with the focus commit's tree
2739+ /// so a later `j id` still sees every unrecorded edit.
2740+ fn inspect_working_copy(
1121− fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<(), OpenError> {
2742+ base: &Arc<ReadonlyRepo>,
1122− let dirty = (1, "working copy has changes not in @; run `j id` to record them or discard them".to_string());
2743+ refresh_clean_state: bool,
2744+ ) -> Result<(MergedTree, bool), OpenError> {
1123− let wc_commit = block_on(self.wc_commit(base))?;
2745+ let wc_tree = block_on(self.wc_commit(base))?.tree();
1124− // snapshot the working directory (without persisting anything) and
1125− // compare against the focused commit's files (§7.7)
1126 let mut ws_guard = self.inner.workspace.lock().unwrap();
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);
1127 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
2749 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
1128 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
2750 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1129− let options = SnapshotOptions {
2751+ let old_tree = locked_ws.locked_wc().old_tree().clone();
1130− base_ignores: GitIgnoreFile::empty(),
1132− start_tracking_matcher: &EverythingMatcher,
1133− force_tracking_matcher: &NothingMatcher,
1134− max_new_file_size: u64::MAX,
1136− let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
2752+ let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
1137 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2753 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1138− let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
2754+ let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
1139− // discard: finish against the current op so no state changes
2755+ let recorded = match marker_style(base.settings()) {
1140− block_on(locked_ws.finish(base.operation().id().clone()))
2756+ Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
1141− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
2757+ Err(e) => Err(e),
1146− Ok(())
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
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.
1149− pub fn cmd_ops(&self) -> Result<(), OpenError> {
2777+ pub fn cmd_ops(&self) -> Result<String, OpenError> {
1150 let base = self.head_repo()?;
2778 let base = self.head_repo()?;
1151 let loader = self.repo_loader();
2779 let loader = self.repo_loader();
1152 let head = base.operation().clone();
2780 let head = base.operation().clone();
@@ -1159,8 +2787,8 @@ impl JjBackend {
1159 let mut tag = "";
2787 let mut tag = "";
1160 match op_kind(op.metadata()) {
2788 match op_kind(op.metadata()) {
1161 OpKind::Normal => {}
2789 OpKind::Normal => {}
1162− OpKind::Undo { .. } => tag = "undo",
2790+ OpKind::Undo => tag = "undo",
1163− OpKind::Redo { .. } => tag = "redo",
2791+ OpKind::Redo => tag = "redo",
1165 if desc.is_empty() {
2793 if desc.is_empty() {
1166 desc = "(no description)".to_string();
2794 desc = "(no description)".to_string();
@@ -1183,8 +2811,7 @@ impl JjBackend {
1183 None => None,
2811 None => None,
1187− Ok(())
2814+ Ok(out)
@@ -1221,6 +2848,7 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
1222 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
2849 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1223 let backend = JjBackend {
2850 let backend = JjBackend {
2851+ replay_cache: Rc::new(ReplayCache::default()),
1224 inner: Arc::new(JjInner {
2852 inner: Arc::new(JjInner {
1225 workspace_root: cwd.clone(),
2853 workspace_root: cwd.clone(),
1226 workspace: Mutex::new(workspace),
2854 workspace: Mutex::new(workspace),
@@ -1229,23 +2857,43 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
1229 visible: Mutex::new(None),
2857 visible: Mutex::new(None),
1230 lock_guard: Mutex::new(None),
2858 lock_guard: Mutex::new(None),
1231 pending: Mutex::new(None),
2859 pending: Mutex::new(None),
2860+ folds_case: OnceLock::new(),
2861+ long_names: Mutex::new(HashMap::new()),
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();
1234 if had_git {
2867 if had_git {
1235 let import_options = backend.git_import_options()?;
2868 let import_options = backend.git_import_options()?;
1236− let mut tx = backend.current_repo().start_transaction();
1237 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
2869 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1238 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
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(
2875+ &backend.inner.workspace_name,
2878+ .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
1239 block_on(tx.repo_mut().rebase_descendants())
2879 block_on(tx.repo_mut().rebase_descendants())
1240 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
2880 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1241− let _ = block_on(backend.publish_tx(tx, "import git refs"))?;
2882+ // §7.8: the current head is git's HEAD (none on an unborn branch)
2886+ .git_head(&backend.inner.workspace_name)
1243− create_initial_wc_commit(&backend, cfg, "init")?;
2890+ create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
1247pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
2894pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
1248 let settings = user_settings_strict(cfg)?;
2895 let settings = user_settings_strict(cfg)?;
2896+ let url = &origin_url(url)?;
1249 let dir_path = PathBuf::from(dir);
2897 let dir_path = PathBuf::from(dir);
1250 if dir_path.join(".jj").is_dir() {
2898 if dir_path.join(".jj").is_dir() {
1251 return Err((2, format!("{} already contains a jj repository", dir)));
2899 return Err((2, format!("{} already contains a jj repository", dir)));
@@ -1259,82 +2907,296 @@ pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
1259 } else if dir_path.exists() {
2907 } else if dir_path.exists() {
1260 return Err((2, format!("{} already exists and is not empty", dir)));
2908 return Err((2, format!("{} already exists and is not empty", dir)));
1262− std::fs::create_dir_all(&dir_path)
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
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();
2931+ // DIR and its missing parents, DIR first ("" is the current
2932+ // directory, which exists)
2933+ let missing: Vec<&std::path::Path> = dir_path
2936+ .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
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 `..`
2944+ if e.kind() == std::io::ErrorKind::AlreadyExists
2946+ && p.is_dir() => {}
2948+ for &(p, id) in created.iter().rev() {
2949+ if !remove_created_dir(p, id, false) {
1263− .map_err(|e| (2, format!("cannot create {}: {}", dir, e)))?;
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 {
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)
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()) {
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()),
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
2992+ "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
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
3003+fn 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)
3008+ std::fs::symlink_metadata(path)
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.
3019+fn remove_created_dir(path: &std::path::Path, id: Option<(u64, u64)>, whole: bool) -> bool {
3020+ if id.is_some() && dir_identity(path, false) == id {
3022+ std::fs::remove_dir_all(path)
3024+ std::fs::remove_dir(path)
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
3034+fn 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(", ")
3039+ [] => String::new(),
3041+ "; left {}, as it is no longer the directory this clone created",
3044+ [init @ .., last] => format!(
3045+ "; left {} and {}, as they are no longer the directories this clone created",
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.
3058+fn 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)))?;
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)
3085+ settings: &UserSettings,
3087+ dir_path: &std::path::Path,
3088+) -> Result<(), OpenError> {
1264 let (workspace, repo) = block_on(Workspace::init_colocated_git(
3089 let (workspace, repo) = block_on(Workspace::init_colocated_git(
1265− &settings,
3090+ settings,
1266− &dir_path,
3091+ dir_path,
1267 gix::hash::Kind::Sha1,
3092 gix::hash::Kind::Sha1,
1269 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
3094 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1270 let backend = JjBackend {
3095 let backend = JjBackend {
3096+ replay_cache: Rc::new(ReplayCache::default()),
1271 inner: Arc::new(JjInner {
3097 inner: Arc::new(JjInner {
1272− workspace_root: dir_path.clone(),
3098+ workspace_root: dir_path.to_path_buf(),
1273 workspace: Mutex::new(workspace),
3099 workspace: Mutex::new(workspace),
1274 repo: Mutex::new(repo),
3100 repo: Mutex::new(repo),
1275 workspace_name: WorkspaceName::DEFAULT.to_owned(),
3101 workspace_name: WorkspaceName::DEFAULT.to_owned(),
1276 visible: Mutex::new(None),
3102 visible: Mutex::new(None),
1277 lock_guard: Mutex::new(None),
3103 lock_guard: Mutex::new(None),
1278 pending: Mutex::new(None),
3104 pending: Mutex::new(None),
3105+ folds_case: OnceLock::new(),
3106+ long_names: Mutex::new(HashMap::new()),
1281 let origin = RemoteName::new("origin");
3109 let origin = RemoteName::new("origin");
1282 let mut tx = backend.current_repo().start_transaction();
3110 let mut tx = backend.current_repo().start_transaction();
3111+ // a signal to stop fails each step before the working-copy commit's
3113+ unless_interrupted(
1283− jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3114+ jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
1284− .map_err(|e| (1, format!("cannot set the remote: {}", e)))?;
3115+ .map_err(|e| (1, format!("cannot set the remote: {}", e))),
1285− backend.git_fetch_refs(tx.repo_mut(), origin)?;
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(),
1286 let import_options = backend.git_import_options()?;
3122 let import_options = backend.git_import_options()?;
3123+ unless_interrupted(
1287− block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3124+ block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1288− .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
3125+ .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
3127+ unless_interrupted(
1289− block_on(tx.repo_mut().rebase_descendants())
3128+ block_on(tx.repo_mut().rebase_descendants())
1290− .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3129+ .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
1291− let _ = block_on(backend.publish_tx(tx, &format!("clone {}", url)))?;
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| {
3136+ .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3142+ // the fetch and the working-copy commit are one operation, the one undo
3143+ // never undoes (§7.7)
1292− create_initial_wc_commit(&backend, cfg, &format!("clone {}", url))?;
3144+ create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
1296−/// §7.8: after init/clone, create a working-copy commit as a child of the
3148+/// §7.8: after init/clone, create a working-copy commit as a child of
1297−/// current head (a bookmark target if there is one, otherwise the root
3149+/// `head` (the root commit when there is none), record it in `tx` with
1298−/// commit) and check it out.
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
1299fn create_initial_wc_commit(
3159fn create_initial_wc_commit(
1300 backend: &JjBackend,
3160 backend: &JjBackend,
1301 cfg: &Config,
3161 cfg: &Config,
3162+ mut tx: jj_lib::transaction::Transaction,
1302 description: &str,
3163 description: &str,
3164+ head: Option<CommitId>,
3166+ back: Option<&str>,
1303) -> Result<(), OpenError> {
3167) -> Result<(), OpenError> {
1304− let base = backend.current_repo();
3168+ let store = tx.base_repo().store().clone();
1305− let store = base.store().clone();
1306− let mut parent = store.root_commit_id().clone();
1307− 'outer: for (_name, target) in base.view().local_bookmarks() {
1308− if let Some(id) = target.as_resolved().and_then(|t| t.clone()) {
1313− if parent == *store.root_commit_id() {
3169+ let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
1314− for (_name, remote_ref) in base.view().remote_bookmarks(RemoteName::new("origin")) {
1315− if let Some(id) = remote_ref.target.as_resolved().and_then(|t| t.clone()) {
1321 // jj's own init already created a working-copy commit; if it is already
3170 // jj's own init already created a working-copy commit; if it is already
1322 // a child of the target parent, keep it and just check it out
3171 // a child of the target parent, keep it and just check it out
1323 let mut to_abandon: Option<CommitId> = None;
3172 let mut to_abandon: Option<CommitId> = None;
1324− if let Ok(existing) = block_on(backend.wc_commit(&base)) {
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()) {
1325 if existing.parent_ids().first() == Some(&parent) {
3175 if existing.parent_ids().first() == Some(&parent) {
1326− let op_id = base.operation().id().clone();
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() {
3180+ block_on(tx.write(truncate_chars(description, 200)))
3181+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
1327− block_on(backend.checkout(&existing, op_id)).map_err(|c| (1, c.msg))?;
3186+ block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
1328 return Ok(());
3187 return Ok(());
1330 if existing.id() != store.root_commit_id() {
3189 if existing.id() != store.root_commit_id() {
1331 to_abandon = Some(existing.id().clone());
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)))?
3196+ let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
1334 let user_sig = backend
3197 let user_sig = backend
1335 .user_signature(cfg)
3198 .user_signature(cfg)
1336 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3199 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1337− let mut tx = base.start_transaction();
1338 if let Some(old_wc) = &to_abandon {
3200 if let Some(old_wc) = &to_abandon {
1339 tx.repo_mut().record_abandoned_commit_with_parents(
3201 tx.repo_mut().record_abandoned_commit_with_parents(
1340 old_wc.clone(),
3202 old_wc.clone(),
@@ -1343,7 +3205,7 @@ fn create_initial_wc_commit(
1344 let wc = block_on(
3206 let wc = block_on(
1345 tx.repo_mut()
3207 tx.repo_mut()
1346− .new_commit(vec![parent], store.empty_merged_tree())
3208+ .new_commit(vec![parent], wc_tree)
1347 .set_author(user_sig.clone())
3209 .set_author(user_sig.clone())
1348 .set_committer(user_sig)
3210 .set_committer(user_sig)
1349 .write(),
3211 .write(),
@@ -1356,19 +3218,49 @@ fn create_initial_wc_commit(
1356 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3218 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
1357 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
3219 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1358 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
3220 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1359− let op_id = unpublished.operation().id().clone();
3221+ // an adopted directory is reset to the commit, not checked out over:
1360− let repo = block_on(unpublished.publish())
3222+ // checking out from the empty tree jj's init recorded would recreate
1361− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
3223+ // the files the user deleted there. Anything else is written, once the
1362− *backend.inner.repo.lock().unwrap() = repo;
3224+ // operation is published (§7.5 step 7).
3225+ let on_disk = if adopt { Some(wc.tree()) } else { None };
1363− block_on(backend.checkout(&wc, op_id)).map_err(|c| (1, c.msg))?;
3226+ block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
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
3236+async fn checkout_tree(tree: MergedTree, backend: &JjBackend) -> Result<MergedTree, OpenError> {
3237+ let folds_case = backend.folds_case();
3238+ let refused: Vec<RepoPathBuf> = tree
3240+ .map(|(path, _)| path)
3242+ path.components().any(|c| {
3243+ crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3247+ if refused.is_empty() {
3250+ let mut builder = MergedTreeBuilder::new(tree);
3251+ for path in refused {
3252+ builder.set_or_remove(path, Merge::absent());
3257+ .map_err(|e| (1, format!("cannot write a tree: {}", e)))
1367/// user settings for init/clone: a missing or malformed `user` is a
3260/// user settings for init/clone: a missing or malformed `user` is a
1368/// configuration error (exit 3) (§7.9)
3261/// configuration error (exit 3) (§7.9)
1369fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3262fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1370− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3263+ let (name, email) = validated_identity(cfg)?;
1371− .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1372 let mut config = StackedConfig::with_defaults();
3264 let mut config = StackedConfig::with_defaults();
1373 let text = format!(
3265 let text = format!(
1374 "[user]\nname = {}\nemail = {}\n",
3266 "[user]\nname = {}\nemail = {}\n",
@@ -1382,6 +3274,39 @@ fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1382 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
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.
3280+pub fn configured_identity(cfg: &Config) -> Result<String, OpenError> {
3281+ let (name, email) = validated_identity(cfg)?;
3282+ Ok(format!("{} <{}>", terminal_text(&name), terminal_text(&email)))
3285+fn terminal_text(text: &str) -> String {
3286+ text.chars().map(|c| {
3287+ if c.is_control() { c.escape_debug().to_string() } else { c.to_string() }
3291+fn 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)?;
3298+fn 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),
1385// ----------------------------------------------------------------------
3310// ----------------------------------------------------------------------
1386// reserved-command helpers
3311// reserved-command helpers
1387// ----------------------------------------------------------------------
3312// ----------------------------------------------------------------------
@@ -1445,41 +3370,72 @@ fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Resul
1445 if out.status.success() {
3370 if out.status.success() {
1447 } else {
3372 } else {
1451− "fetch failed: {}",
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)
3379+fn git_failure(out: &std::process::Output) -> String {
3380+ use std::os::unix::process::ExitStatusExt;
1452− String::from_utf8_lossy(&out.stderr).trim()
3381+ let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
1453− ),
3382+ match (said.is_empty(), out.status.signal()) {
1454− ))
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)
3392+fn 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)
3400+ .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401+ .stdin(std::process::Stdio::null())
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())
1458enum OpKind {
3414enum OpKind {
1460− /// the operation this undo discarded the view of
3416+ Undo,
1461− Undo { #[allow(dead_code)] undid: OperationId },
3417+ Redo,
1462− /// the undo this redo reversed
1463− Redo { redid: OperationId },
1466/// a marker parsed from an operation description (§7.7 kinds)
3420/// a marker parsed from an operation description (§7.7 kinds)
1467struct OpMarker {
3421struct OpMarker {
1468 kind: &'static str,
3422 kind: &'static str,
1469 id: OperationId,
3423 id: OperationId,
1470− /// whether the description is j's own exact marker (`undo: <id>` /
3424+ /// jj 0.45 names the operation whose view was restored, rather than
1471− /// `redo: <id>`); only those are reliably loadable for the walk
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.
1472 authored_by_j: bool,
3429 authored_by_j: bool,
1475−/// §7.7 kinds: `j` marks undo/redo operations as `undo: <opid>` /
3432+/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
1476−/// `redo: <opid>` in the description (0.45's Transaction cannot write
3433+/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
1477−/// metadata tags); jj's own `undo operation <id>` / `redo operation <id>`
3434+/// `redo operation <id>` descriptions remain readable too.
1478−/// markers are treated the same way.
1479fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
3435fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
1480 match op_marker(meta) {
3436 match op_marker(meta) {
1481− Some(OpMarker { kind: "undo", id, .. }) => OpKind::Undo { undid: id },
3437+ Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
1482− Some(OpMarker { kind: "redo", id, .. }) => OpKind::Redo { redid: id },
3438+ Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
1483 _ => OpKind::Normal,
3439 _ => OpKind::Normal,
@@ -1487,20 +3443,33 @@ fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
1487fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
3443fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
1488 let desc = meta.description.trim();
3444 let desc = meta.description.trim();
1489 for kind in ["undo", "redo"] {
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 });
1490 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
3451 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
1491 if let Some(id) = parse_op_id(rest.trim()) {
3452 if let Some(id) = parse_op_id(rest.trim()) {
1492− return Some(OpMarker { kind, id, authored_by_j: true });
3453+ return Some(OpMarker { kind, id, restores_view: false, authored_by_j: true });
1495 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
3456 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
1496 if let Some(id) = parse_op_id(rest.trim()) {
3457 if let Some(id) = parse_op_id(rest.trim()) {
1497− return Some(OpMarker { kind, id, authored_by_j: false });
3458+ return Some(OpMarker { kind, id, restores_view: false, authored_by_j: false });
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).
3468+fn op_created_repo(meta: &jj_lib::op_store::OperationMetadata) -> bool {
3469+ let desc = meta.description.trim();
3470+ desc == "init" || desc.starts_with("clone ")
1504/// a full-length (or prefix) hex operation id
3473/// a full-length (or prefix) hex operation id
1505fn parse_op_id(s: &str) -> Option<OperationId> {
3474fn parse_op_id(s: &str) -> Option<OperationId> {
1506 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
3475 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
@@ -1526,21 +3495,39 @@ fn op_parent_id(
1526 Ok(op.parent_ids().first().cloned())
3495 Ok(op.parent_ids().first().cloned())
1529−/// follow a chain of redos to the operation that was ultimately reapplied
3498+/// A restore operation represents an earlier view. Follow that view to a
1530−async fn chain_through_redos(
3499+/// normal operation for useful confirmation text without changing undo's
3500+/// navigation or its jj-compatible metadata.
3501+fn restored_operation_description(
1531 loader: &jj_lib::repo::RepoLoader,
3502 loader: &jj_lib::repo::RepoLoader,
1532− first: &OperationId,
3503+ op: &jj_lib::operation::Operation,
1533−) -> Result<OperationId, OpenError> {
3504+) -> String {
1534− let mut id = first.clone();
3505+ let mut represented = op.clone();
1535− loop {
3506+ let mut seen = BTreeSet::new();
1536− let op = loader
3507+ while seen.insert(represented.id().clone()) {
1537− .load_operation(&id)
3508+ let Some(marker) = op_marker(represented.metadata()) else { break };
1538− .await
3509+ let target = if marker.restores_view {
1539− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
3510+ Some(marker.id)
1540− match op_kind(op.metadata()) {
3511+ } else if marker.authored_by_j {
1541− OpKind::Redo { redid } => id = redid,
3512+ // A legacy undo names what it discarded; a legacy redo names
1542− _ => return Ok(id),
3513+ // the undo it reversed. Either restored its target's parent.
1543− }
3514+ op_parent_id(loader, &marker.id).ok().flatten()
3516+ // Unknown legacy jj history is best identified by its actual
3517+ // description instead of guessing which operation it applied.
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))
3530+ terminal_text(description)
@@ -1610,6 +3597,11 @@ fn parse_push_records(
1610 let rec = vis.commits.get(&change_id).ok_or_else(|| {
3597 let rec = vis.commits.get(&change_id).ok_or_else(|| {
1611 (1, format!("push: `@{}` is not a visible commit", change_id))
3598 (1, format!("push: `@{}` is not a visible commit", change_id))
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()));
1613 out.push((name, Some(rec.commit.id().clone())));
3605 out.push((name, Some(rec.commit.id().clone())));
1615 None => {
3607 None => {
@@ -1629,68 +3621,190 @@ fn parse_push_records(
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
1632−/// §7.6: refuse to send commits with unresolved files or empty descriptions,
3627+/// to send commits with unresolved files or empty descriptions. Returns
1633−/// and refuse to move/delete a bookmark whose current target is immutable
3628+/// whether such a delete is allowed only because a record's new target
1634−/// away from a descendant of that 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
1635fn check_push_records(
3632fn check_push_records(
1636 records: &[(String, Option<CommitId>)],
3633 records: &[(String, Option<CommitId>)],
1638 repo: &Arc<ReadonlyRepo>,
3634 repo: &Arc<ReadonlyRepo>,
3636+ immutable: &BTreeSet<String>,
1639−) -> Result<(), OpenError> {
3637+) -> Result<bool, OpenError> {
1640 let store = repo.store().clone();
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();
1641 for (name, target) in records {
3648 for (name, target) in records {
1642− let mut seeds: Vec<String> = Vec::new();
1643− if let Some(commit_id) = target {
1644− let commit = block_on(store.get_commit_async(commit_id))
1645− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1646− let change_id = commit.change_id().reverse_hex();
1647− if vis.commits.contains_key(&change_id) {
1648− seeds.push(change_id);
1651− if let Some(current) = repo
3649+ let Some(current) = repo
1653− .get_remote_bookmark(RefName::new(name).to_remote_symbol(RemoteName::new("origin")))
3651+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
1655 .as_resolved()
3653 .as_resolved()
1656 .cloned()
3654 .cloned()
1657 .unwrap_or(None)
3655 .unwrap_or(None)
1659− let commit = block_on(store.get_commit_async(¤t))
3659+ let change_id = block_on(store.get_commit_async(¤t))
1660− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
3660+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
1661− let change_id = commit.change_id().reverse_hex();
3662+ .reverse_hex();
1662− if vis.commits.contains_key(&change_id) {
3663+ // the immutable set holds visible commits: a target rewritten here
1663− seeds.push(change_id);
3664+ // since the last fetch or push is hidden, and not in it although the
1664− }
3665+ // visible rewrite carries its change id (and its label)
1666− let ancestors = ancestors_of_change_ids(vis, &seeds);
1667− for change_id in &ancestors {
1668− let rec = vis.commits.get(change_id).expect("ancestors are stored");
3666+ let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == ¤t);
1669− for (_path, value) in rec.commit.tree().entries() {
3667+ if !visible || !immutable.contains(&change_id) {
1670− let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3668+ continue;
1671− if !value.is_resolved() {
3669+ }
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();
3680+ .remote_bookmarks(origin)
3681+ .filter(|(n, _)| !named.contains(n.as_str()))
3682+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
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)))
3694+ if !reached(unnamed)? {
3695+ if !reached(&new_targets)? {
3699+ "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3704+ waiting.push(name);
3708+ let descends = block_on(repo.index().is_ancestor(¤t, new))
3709+ .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3714+ "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
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();
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
3744+ .map(|(n, _)| n.as_str())
3745+ .chain(records.iter().map(|(n, _)| n.as_str()))
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);
1672 return Err((
3752 return Err((
1674 format!(
3754 format!(
1675− "push: commit `@{}` has unresolved files",
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})`"
1681− if rec.commit.description().trim().is_empty() && rec.commit.id() != store.root_commit_id() {
3760+ }
1682− return Err((
3761+ }
1683− 1,
3762+ // the commits that would be sent: ancestors of the new targets that no
1684− format!("push: commit `@{}` has an empty description", rec.change_id),
3763+ // bookmark on the remote already reaches; a delete sends none
1685− ));
3764+ if new_targets.is_empty() {
3765+ return Ok(deletes_wait);
3767+ let on_remote: Vec<CommitId> = repo
3769+ .remote_bookmarks(origin)
3770+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
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"
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)));
1689− Ok(())
3803+ Ok(deletes_wait)
1692/// change-id closure of jj-parents from the seeds (all jj parents, not just
3806/// change-id closure of jj-parents from the seeds (all jj parents, not just
1693−/// the first), for the push content checks
3807+/// the first), for the immutable set (§7.5)
1694fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
3808fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
1695 let mut seen = BTreeSet::new();
3809 let mut seen = BTreeSet::new();
1696 let mut stack: Vec<String> = seeds.to_vec();
3810 let mut stack: Vec<String> = seeds.to_vec();
@@ -1711,23 +3825,11 @@ fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<Stri
1712/// git-check-ref-format --branch rules
3826/// git-check-ref-format --branch rules
1713fn is_valid_git_branch_name(name: &str) -> bool {
3827fn is_valid_git_branch_name(name: &str) -> bool {
1715− || name.starts_with('-')
3828+ if name.starts_with('-') {
1716− || name.starts_with('.')
1717− || name.ends_with('.')
1718− || name.ends_with('/')
1719− || name.ends_with(".lock")
1720− || name.contains("..")
1721− || name.contains("//")
1722− || name.contains("@{")
1725 return false;
3829 return false;
1727− name.chars().all(|c| {
3831+ let full_name = format!("refs/heads/{name}");
1728− !(c.is_ascii_control()
3832+ gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
1729− || matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
1733// ----------------------------------------------------------------------
3835// ----------------------------------------------------------------------
@@ -1735,6 +3837,12 @@ fn is_valid_git_branch_name(name: &str) -> bool {
1735// ----------------------------------------------------------------------
3837// ----------------------------------------------------------------------
1737impl Backend for JjBackend {
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))
1738 fn visible_ids(&self) -> Vec<String> {
3846 fn visible_ids(&self) -> Vec<String> {
1739 match self.inner.visible.lock().unwrap().as_ref() {
3847 match self.inner.visible.lock().unwrap().as_ref() {
1740 Some(v) => v.commits.keys().cloned().collect(),
3848 Some(v) => v.commits.keys().cloned().collect(),
@@ -1769,38 +3877,93 @@ impl Backend for JjBackend {
1769 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
3877 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
1772− fn is_empty(&self, id: &str) -> Option<bool> {
3880+ fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
1773 let vis = self.inner.visible.lock().unwrap().clone();
3881 let vis = self.inner.visible.lock().unwrap().clone();
1774− // O(1): empty iff the tree id equals the first parent's tree id
3882+ // O(1): empty iff the tree id equals the parent's tree id
1775 vis.and_then(|v| {
3883 vis.and_then(|v| {
1776 let rec = v.commits.get(id)?;
3884 let rec = v.commits.get(id)?;
1777− let parent = v.commits.get(&rec.first_parent)?;
3885+ let parent = v.commits.get(parent)?;
1778 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3886 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
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 {
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"))
3913+ fn name_fits(&self, name: &str) -> bool {
3914+ if name.len() <= crate::repo::NAME_MAX {
3917+ // a component, never a path: one with `/` would be created elsewhere
3918+ if name.contains(['/', '\0']) {
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) {
3929+ let fits = match tempfile::Builder::new()
3931+ .tempdir_in(self.inner.workspace_root.join(".jj"))
3934+ let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3940+ known.insert(name.to_string(), fits);
1782 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
3944 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
1783 let vis = self.inner.visible.lock().unwrap().clone();
3945 let vis = self.inner.visible.lock().unwrap().clone();
1784 let Some(vis) = vis else {
3946 let Some(vis) = vis else {
1785 return BTreeSet::new();
3947 return BTreeSet::new();
1787− let mut seen = BTreeSet::new();
3949+ // every jj parent, not just the first: rewriting a commit reached
3950+ // only through a merge's second parent rewrites the merge too
1788− let mut stack: Vec<String> = ids.iter().cloned().collect();
3952+ let seeds: Vec<String> = ids.iter().cloned().collect();
1789− while let Some(id) = stack.pop() {
3953+ ancestors_of_change_ids(&vis, &seeds)
1790− if seen.insert(id.clone()) {
1791− if let Some(rec) = vis.commits.get(&id) {
1792− if !rec.first_parent.is_empty() {
1793− stack.push(rec.first_parent.clone());
1801 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
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> {
1802 let store = self.current_repo().store().clone();
3965 let store = self.current_repo().store().clone();
1803− block_on(jj_replay(&store, onto, from, to))
3966+ block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
@@ -1808,30 +3971,197 @@ impl Backend for JjBackend {
1808// jj three-way tree merge for `replay` (§7.3)
3971// jj three-way tree merge for `replay` (§7.3)
1809// ----------------------------------------------------------------------
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.
3978+struct ReplayCache {
3979+ trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980+ empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981+ next_sweep: Cell<usize>,
3983+ entries: EntryCache,
3987+ storage: Weak<Vec<Value>>,
3989+ /// A list read from a resolved tree, already in canonical path order.
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(),
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 {
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);
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) {
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()) {
4050+ let known = self.empty_subtrees.borrow().get(&id).copied();
4051+ if let Some(found) = known {
4053+ self.empty_subtrees.borrow_mut().insert(root.clone(), true);
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);
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)));
1811async fn jj_replay(
4077async fn jj_replay(
1812 store: &Arc<Store>,
4078 store: &Arc<Store>,
4079+ cache: &ReplayCache,
1813− onto: &[Value],
4080+ onto: &Value,
1814− from: &[Value],
4081+ from: &Value,
1815− to: &[Value],
4082+ to: &Value,
1816−) -> Result<Vec<Value>, Crash> {
4083+) -> Result<Value, Crash> {
1817− let onto_tree = build_tree(store, &Value::list(onto.to_vec())).await?;
4084+ let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
1818− let from_tree = build_tree(store, &Value::list(from.to_vec())).await?;
4085+ let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
1819− let to_tree = build_tree(store, &Value::list(to.to_vec())).await?;
4086+ let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
1820− let merge = Merge::from_removes_adds(
4087+ // any of the three may itself be conflicted (a snapshot with unresolved
1821− vec![from_tree.tree_ids().as_resolved().cloned().unwrap()],
4088+ // blobs, §7.3), so merge the trees the way jj's rebase does:
1822− vec![
4089+ // MergedTree::merge flattens conflicted inputs into one merge, simplifies
1823− onto_tree.tree_ids().as_resolved().cloned().unwrap(),
1824− to_tree.tree_ids().as_resolved().cloned().unwrap(),
1827− // tree_merge::merge_trees resolves file contents line-level and keeps
4090+ // it, and resolves file contents line-level with tree_merge::merge_trees,
1828− // unresolvable paths as conflicts (tree_merge.rs:78)
4091+ // keeping unresolvable paths as conflicts. Empty labels leave the result
1829− let merged = jj_lib::tree_merge::merge_trees(store, merge)
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())],
1831− .map_err(|e| Crash::new(format!("replay: {}", e)))?;
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),
4108+ if canonical && merged.tree_ids() == tree.tree_ids() {
4109+ return Ok(files.clone());
1832 // conflicts stay as unresolved blobs (§7.3)
4112 // conflicts stay as unresolved blobs (§7.3)
1833− let merged = MergedTree::new(store.clone(), merged, jj_lib::conflict_labels::ConflictLabels::unlabeled());
4113+ cache.files_of(store, &merged).await
1834− tree_to_files(store, &merged, &BlobCache::default(), &EntryCache::default()).await
4114+}
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.
4123+async fn refuse_file_directory_clash(
4124+ merged: &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)))
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| {
4140+ .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4142+ if !has(false) || !has(true) {
4146+ value_at(onto, &path).await?,
4147+ value_at(from, &path).await?,
4148+ value_at(to, &path).await?,
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()) {
4155+ let side = |dir: &MergedTreeValueT| {
4156+ dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
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));
1837// ----------------------------------------------------------------------
4167// ----------------------------------------------------------------------
@@ -1852,6 +4182,107 @@ impl ConflictTreeValue {
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.
4191+struct 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>>,
4200+impl<'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()),
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()) {
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`.
4258+async 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 {
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() {
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) => {}
1855async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4286async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
1856 let entries = files
4287 let entries = files
1857 .as_list()
4288 .as_list()
@@ -1911,19 +4342,41 @@ async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<Merge
1911 return Err(Crash::new("persistence: malformed conflict blob"));
4342 return Err(Crash::new("persistence: malformed conflict blob"));
1913 // jj order: [add, (remove, add)*] → Merge with interleaved adds
4344 // jj order: [add, (remove, add)*] → Merge with interleaved adds
1914− // and removes
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
1915 let mut values: Vec<Option<TreeValue>> = Vec::new();
4348 let mut values: Vec<Option<TreeValue>> = Vec::new();
1916 for side in sides {
4349 for side in sides {
4350+ let Some(side) = side else {
4354+ let value = if let Some(hex) = &side.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"))?;
4363+ .write_symlink(&RepoPathBuf::root(), target)
4365+ .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366+ TreeValue::Symlink(id)
1917− let mut slice: &[u8] = side.as_ref();
4368+ let mut slice: &[u8] = side.bytes.as_ref();
1918− let id = store
4369+ let id = store
1919− .write_file(&RepoPathBuf::root(), &mut slice)
4370+ .write_file(&RepoPathBuf::root(), &mut slice)
1921− .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4372+ .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
1922− values.push(Some(TreeValue::File {
4373+ TreeValue::File {
1924− executable: b.kind == BlobKind::Executable,
4375+ executable: side.kind == BlobKind::Executable,
1925− copy_id: CopyId::placeholder(),
4376+ copy_id: CopyId::placeholder(),
4379+ values.push(Some(value));
1928 let mut adds = Vec::new();
4381 let mut adds = Vec::new();
1929 let mut removes = Vec::new();
4382 let mut removes = Vec::new();
@@ -1953,34 +4406,49 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
1953// jj trees -> values
4406// jj trees -> values
1954// ----------------------------------------------------------------------
4407// ----------------------------------------------------------------------
1956−/// blob contents keyed by file id, shared across the whole repo load: most
4409+/// Blob contents keyed by file id, shared while snapshots hold their bytes.
1957−/// files are unchanged between commits, so inflating each blob once (instead
4410+/// Weak entries let discarded replay results release their conflict sides.
1958−/// of once per commit that references it) keeps loading linear in the
1959−/// repository's unique data rather than commits × files
1960#[derive(Clone, Default)]
4411#[derive(Clone, Default)]
1961−struct BlobCache(Rc<RefCell<HashMap<FileId, Rc<Vec<u8>>>>>);
4413+ bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
4414+ next_sweep: Rc<Cell<usize>>,
1963impl BlobCache {
4417impl BlobCache {
1964 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
4418 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
1965− self.0.borrow().get(id).cloned()
4419+ self.bytes.borrow().get(id).and_then(Weak::upgrade)
1967 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
4421 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
1968− self.0.borrow_mut().insert(id, bytes);
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));
1972−/// whole file *entries* ({path, content}) keyed by (path, blob identity),
4431+/// Whole file entries keyed by (path, blob identity), shared by loaded
1973−/// shared across the whole repo load: an unchanged file is the same Value in
4432+/// snapshots and replays. Weak references let transient replay results go
1974−/// every commit, so it is built once and cloned (an Rc bump) thereafter
4433+/// away while unchanged entries still held by a snapshot are reused.
1975#[derive(Clone, Default)]
4434#[derive(Clone, Default)]
1976−struct EntryCache(Rc<RefCell<HashMap<String, Value>>>);
4435+struct EntryCache {
4436+ entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437+ next_sweep: Rc<Cell<usize>>,
1978impl EntryCache {
4440impl EntryCache {
1979 fn get(&self, key: &str) -> Option<Value> {
4441 fn get(&self, key: &str) -> Option<Value> {
1980− self.0.borrow().get(key).cloned()
4442+ self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
1982 fn insert(&self, key: String, entry: Value) {
4444 fn insert(&self, key: String, entry: Value) {
4445+ let Value::Record(record) = entry else { return };
1983− self.0.borrow_mut().insert(key, entry);
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));
@@ -2008,20 +4476,64 @@ async fn read_file_bytes(
2011−async fn side_to_bytes(
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)
4481+async fn conflict_side(
2012 store: &Arc<Store>,
4482 store: &Arc<Store>,
2013 path: &RepoPath,
4483 path: &RepoPath,
2014 v: &Option<TreeValue>,
4484 v: &Option<TreeValue>,
2015 cache: &BlobCache,
4485 cache: &BlobCache,
2016−) -> Result<Rc<Vec<u8>>, Crash> {
4486+) -> Result<Option<ConflictSide>, Crash> {
2017 match v {
4487 match v {
4489+ Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490+ kind: if *executable {
4491+ BlobKind::Executable
2018− Some(TreeValue::File { id, .. }) => read_file_bytes(store, path, id, cache).await,
4495+ bytes: read_file_bytes(store, path, id, cache).await?,
2019− Some(TreeValue::Symlink(id)) => store
4498+ Some(TreeValue::Symlink(id)) => {
2020− .read_symlink(path, id)
4500+ .read_symlink(path, id)
2022− .map(|s| Rc::new(s.into_bytes()))
2023− .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e))),
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()),
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,
2024− _ => Ok(Rc::new(Vec::new())),
4514+ bytes: Rc::new(Vec::new()),
4515+ tree: Some(id.hex()),
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.
4533+fn 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),
@@ -2031,6 +4543,9 @@ async fn merged_value_to_blob(
2031 value: &jj_lib::backend::MergedTreeValue,
4543 value: &jj_lib::backend::MergedTreeValue,
2032 cache: &BlobCache,
4544 cache: &BlobCache,
2033) -> Result<Value, Crash> {
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();
2034 if let Some(v) = value.as_resolved() {
4549 if let Some(v) = value.as_resolved() {
2035 return match v {
4550 return match v {
2036 Some(tv) => tree_value_to_blob(store, path, tv).await,
4551 Some(tv) => tree_value_to_blob(store, path, tv).await,
@@ -2041,11 +4556,11 @@ async fn merged_value_to_blob(
2043 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
4558 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
2044− let mut sides: Vec<Rc<Vec<u8>>> = Vec::new();
4559+ let mut sides: Vec<Option<ConflictSide>> = Vec::new();
2045 for (i, add) in value.adds().enumerate() {
4560 for (i, add) in value.adds().enumerate() {
2046− sides.push(side_to_bytes(store, path, add, cache).await?);
4561+ sides.push(conflict_side(store, path, add, cache).await?);
2047 if let Some(remove) = value.get_remove(i) {
4562 if let Some(remove) = value.get_remove(i) {
2048− sides.push(side_to_bytes(store, path, remove, cache).await?);
4563+ sides.push(conflict_side(store, path, remove, cache).await?);
2051 Ok(Value::Blob(Rc::new(BlobVal {
4566 Ok(Value::Blob(Rc::new(BlobVal {
@@ -2097,8 +4612,9 @@ async fn tree_value_to_blob(
2097 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
4612 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
2100− // a conflicted directory is emitted without its children by
4615+ // MergedTree::entries recurses into a resolved directory, and a
4616+ // directory side of a conflict goes through conflict_side, so this is
2101− // MergedTree::entries; §12 puts such repairs out of scope
4617+ // a submodule, which §12 puts out of scope
2102 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
4618 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
2103 kind: BlobKind::Regular,
4619 kind: BlobKind::Regular,
2104 content: BlobContent::Resolved(Rc::new(Vec::new())),
4620 content: BlobContent::Resolved(Rc::new(Vec::new())),
@@ -2112,9 +4628,27 @@ async fn tree_to_files(
2112 cache: &BlobCache,
4628 cache: &BlobCache,
2113 entries: &EntryCache,
4629 entries: &EntryCache,
2114) -> Result<Vec<Value>, Crash> {
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.
4637+async fn tree_to_files_inner(
4638+ store: &Arc<Store>,
4641+ entries: &EntryCache,
4642+) -> Result<(Vec<Value>, bool), Crash> {
2115 let mut out = Vec::new();
4643 let mut out = Vec::new();
4644+ let mut reusable = tree.tree_ids().is_resolved();
2116 for (path, value) in tree.entries() {
4645 for (path, value) in tree.entries() {
2117 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
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,
2118 // cache key: the entry is fully determined by its path and the
4652 // cache key: the entry is fully determined by its path and the
2119 // identity of its (resolved) content; conflicts are not cached
4653 // identity of its (resolved) content; conflicts are not cached
2120 let key = match value.as_resolved() {
4654 let key = match value.as_resolved() {
@@ -2145,13 +4679,22 @@ async fn tree_to_files(
2146 out.push(entry);
4680 out.push(entry);
2148− Ok(out)
4682+ Ok((out, reusable))
2151// ----------------------------------------------------------------------
4685// ----------------------------------------------------------------------
2152// value helpers
4686// value helpers
2153// ----------------------------------------------------------------------
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.
4693+fn tree_name(tree: &MergedTree) -> String {
4694+ let ids: Vec<String> = tree.tree_ids().iter().map(|id| id.hex()).collect();
2155fn commit_id_of(commit: &Value) -> Result<String, Crash> {
4698fn commit_id_of(commit: &Value) -> Result<String, Crash> {
2156 match commit.field("id")? {
4699 match commit.field("id")? {
2157 Value::Id(i) => Ok(i.to_string()),
4700 Value::Id(i) => Ok(i.to_string()),
@@ -2179,15 +4722,13 @@ fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
2179// building the Repo value from the visible map (§7.2)
4722// building the Repo value from the visible map (§7.2)
2180// ----------------------------------------------------------------------
4723// ----------------------------------------------------------------------
2182−fn build_repo_value(vis: &VisibleRepo, focus_change: &str) -> Result<Value, OpenError> {
4725+fn build_repo_value(vis: &VisibleRepo, focus_change: &str, replay_cache: &Rc<ReplayCache>) -> Result<Value, OpenError> {
2183 let root_rec = vis
4726 let root_rec = vis
2184 .commits
4727 .commits
2185 .get(ROOT_ID)
4728 .get(ROOT_ID)
2186 .ok_or_else(|| (2, "no root commit".to_string()))?;
4729 .ok_or_else(|| (2, "no root commit".to_string()))?;
2187 let store = root_rec.commit.store().clone();
4730 let store = root_rec.commit.store().clone();
2188− let cache = BlobCache::default();
2189− let entries = EntryCache::default();
2190− let mut subtree = build_subtree(vis, root_rec, &store, &cache, &entries)?;
4731+ let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
2191 sort_subtree(&mut subtree, vis);
4732 sort_subtree(&mut subtree, vis);
2192 let top = Value::record(&[
4733 let top = Value::record(&[
2193 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
4734 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
@@ -2204,19 +4745,20 @@ fn build_subtree(
2204 vis: &VisibleRepo,
4745 vis: &VisibleRepo,
2205 rec: &StoredCommit,
4746 rec: &StoredCommit,
2206 store: &Arc<Store>,
4747 store: &Arc<Store>,
2207− cache: &BlobCache,
4748+ replay_cache: &Rc<ReplayCache>,
2208− entries: &EntryCache,
2209) -> Result<Value, OpenError> {
4749) -> Result<Value, OpenError> {
2210 // the files list is lazy (§7.2): most commits are never inspected, so
4750 // the files list is lazy (§7.2): most commits are never inspected, so
2211− // their file entries are materialized only on first `field("files")`
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)
2212 let files_thunk = {
4755 let files_thunk = {
2213 let store = store.clone();
4756 let store = store.clone();
2214 let tree = rec.commit.tree();
4757 let tree = rec.commit.tree();
2215− let cache = cache.clone();
4758+ let name = Some(tree_name(&tree));
2216− let entries = entries.clone();
4759+ let replay_cache = replay_cache.clone();
2217− Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
4760+ Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
2218− let files = block_on(tree_to_files(&store, &tree, &cache, &entries))?;
4761+ block_on(replay_cache.files_of(&store, &tree))
2219− Ok(Value::list(files))
2222 let labels: Vec<Value> = vis
4764 let labels: Vec<Value> = vis
@@ -2237,7 +4779,7 @@ fn build_subtree(
2237 if let Some(kids) = vis.children.get(&rec.change_id) {
4779 if let Some(kids) = vis.children.get(&rec.change_id) {
2238 for kid in kids {
4780 for kid in kids {
2239 if let Some(krec) = vis.commits.get(kid) {
4781 if let Some(krec) = vis.commits.get(kid) {
2240− children.push(build_subtree(vis, krec, store, cache, entries)?);
4782+ children.push(build_subtree(vis, krec, store, replay_cache)?);
@@ -2272,3 +4814,165 @@ fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
2272 let root = subtree.field("root").expect("subtree has a root");
4814 let root = subtree.field("root").expect("subtree has a root");
2273 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
4815 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
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,
4829+ (dir, repo.store().clone())
4832+ fn file(path: &str, content: &str) -> Value {
4834+ ("path", Value::list(vec![Value::text(path)])),
4835+ ("content", BlobVal::text_blob(content)),
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()
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]),
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");
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);
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)),
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());
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);
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));
4926+ assert!(weak_bytes.upgrade().is_none());
4927+ assert!(cache.blobs.get(&id).is_none());
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)
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);
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() })
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);