@@ -6,33 +6,38 @@
66use crate::config::Config;
77use crate::domain::{Backend, MetaInfo, ROOT_ID};
88use 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};
1011use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
1112use jj_lib::commit::Commit;
1213use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14+use jj_lib::conflicts::ConflictMarkerStyle;
1315use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
1416use jj_lib::gitignore::GitIgnoreFile;
15−use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
17+use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
18+use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
1619use jj_lib::merge::{Diff, Merge};
1720use jj_lib::merged_tree::MergedTree;
1821use jj_lib::merged_tree_builder::MergedTreeBuilder;
19−use jj_lib::backend::{CommitId, FileId};
22+use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
2023use jj_lib::object_id::ObjectId;
2124use jj_lib::op_store::OperationId;
2225use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
2326use 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};
2529use jj_lib::settings::UserSettings;
2630use jj_lib::store::Store;
27−use jj_lib::working_copy::SnapshotOptions;
31+use jj_lib::transaction::UnpublishedOperation;
32+use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
2833use jj_lib::workspace::Workspace;
29−use std::cell::RefCell;
34+use std::cell::{Cell, RefCell};
3035use std::collections::{BTreeMap, BTreeSet, HashMap};
3136use std::future::Future;
3237use std::os::unix::io::AsRawFd;
3338use std::path::PathBuf;
34−use std::rc::Rc;
35−use std::sync::{Arc, Mutex};
39+use std::rc::{Rc, Weak};
40+use std::sync::{Arc, Mutex, OnceLock};
3641
3742fn block_on<F: Future>(f: F) -> F::Output {
3843 pollster::block_on(f)
@@ -40,6 +45,15 @@ fn block_on<F: Future>(f: F) -> F::Output {
4045
4146type OpenError = (u8, String);
4247
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,
53+ pub deleted: usize,
54+ pub unchanged: usize,
55+}
56+
4357// ----------------------------------------------------------------------
4458// locating and opening the repository (§7.1)
4559// ----------------------------------------------------------------------
@@ -64,6 +78,7 @@ fn toml_string(s: &str) -> String {
6478 '\n' => out.push_str("\\n"),
6579 '\r' => out.push_str("\\r"),
6680 '\t' => out.push_str("\\t"),
81+ c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
6782 c => out.push(c),
6883 }
6984 }
@@ -252,31 +267,1166 @@ pub struct JjInner {
252267 repo: Mutex<Arc<ReadonlyRepo>>,
253268 workspace_name: WorkspaceNameBuf,
254269 /// 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)
256272 visible: Mutex<Option<Arc<VisibleRepo>>>,
257273 lock_guard: Mutex<Option<FileLock>>,
258− /// the snapshot transaction and working-copy lock, held between
259− /// build_interp and persist so the snapshot and the expression's edits
260− /// form one jj operation (§1.2); dropped uncommitted by printing runs
274+ /// the snapshot's unpublished rewrite, kept between build_interp and
275+ /// persist so the snapshot and the expression's edits form one jj
276+ /// operation (§1.2); dropped uncommitted by printing runs. The
277+ /// working-copy lock is not held: its saved state is still the tree
278+ /// recorded before the snapshot until `persist`'s checkout scans the
279+ /// directory again
261280 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>>,
262287}
263288
264289/// holding the file keeps the flock
265290pub struct FileLock(#[allow(dead_code)] std::fs::File);
266291
267−#[allow(dead_code)]
268292struct PendingSnapshot {
269293 /// repo as loaded at head, before the snapshot
270294 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
272296 /// persisting operation (§1.2 step 8, §7.7)
273− new_wc_id: CommitId,
274297 tree: MergedTree,
298+ /// false when the wc commit is immutable: the snapshot then belongs to
299+ /// the focus's new child (§7.2), and the wc commit is kept as stored
300+ fold_into_wc: bool,
301+ /// the tree the scan found in the working directory, which `persist`'s
302+ /// checkout starts from: `tree` itself, unless the working copy was
303+ /// stale and `tree` merges the directory's changes onto the commit
304+ /// (`snapshot_tree`)
305+ scanned: MergedTree,
306+}
307+
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> {
313+ SnapshotOptions {
314+ base_ignores: GitIgnoreFile::empty(),
315+ progress: None,
316+ start_tracking_matcher: &EverythingMatcher,
317+ force_tracking_matcher: &NothingMatcher,
318+ max_new_file_size: u64::MAX,
319+ }
320+}
321+
322+/// Scan the working directory with `options`, as `wc.snapshot` does, where
323+/// `base` is the tree the working copy's state records, as `saved` holds
324+/// it; return the tree scanned. jj's scan loses a regular file found where
325+/// `base` has a conflict it cannot write a file into, one with a side that
326+/// is no file, and keeps the conflict, so the file reaches no tree (§7.4):
327+///
328+/// - at a conflicted path, where a checkout wrote jj's description of the
329+/// sides and a file written since holds something else
330+/// (`FilesOnConflicts::at`). The state is first reset to a resolved file
331+/// there, so that the scan reads the file whole, as it reads any tracked
332+/// file; the path stays tracked where a `.gitignore` has come to match it.
333+/// - above one, where `base` has a directory holding a conflict, whose
334+/// differing sides the scan keeps at the path while it removes the
335+/// entries below (and a debug build fails an assertion). Such a file is
336+/// first left untracked, which scans it as if it had been moved away and
337+/// records the directory's removal; a second scan then records the file
338+/// where nothing is, as a later run would once it was put back.
339+async fn scan(
340+ wc: &mut dyn LockedWorkingCopy,
341+ root: &std::path::Path,
342+ base: &MergedTree,
343+ saved: &SavedState,
344+ options: &SnapshotOptions<'_>,
345+) -> Result<MergedTree, String> {
346+ let files = files_on_conflicts(root, base, Some(saved))?;
347+ let mut state = base.clone();
348+ if !files.at.is_empty() {
349+ // the empty file: a reset records the file at each path it changes
350+ // as an empty one, not executable, modified at the epoch, so the
351+ // scan reads any other file there, and keeps this one for such a file
352+ let store = base.store();
353+ let id = store.write_file(RepoPath::root(), &mut &[][..]).await.map_err(|e| e.to_string())?;
354+ let copy_id = CopyId::placeholder();
355+ let empty = Merge::normal(TreeValue::File { id, executable: false, copy_id });
356+ state = reset_paths(wc, &state, files.at.into_iter().map(|path| (path, empty.clone()))).await?;
357+ }
358+ if !files.over.is_empty() {
359+ let untracked = FilesMatcher::new(&files.over);
360+ let start = DifferenceMatcher::new(options.start_tracking_matcher, &untracked);
361+ let first = SnapshotOptions { start_tracking_matcher: &start, ..options.clone() };
362+ state = scan_pass(wc, root, &state, &first).await?;
363+ // a state that agrees with its tree tracks every entry below the
364+ // directory, so none is left; were a conflict left below, the
365+ // second scan would lose the file after all
366+ if let Some(path) = files_on_conflicts(root, &state, None)?.over.first() {
367+ return Err(format!(
368+ "cannot record the file `{}` in place of a directory holding a conflict",
369+ path.as_internal_file_string()
370+ ));
371+ }
372+ }
373+ scan_pass(wc, root, &state, options).await
374+}
375+
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.
385+async fn scan_pass(
386+ wc: &mut dyn LockedWorkingCopy,
387+ root: &std::path::Path,
388+ state: &MergedTree,
389+ options: &SnapshotOptions<'_>,
390+) -> Result<MergedTree, String> {
391+ let err = match wc.snapshot(options).await {
392+ Ok((scanned, _stats)) => return Ok(scanned),
393+ Err(err) => err,
394+ };
395+ let not_a_dir = |e: &std::io::Error| e.kind() == std::io::ErrorKind::NotADirectory;
396+ let SnapshotError::Other { err: source, .. } = &err else {
397+ return Err(err.to_string());
398+ };
399+ if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400+ return Err(err.to_string());
401+ }
402+ // one `lstat` per path the state tracks, only once a scan has failed
403+ let mut gone = Vec::new();
404+ for (path, value) in state.entries() {
405+ value.map_err(|e| e.to_string())?;
406+ let Ok(disk) = path.to_fs_path(root) else {
407+ continue;
408+ };
409+ if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410+ gone.push((path, Merge::absent()));
411+ }
412+ }
413+ if gone.is_empty() {
414+ return Err(err.to_string());
415+ }
416+ reset_paths(wc, state, gone).await?;
417+ let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
418+ Ok(scanned)
419+}
420+
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,
427+ state: &MergedTree,
428+ values: impl IntoIterator<Item = (RepoPathBuf, jj_lib::backend::MergedTreeValue)>,
429+) -> Result<MergedTree, String> {
430+ let mut builder = MergedTreeBuilder::new(state.clone());
431+ for (path, value) in values {
432+ builder.set_or_remove(path, value);
433+ }
434+ let tree = builder.write_tree().await.map_err(|e| e.to_string())?;
435+ // a reset reads only the commit's tree
436+ let commit = with_tree(&state.store().root_commit(), tree.clone());
437+ wc.reset(&commit).await.map_err(|e| e.to_string())?;
438+ Ok(tree)
439+}
440+
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
447+ /// conflict
448+ at: Vec<RepoPathBuf>,
449+ /// the directories holding a conflict, that is those above a conflicted
450+ /// path: a directory's sides differ exactly when a conflict is below
451+ /// it, as jj writes no tree whose sides differ where every path resolves
452+ over: Vec<RepoPathBuf>,
453+}
454+
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.
458+struct SavedState {
459+ store: Arc<Store>,
460+ root: PathBuf,
461+ path: PathBuf,
462+ settings: UserSettings,
463+}
464+
465+impl SavedState {
466+ /// Where `ws` saves its working copy's state; taken before the lock,
467+ /// which borrows `ws`
468+ fn of(ws: &Workspace) -> SavedState {
469+ let root = ws.workspace_root().to_owned();
470+ let path = match ws.working_copy().downcast_ref::<LocalWorkingCopy>() {
471+ Some(wc) => wc.state_path().to_owned(),
472+ None => root.join(".jj").join("working_copy"),
473+ };
474+ SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
475+ }
476+
477+ /// The state as saved, where it records `tree`, with the time it was
478+ /// saved; None where it cannot be read or records another tree. A state
479+ /// file that is not there is not read, as jj would write one to read.
480+ fn load(&self, tree: &MergedTree) -> Option<(TreeState, i64)> {
481+ let meta = self.path.join("tree_state").symlink_metadata().ok()?;
482+ let saved_at = millis(meta.modified().ok()?)?;
483+ let settings = TreeStateSettings::try_from_user_settings(&self.settings).ok()?;
484+ let state = TreeState::load(self.store.clone(), self.root.clone(), self.path.clone(), &settings).ok()?;
485+ (state.current_tree().tree_ids() == tree.tree_ids()).then_some((state, saved_at))
486+ }
487+}
488+
489+/// A modification time in milliseconds since the epoch, as jj's working
490+/// copy records one
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),
495+ }
496+}
497+
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
508+}
509+
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') {
519+ return None;
520+ }
521+ let (mut removes, mut adds) = (Vec::new(), Vec::new());
522+ for line in lines.split_terminator('\n') {
523+ let (terms, term) = match (line.strip_prefix(" Removing "), line.strip_prefix(" Adding ")) {
524+ (Some(term), _) => (&mut removes, term),
525+ (_, Some(term)) => (&mut adds, term),
526+ _ => return None,
527+ };
528+ // a term ends with its object's id, which holds no space
529+ let id = term.find(" with id ")? + " with id ".len();
530+ let end = term[id..].find(' ').map_or(term.len(), |n| id + n);
531+ let (term, label) = term.split_at(end);
532+ if !label.is_empty() && !(label.starts_with(" (") && label.ends_with(')')) {
533+ return None;
534+ }
535+ terms.push(term);
536+ }
537+ removes.retain(|term| match adds.iter().position(|t| t == term) {
538+ Some(n) => {
539+ adds.swap_remove(n);
540+ false
541+ }
542+ None => true,
543+ });
544+ removes.sort_unstable();
545+ adds.sort_unstable();
546+ Some((removes, adds))
547+}
548+
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,
558+ tree: &MergedTree,
559+ saved: Option<&SavedState>,
560+) -> Result<FilesOnConflicts, String> {
561+ let mut files = FilesOnConflicts { at: Vec::new(), over: Vec::new() };
562+ // whether each directory looked at so far is one on disk
563+ let mut dirs: HashMap<RepoPathBuf, bool> = HashMap::new();
564+ // the state as saved, read once a file differs from its description,
565+ // and what it records of the files, taken once
566+ let loaded: std::cell::OnceCell<Option<(TreeState, i64)>> = std::cell::OnceCell::new();
567+ let recorded: std::cell::OnceCell<Option<(FileStates<'_>, i64)>> = std::cell::OnceCell::new();
568+ for (path, value) in tree.conflicts() {
569+ let value = value.map_err(|e| e.to_string())?;
570+ let parent = path.parent().expect("a conflicted path has a parent");
571+ let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
572+ // from the top down, stopping at the first that is not a directory
573+ let below_dirs = above.into_iter().rev().all(|dir| match dirs.get(dir) {
574+ Some(&is_dir) => is_dir,
575+ None => {
576+ let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
577+ if meta.as_ref().is_some_and(|m| m.is_file()) {
578+ files.over.push(dir.to_owned());
579+ }
580+ let is_dir = meta.is_some_and(|m| m.is_dir());
581+ dirs.insert(dir.to_owned(), is_dir);
582+ is_dir
583+ }
584+ });
585+ let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
586+ continue;
587+ };
588+ let Some((disk, meta)) = path.to_fs_path(root).ok().and_then(|disk| {
589+ let meta = disk.symlink_metadata().ok()?;
590+ meta.is_file().then_some((disk, meta))
591+ }) else {
592+ continue;
593+ };
594+ // what jj's checkout writes for it (§7.4)
595+ let written = value.describe(tree.labels());
596+ let mut bytes = None;
597+ if meta.len() == written.len() as u64 {
598+ match std::fs::read(&disk) {
599+ Ok(read) if read == written.as_bytes() => continue,
600+ Ok(read) => bytes = Some(read),
601+ Err(_) => continue,
602+ }
603+ }
604+ // the file a checkout wrote, left as it was: it describes the
605+ // conflict as the checkout had it, which the state may since
606+ // record otherwise with no file written, as a scan that drops a
607+ // pair of sides that cancel, or a reset that only relabels, does
608+ let states = recorded.get_or_init(|| {
609+ let (state, saved_at) = loaded.get_or_init(|| saved.load(tree)).as_ref()?;
610+ Some((state.file_states(), *saved_at))
611+ });
612+ if states.as_ref().is_some_and(|(file_states, saved_at)| {
613+ file_states.get(&path).is_some_and(|state| left_as_recorded(&state, &meta, *saved_at))
614+ }) {
615+ continue;
616+ }
617+ // written since, or not known to be left: the conflict is kept
618+ // while the file describes it, as when an editor saves it unchanged
619+ let Some(bytes) = bytes.or_else(|| std::fs::read(&disk).ok()) else {
620+ continue;
621+ };
622+ let unlabelled = value.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
623+ let terms = described_terms(unlabelled.as_bytes());
624+ if terms.is_none() || described_terms(&bytes) != terms {
625+ files.at.push(path);
626+ }
627+ }
628+ Ok(files)
629+}
630+
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,
657+ wc: &MergedTree,
658+ state: &MergedTree,
659+ current: bool,
660+ scanned: &MergedTree,
661+) -> Result<MergedTree, String> {
662+ if state.tree_ids() == wc.tree_ids() {
663+ return Ok(scanned.clone());
664+ }
665+ if current {
666+ return conflicts_written(root, markers, wc, state, scanned).await;
667+ }
668+ if scanned.tree_ids() == state.tree_ids() {
669+ return Ok(wc.clone());
670+ }
671+ let scanned = &conflicts_written(root, markers, wc, state, scanned).await?;
672+ let err = |e: jj_lib::backend::BackendError| e.to_string();
673+ // the paths where the directory holds something other than the commit
674+ let mut apart = std::collections::HashSet::new();
675+ let mut diff = wc.diff_stream(scanned, &EverythingMatcher);
676+ while let Some(entry) = diff.next().await {
677+ entry.values.map_err(err)?;
678+ apart.insert(entry.path);
679+ }
680+ // the base: the state, but for what the directory changed as the
681+ // commit did
682+ let mut base = MergedTreeBuilder::new(state.clone());
683+ let mut same = false;
684+ let mut diff = state.diff_stream(scanned, &EverythingMatcher);
685+ while let Some(entry) = diff.next().await {
686+ let Diff { after, .. } = entry.values.map_err(err)?;
687+ if !apart.contains(&entry.path) {
688+ base.set_or_remove(entry.path, after);
689+ same = true;
690+ }
691+ }
692+ let base = if same { base.write_tree().await.map_err(err)? } else { state.clone() };
693+ let merged = MergedTree::merge(Merge::from_removes_adds(
694+ vec![(base.clone(), "last checkout".to_string())],
695+ vec![
696+ (wc.clone(), "working-copy commit".to_string()),
697+ (scanned.clone(), "working directory".to_string()),
698+ ],
699+ ))
700+ .await
701+ .map_err(err)?;
702+ // A conflict setting a directory against a file, a symlink or nothing,
703+ // which jj keeps at the path itself, cannot be a blob that lists the
704+ // directory's entries (§7.3), and a checkout would write jj's
705+ // description of it in place of what the directory holds there,
706+ // removing a directory's files: where the directory changed the path,
707+ // take what it holds, as a snapshot over a current state does. The
708+ // commit's change there is left in the operation before. Where the
709+ // directory holds what the base does, the merge took the commit's value
710+ // as it is, so such a conflict is one the commit already has, and it
711+ // stays. A tree with no conflict is not walked.
712+ let mut kept = MergedTreeBuilder::new(merged.clone());
713+ let mut mixed = false;
714+ for (path, value) in merged.conflicts() {
715+ if value.map_err(err)?.iter().any(|term| matches!(term, Some(TreeValue::Tree(_)))) {
716+ let held = scanned.path_value(&path).await.map_err(err)?;
717+ if held == base.path_value(&path).await.map_err(err)? {
718+ continue;
719+ }
720+ kept.set_or_remove(path, held);
721+ mixed = true;
722+ }
723+ }
724+ if !mixed {
725+ return Ok(merged);
726+ }
727+ kept.write_tree().await.map_err(err)
728+}
729+
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,
743+ wc: &MergedTree,
744+ state: &MergedTree,
745+ scanned: &MergedTree,
746+) -> Result<MergedTree, String> {
747+ let err = |e: jj_lib::backend::BackendError| e.to_string();
748+ let mut tree = MergedTreeBuilder::new(scanned.clone());
749+ let mut found = false;
750+ for (path, value) in wc.conflicts() {
751+ let value = value.map_err(err)?;
752+ if state.path_value(&path).await.map_err(err)? == value
753+ || scanned.path_value(&path).await.map_err(err)? == value
754+ {
755+ continue;
756+ }
757+ let Ok(disk) = path.to_fs_path(root) else {
758+ continue;
759+ };
760+ if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761+ tree.set_or_remove(path, value);
762+ found = true;
763+ }
764+ }
765+ if !found {
766+ return Ok(scanned.clone());
767+ }
768+ tree.write_tree().await.map_err(err)
769+}
770+
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(
779+ store: &Arc<Store>,
780+ path: &RepoPath,
781+ value: jj_lib::backend::MergedTreeValue,
782+ labels: &jj_lib::conflict_labels::ConflictLabels,
783+ markers: ConflictMarkerStyle,
784+ disk: &std::path::Path,
785+) -> Result<bool, String> {
786+ use jj_lib::conflicts::{self, MaterializedTreeValue};
787+ let symlink = matches!(value.as_normal(), Some(TreeValue::Symlink(_)));
788+ let Some(meta) = disk.symlink_metadata().ok().filter(|m| if symlink { m.is_symlink() } else { m.is_file() })
789+ else {
790+ return Ok(false);
791+ };
792+ let written = conflicts::materialize_tree_value(store, path, value, labels)
793+ .await
794+ .map_err(|e| e.to_string())?;
795+ match written {
796+ MaterializedTreeValue::Symlink { target, .. } => {
797+ Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
798+ }
799+ MaterializedTreeValue::File(mut file) => {
800+ let bytes = file.read_all(path).await.map_err(|e| e.to_string())?;
801+ Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
802+ }
803+ MaterializedTreeValue::FileConflict(file) => {
804+ let options = conflicts::ConflictMaterializeOptions {
805+ marker_style: markers,
806+ marker_len: Some(conflicts::choose_materialized_conflict_marker_len(&file.contents)),
807+ merge: store.merge_options().clone(),
808+ };
809+ let bytes: Vec<u8> =
810+ conflicts::materialize_merge_result_to_bytes(&file.contents, &file.labels, &options).into();
811+ Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
812+ }
813+ MaterializedTreeValue::OtherConflict { id, .. } => {
814+ let unlabelled = id.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
815+ let terms = described_terms(unlabelled.as_bytes());
816+ Ok(terms.is_some() && std::fs::read(disk).is_ok_and(|b| described_terms(&b) == terms))
817+ }
818+ _ => Ok(false),
819+ }
820+}
821+
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)
825+}
826+
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`.
833+fn first_not_a_dir(
834+ root: &std::path::Path,
835+ path: &RepoPath,
836+ dirs: &mut HashMap<RepoPathBuf, Option<std::fs::FileType>>,
837+) -> Option<(RepoPathBuf, Option<std::fs::FileType>)> {
838+ let parent = path.parent()?;
839+ let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
840+ for dir in above.into_iter().rev() {
841+ let stands = match dirs.get(dir) {
842+ Some(&stands) => stands,
843+ None => {
844+ let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
845+ let stands = meta.map(|m| m.file_type());
846+ dirs.insert(dir.to_owned(), stands);
847+ stands
848+ }
849+ };
850+ if !stands.is_some_and(|t| t.is_dir()) {
851+ return Some((dir.to_owned(), stands));
852+ }
853+ }
854+ None
855+}
856+
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,
867+ to: &MergedTree,
868+) -> Result<Option<RepoPathBuf>, String> {
869+ let err = |e: jj_lib::backend::BackendError| e.to_string();
870+ let mut diff = on_disk.diff_stream(rescanned, &EverythingMatcher);
871+ while let Some(entry) = diff.next().await {
872+ let Diff { before, .. } = entry.values.map_err(err)?;
873+ // a diff has no tree on either side
874+ let focus = to.path_value(&entry.path).await.map_err(err)?;
875+ let focus = if focus.is_tree() { Merge::absent() } else { focus };
876+ if focus != before {
877+ return Ok(Some(entry.path));
878+ }
879+ }
880+ Ok(None)
881+}
882+
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
886+struct InTheWay {
887+ path: RepoPathBuf,
888+ holder: RepoPathBuf,
889+}
890+
891+impl InTheWay {
892+ /// Whether it is a directory at `path`, holding `holder`
893+ fn is_dir(&self) -> bool {
894+ self.holder != self.path && self.holder.starts_with(&self.path)
895+ }
896+
897+ /// What a refused checkout says of it among others: a directory by its
898+ /// own path, with what it holds, anything else by its path
899+ fn named(&self) -> String {
900+ if self.is_dir() {
901+ format!(
902+ "`{}` (a directory holding untracked files, such as `{}`)",
903+ self.path.as_internal_file_string(),
904+ self.holder.as_internal_file_string()
905+ )
906+ } else {
907+ format!("`{}`", self.holder.as_internal_file_string())
908+ }
909+ }
910+}
911+
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;
917+ match in_the_way {
918+ [one] if one.is_dir() => format!(
919+ "it has `{0}`, and `{0}` in the working directory is a directory holding untracked files, such as `{1}`; move it aside",
920+ one.path.as_internal_file_string(),
921+ one.holder.as_internal_file_string()
922+ ),
923+ [one] => format!(
924+ "it has `{}`, and an untracked `{}` in the working directory is in its way; move it aside",
925+ one.path.as_internal_file_string(),
926+ one.holder.as_internal_file_string()
927+ ),
928+ _ => {
929+ let mut named: Vec<String> = in_the_way.iter().take(LISTED).map(InTheWay::named).collect();
930+ let last = match in_the_way.len() - named.len() {
931+ 0 => named.pop().unwrap_or_default(),
932+ more => format!("{} more", more),
933+ };
934+ format!(
935+ "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
936+ named.join(", "),
937+ last
938+ )
939+ }
940+ }
941+}
942+
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,
964+ from: &MergedTree,
965+ to: &MergedTree,
966+) -> Result<Result<Vec<RepoPathBuf>, Vec<InTheWay>>, String> {
967+ let err = |e: jj_lib::backend::BackendError| e.to_string();
968+ let mut dirs = HashMap::new();
969+ // each thing that is no directory looked up in `from`, above a path
970+ let mut looked_up = std::collections::HashSet::new();
971+ let mut clear = Vec::new();
972+ let mut in_the_way = Vec::new();
973+ let mut diff = from.diff_stream(to, &EverythingMatcher);
974+ while let Some(entry) = diff.next().await {
975+ let Diff { before, after } = entry.values.map_err(err)?;
976+ // a submodule's directory is the submodule's own, which jj keeps
977+ if before.is_present() || after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
978+ continue;
979+ }
980+ let path = entry.path;
981+ if let Some((dir, stands)) = first_not_a_dir(root, &path, &mut dirs) {
982+ // where nothing stands above the path, or what `from` tracks
983+ // and the checkout removes, nothing stands at it
984+ if stands.is_some() && looked_up.insert(dir.clone()) {
985+ let is_tracked = from.path_value(&dir).await.map_err(err)?.is_present();
986+ if !is_tracked {
987+ in_the_way.push(InTheWay { path, holder: dir });
988+ }
989+ }
990+ continue;
991+ }
992+ let Ok(disk) = path.to_fs_path(root) else {
993+ continue;
994+ };
995+ let Ok(meta) = disk.symlink_metadata() else {
996+ continue;
997+ };
998+ let holder = if meta.is_dir() {
999+ untracked_below(&disk, &path, from)
1000+ } else if holds_written(to.store(), &path, after, to.labels(), markers, &disk).await? {
1001+ None
1002+ } else {
1003+ Some(path.clone())
1004+ };
1005+ match holder {
1006+ Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007+ None => clear.push(path),
1008+ }
1009+ }
1010+ if in_the_way.is_empty() {
1011+ Ok(Ok(clear))
1012+ } else {
1013+ Ok(Err(in_the_way))
1014+ }
1015+}
1016+
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 {
1027+ return Some(at);
1028+ };
1029+ for entry in entries {
1030+ let Ok(entry) = entry else {
1031+ return Some(at);
1032+ };
1033+ let name = entry.file_name();
1034+ // a name no path of a tree holds
1035+ let Some(below) = name.to_str().and_then(|n| RepoPathComponent::new(n).ok()).map(|c| at.join(c)) else {
1036+ return Some(at);
1037+ };
1038+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039+ dirs.push((entry.path(), below));
1040+ continue;
1041+ }
1042+ let tracked = tracked.get_or_insert_with(|| {
1043+ let under = jj_lib::matchers::PrefixMatcher::new([path]);
1044+ from.entries_matching(&under).filter(|(_, value)| value.is_ok()).map(|(p, _)| p).collect()
1045+ });
1046+ if !tracked.contains(&below) {
1047+ return Some(below);
1048+ }
1049+ }
1050+ }
1051+ None
1052+}
1053+
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,
1067+ to: &MergedTree,
1068+ paths: &[RepoPathBuf],
1069+) -> Aside {
1070+ fn clear(dir: &std::path::Path) {
1071+ if let Ok(entries) = std::fs::read_dir(dir) {
1072+ for entry in entries.flatten() {
1073+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1074+ clear(&entry.path());
1075+ }
1076+ }
1077+ }
1078+ let _ = std::fs::remove_dir(dir);
1079+ }
1080+ let mut aside = Aside { root: root.to_owned(), dir: None, stuck: None, returned: false };
1081+ let mut dirs = HashMap::new();
1082+ for path in paths {
1083+ if first_not_a_dir(root, path, &mut dirs).is_some() {
1084+ continue;
1085+ }
1086+ let Ok(disk) = path.to_fs_path(root) else {
1087+ continue;
1088+ };
1089+ let Ok(meta) = disk.symlink_metadata() else {
1090+ continue;
1091+ };
1092+ if meta.is_dir() {
1093+ clear(&disk);
1094+ continue;
1095+ }
1096+ let Ok(value) = to.path_value(path).await else {
1097+ continue;
1098+ };
1099+ if holds_written(to.store(), path, value, to.labels(), markers, &disk).await.unwrap_or(false) {
1100+ if let Err(e) = aside.take(path, &disk) {
1101+ aside.stuck.get_or_insert((path.clone(), e));
1102+ }
1103+ }
1104+ }
1105+ aside
1106+}
1107+
1108+/// The start of the name of each directory in `.jj` that `Aside` moves
1109+/// files into
1110+const ASIDE: &str = "aside";
1111+
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`).
1126+struct Aside {
1127+ root: std::path::PathBuf,
1128+ dir: Option<tempfile::TempDir>,
1129+ stuck: Option<(RepoPathBuf, std::io::Error)>,
1130+ returned: bool,
1131+}
1132+
1133+impl Aside {
1134+ /// Move `disk`, the file at `path`, into the directory, made on first
1135+ /// use; renamed, it is neither copied nor followed if a symlink
1136+ fn take(&mut self, path: &RepoPath, disk: &std::path::Path) -> std::io::Result<()> {
1137+ let dir = match &mut self.dir {
1138+ Some(dir) => dir,
1139+ none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
1140+ };
1141+ let to = path.to_fs_path(dir.path()).map_err(std::io::Error::other)?;
1142+ if let Some(parent) = to.parent() {
1143+ std::fs::create_dir_all(parent)?;
1144+ }
1145+ std::fs::rename(disk, to)
1146+ }
1147+}
1148+
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);
1153+ }
1154+ }
1155+}
1156+
1157+/// Put each file in `aside`, where `Aside` moved it, back at its path in
1158+/// the working directory `root` where nothing stands, making the
1159+/// directories above it that are missing, and, where the checkout may have
1160+/// been `cut` short, where a start of it stands, less than all of it, as a
1161+/// checkout cut short while writing it leaves (`cut_short`), which it
1162+/// replaces; one that cannot be put back, as something else stands there
1163+/// or in place of a directory above it, stays in `aside`. No symlink is
1164+/// followed.
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)
1168+ };
1169+ // whether each directory above `path` in `root` is one, or is made
1170+ let dirs_above = |path: &std::path::Path| {
1171+ let mut at = root.to_owned();
1172+ for name in path.parent().into_iter().flat_map(|p| p.components()) {
1173+ at.push(name);
1174+ let made = match at.symlink_metadata() {
1175+ Ok(meta) => meta.is_dir(),
1176+ Err(e) => e.kind() == std::io::ErrorKind::NotFound && std::fs::create_dir(&at).is_ok(),
1177+ };
1178+ if !made {
1179+ return false;
1180+ }
1181+ }
1182+ true
1183+ };
1184+ let mut dirs = vec![std::path::PathBuf::new()];
1185+ while let Some(dir) = dirs.pop() {
1186+ let Ok(entries) = std::fs::read_dir(aside.join(&dir)) else {
1187+ continue;
1188+ };
1189+ for entry in entries.flatten() {
1190+ let path = dir.join(entry.file_name());
1191+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1192+ dirs.push(path);
1193+ continue;
1194+ }
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);
1200+ }
1201+ }
1202+ }
1203+}
1204+
1205+/// Whether `disk` is a file holding a start of `whole`, a file moved
1206+/// aside, and less than all of it, or, where `whole` is a symlink, an
1207+/// empty file: what jj's checkout leaves where it was cut short (a full
1208+/// disk, a run killed) while it wrote `whole` there, as it makes a file
1209+/// empty before it writes it, and tries a symlink's path with an empty
1210+/// file first. Reads no more of either than `disk` holds. No symlink is
1211+/// followed.
1212+fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1213+ use std::io::Read;
1214+ use std::os::unix::fs::OpenOptionsExt;
1215+ let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1216+ return false;
1217+ };
1218+ if !stands.is_file() {
1219+ return false;
1220+ }
1221+ if copy.is_symlink() {
1222+ return stands.len() == 0;
1223+ }
1224+ if !copy.is_file() || stands.len() >= copy.len() {
1225+ return false;
1226+ }
1227+ let stands_file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW).open(disk);
1228+ let (Ok(mut stands_file), Ok(mut copy_file)) = (stands_file, std::fs::File::open(whole)) else {
1229+ return false;
1230+ };
1231+ let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232+ let mut rest = stands.len();
1233+ while rest > 0 {
1234+ let n = rest.min(a.len() as u64) as usize;
1235+ let same = stands_file.read_exact(&mut a[..n]).is_ok()
1236+ && copy_file.read_exact(&mut b[..n]).is_ok()
1237+ && a[..n] == b[..n];
1238+ if !same {
1239+ return false;
1240+ }
1241+ rest -= n as u64;
1242+ }
1243+ true
1244+}
1245+
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 {
1253+ return;
1254+ };
1255+ for entry in entries.flatten() {
1256+ let named = entry.file_name().to_str().is_some_and(|n| n.starts_with(ASIDE));
1257+ if named && entry.file_type().is_ok_and(|t| t.is_dir()) {
1258+ put_back(root, &entry.path(), true);
1259+ let _ = std::fs::remove_dir_all(entry.path());
1260+ }
1261+ }
1262+}
1263+
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,
1273+ from: &MergedTree,
1274+ to: &MergedTree,
1275+) -> Result<Option<RepoPathBuf>, String> {
1276+ let mut dirs = HashMap::new();
1277+ let mut diff = from.diff_stream(to, &EverythingMatcher);
1278+ while let Some(entry) = diff.next().await {
1279+ let Diff { before, after } = entry.values.map_err(|e| e.to_string())?;
1280+ if after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
1281+ continue;
1282+ }
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));
1287+ }
1288+ continue;
1289+ }
1290+ let Ok(disk) = path.to_fs_path(root) else {
1291+ continue;
1292+ };
1293+ let Ok(meta) = disk.symlink_metadata() else {
1294+ continue;
1295+ };
1296+ if meta.is_dir()
1297+ || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1298+ {
1299+ return Ok(Some(path));
1300+ }
1301+ }
1302+ Ok(None)
1303+}
1304+
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)
1311+async fn save_scan(
1312+ mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313+ scanned: &MergedTree,
1314+ wc: &MergedTree,
1315+ head: &OperationId,
1316+) -> Result<(), String> {
1317+ let op_id = if scanned.tree_ids() == wc.tree_ids() {
1318+ head.clone()
1319+ } else {
1320+ locked_ws.locked_wc().old_operation_id().clone()
1321+ };
1322+ locked_ws.finish(op_id).await.map_err(|e| e.to_string())
1323+}
1324+
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);
1328+
1329+extern "C" fn defer(sig: libc::c_int) {
1330+ DEFERRED.store(sig, std::sync::atomic::Ordering::SeqCst);
1331+}
1332+
1333+/// While it lives, SIGINT, SIGTERM, SIGHUP and SIGQUIT, whichever thread
1334+/// gets them, only note that they came: it lives from the operation's
1335+/// publishing until the working copy's state is saved, so that a checkout
1336+/// the user interrupts completes, leaving the working copy current, and
1337+/// through a clone, which one that comes before the working-copy commit is
1338+/// made fails (`unless_interrupted`), so that it removes what it made
1339+/// (§1.3). A signal the process ignores stays ignored, as it does for the
1340+/// git a clone runs (`nohup j clone`, a background job). Dropping it gives
1341+/// each signal back what it did before, and `raise_deferred` then acts on
1342+/// one that came.
1343+struct Deferral {
1344+ previous: Vec<(libc::c_int, libc::sigaction)>,
1345+}
1346+
1347+impl Deferral {
1348+ fn start() -> Deferral {
1349+ let mut previous = Vec::new();
1350+ for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1351+ // SAFETY: the handler only stores to an atomic, which is
1352+ // async-signal-safe; both actions are initialised before use
1353+ unsafe {
1354+ let mut old: libc::sigaction = std::mem::zeroed();
1355+ if libc::sigaction(sig, std::ptr::null(), &mut old) != 0
1356+ || old.sa_sigaction == libc::SIG_IGN
1357+ {
1358+ continue;
1359+ }
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));
1366+ }
1367+ }
1368+ }
1369+ Deferral { previous }
1370+ }
1371+}
1372+
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()) };
1378+ }
1379+ }
1380+}
1381+
1382+/// Act on a signal a checkout or a clone deferred (§1.3) as it would have
1383+/// acted then: with its default action, stopping the process (`Deferral`
1384+/// defers no signal the process ignores). Called once the run has reported
1385+/// what it did.
1386+pub fn raise_deferred() {
1387+ let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
1388+ if sig != 0 {
1389+ // a process a signal stops does not flush what it printed
1390+ use std::io::Write;
1391+ let _ = std::io::stdout().flush();
1392+ // SAFETY: raising a signal whose action `Deferral` gave back
1393+ unsafe { libc::raise(sig) };
1394+ }
1395+}
1396+
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()))
1406+ } else {
1407+ done
1408+ }
1409+}
1410+
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();
1415+ Commit::new(
1416+ commit.store().clone(),
1417+ commit.id().clone(),
1418+ Arc::new(jj_lib::backend::Commit {
1419+ root_tree: tree_ids,
1420+ conflict_labels: labels.into_merge(),
1421+ ..commit.store_commit().as_ref().clone()
1422+ }),
1423+ )
2751424}
2761425
2771426#[derive(Clone)]
2781427pub struct JjBackend {
2791428 inner: Arc<JjInner>,
1429+ replay_cache: Rc<ReplayCache>,
2801430}
2811431
2821432impl JjBackend {
@@ -284,7 +1434,7 @@ impl JjBackend {
2841434 let root = find_repo_root().ok_or_else(|| {
2851435 (
2861436 2,
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(),
2881438 )
2891439 })?;
2901440 let settings = user_settings(cfg);
@@ -305,10 +1455,22 @@ impl JjBackend {
3051455 ),
3061456 ));
3071457 }
1458+ // Snapshot and checkout both apply patterns saved by `jj sparse`,
1459+ // independently of our SnapshotOptions. Accepting a partial working
1460+ // copy would silently omit disk edits and skip files during checkout.
1461+ let sparse = workspace.working_copy().sparse_patterns()
1462+ .map_err(|e| (2, format!("cannot read the working copy's sparse patterns: {}", e)))?;
1463+ if !sparse.iter().any(|path| path.is_root()) {
1464+ return Err((
1465+ 2,
1466+ "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
1467+ ));
1468+ }
3081469 let workspace_name = workspace.workspace_name().to_owned();
3091470 let repo = block_on(workspace.repo_loader().load_at_head())
3101471 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
3111472 Ok(JjBackend {
1473+ replay_cache: Rc::new(ReplayCache::default()),
3121474 inner: Arc::new(JjInner {
3131475 workspace_root: root,
3141476 workspace: Mutex::new(workspace),
@@ -317,6 +1479,8 @@ impl JjBackend {
3171479 visible: Mutex::new(None),
3181480 lock_guard: Mutex::new(None),
3191481 pending: Mutex::new(None),
1482+ folds_case: OnceLock::new(),
1483+ long_names: Mutex::new(HashMap::new()),
3201484 }),
3211485 })
3221486 }
@@ -336,21 +1500,22 @@ impl JjBackend {
3361500 {
3371501 Ok(f) => f,
3381502 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));
3401504 std::process::exit(2);
3411505 }
3421506 };
3431507 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
3441508 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"));
3461510 std::process::exit(2);
3471511 }
3481512 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513+ put_back_left(&self.inner.workspace_root);
3491514 }
3501515
3511516 pub fn build_interp(
3521517 &self,
353− cfg: &Config,
1518+ cfg: &mut Config,
3541519 _text: &str,
3551520 snapshot: bool,
3561521 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -359,8 +1524,8 @@ impl JjBackend {
3591524 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
3601525 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
3611526 *self.inner.repo.lock().unwrap() = head_repo;
362− let loaded_visible = block_on(read_visible(&self.current_repo()))?;
363− let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id)?;
1527+ let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
1528+ let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
3641529
3651530 let current = if snapshot {
3661531 // §7.4: snapshot the working directory into the wc commit; the
@@ -371,19 +1536,19 @@ impl JjBackend {
3711536 // no working-copy change: the repo is the one already loaded,
3721537 // so reuse its value instead of rebuilding it
3731538 None => {
374− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1539+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
3751540 loaded.clone()
3761541 }
3771542 Some(pending) => {
3781543 *self.inner.pending.lock().unwrap() = Some(pending);
3791544 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)?;
3811546 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
3821547 cur
3831548 }
3841549 }
3851550 } else {
386− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1551+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
3871552 loaded.clone()
3881553 };
3891554
@@ -392,35 +1557,59 @@ impl JjBackend {
3921557 cfg.shapes.clone(),
3931558 crate::value::Env::empty(),
3941559 );
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()
1564+ {
1565+ return Err((
1566+ 3,
1567+ format!(
1568+ "config.j: `@{}` in `{}` does not resolve to a unique commit",
1569+ prefix, def
1570+ ),
1571+ ));
1572+ }
3951573 // §7.2: if the focused commit is in the immutable set, the snapshot
3961574 // cannot go into it; the focus is a new empty child of it holding the
3971575 // snapshot, and persisting records that child
398− match self.focus_if_immutable(&mut interp, cfg, ¤t)? {
399− Some(adjusted) => {
400− // the persisted child is part of the loaded value too, so a
401− // run that changes nothing is a no-op
402− Ok((interp, adjusted.clone(), adjusted))
1576+ match self.focus_if_immutable(&mut interp, cfg, &loaded, ¤t)? {
1577+ Some((loaded, current)) => {
1578+ // the snapshot's rewrite of the wc commit (and its rebase of
1579+ // the descendants) is not the repository's: lookups answer
1580+ // from the stored commits, and persisting leaves the wc
1581+ // commit as stored rather than folding the snapshot into it
1582+ *self.inner.visible.lock().unwrap() = Some(loaded_visible);
1583+ if let Some(p) = self.inner.pending.lock().unwrap().as_mut() {
1584+ p.fold_into_wc = false;
1585+ }
1586+ Ok((interp, loaded, current))
4031587 }
4041588 None => Ok((interp, loaded, current)),
4051589 }
4061590 }
4071591
408− /// If the focus of `current` is immutable, return a repo refocused on a
409− /// new empty child holding the focus's files (§7.2).
1592+ /// If the focus of `current` is immutable, return the loaded and current
1593+ /// repos refocused on a new empty child of it (§7.2). Both are `loaded`
1594+ /// with that one child added: the snapshot goes into neither the focus
1595+ /// nor, through a rebase, its descendants. The child holds the focus's
1596+ /// stored files in the loaded repo and the snapshot in the current one,
1597+ /// so a dirty `j id` differs from the loaded value and records the child
1598+ /// (§1.2 step 8).
4101599 fn focus_if_immutable(
4111600 &self,
4121601 interp: &mut Interp,
4131602 cfg: &Config,
1603+ loaded: &Value,
4141604 current: &Value,
415− ) -> Result<Option<Value>, OpenError> {
416− // evaluate the config to compute the immutable set
417− let mut probe = Interp::new(
418− Rc::new(self.clone()),
419− cfg.shapes.clone(),
420− crate::value::Env::empty(),
421− );
422− crate::config::eval_config(&mut probe, cfg).map_err(|c| (3, c.msg))?;
423− let immutable = crate::repo::compute_immutable(&mut probe, current)
1605+ ) -> Result<Option<(Value, Value)>, OpenError> {
1606+ // Evaluate the config to compute the immutable set. This uses the
1607+ // interpreter the run will go on to use, rather than a throwaway one,
1608+ // so that the `immutable` (and the `trunk` inside it) computed here is
1609+ // reused when rendering or persisting asks for the same Repo value —
1610+ // each is a full interpreted walk of the history.
1611+ crate::config::eval_config(interp, cfg).map_err(|c| (3, c.msg))?;
1612+ let immutable = crate::repo::compute_immutable(interp, current)
4241613 .map_err(|c| (1, c.msg))?;
4251614 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
4261615 Ok(Value::Id(i)) => i.to_string(),
@@ -429,90 +1618,101 @@ impl JjBackend {
4291618 if !immutable.contains(&focus_id) {
4301619 return Ok(None);
4311620 }
432− // build: new empty child of the focus with the focus's files, minted id
433− let _ = interp;
434− let root = current.field("root").map_err(|c| (1, c.msg))?;
435− let files = root.field("files").map_err(|c| (1, c.msg))?;
436− let child = crate::value::Value::record(&[
437− ("files", files),
438− ("message", crate::value::Value::text("")),
439− ("labels", crate::value::Value::list(vec![])),
440− (
441− "id",
442− crate::value::Value::Id(Rc::new(probe.mint_id())),
443− ),
444− ]);
445− let child_subtree = crate::value::Value::record(&[
446− ("root", child.clone()),
447− ("children", crate::value::Value::list(vec![])),
448− ]);
449− let mut new_children: Vec<Value> = current
450− .field("children")
451− .map_err(|c| (1, c.msg))?
452− .as_list()
453− .map_err(|c| (1, c.msg))?
454− .to_vec();
455− new_children.push(child_subtree);
456− let parent = crate::value::Value::record(&[
457− ("children", crate::value::Value::list(new_children)),
458− ("context", current.field("context").map_err(|c| (1, c.msg))?),
459− ("root", root),
1621+ // the focus's location in `loaded`, pre-snapshot; the child is its
1622+ // last child, so every existing child sits to the left of it
1623+ let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
1624+ let root = field(loaded, "root")?;
1625+ let frame = Value::record(&[
1626+ ("left", field(loaded, "children")?),
1627+ ("parent", root.clone()),
1628+ ("right", Value::list(vec![])),
4601629 ]);
461− // refocus onto the child (it is the last child)
462− let child_id = match child.field("id").map_err(|c| (1, c.msg))? {
463− Value::Id(i) => i.to_string(),
464− _ => unreachable!(),
1630+ let mut frames = vec![frame];
1631+ frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
1632+ let context = Value::list(frames);
1633+ let id = Value::Id(Rc::new(interp.mint_id()));
1634+ let at_child = |files: Value| {
1635+ Value::record(&[
1636+ ("children", Value::list(vec![])),
1637+ ("context", context.clone()),
1638+ (
1639+ "root",
1640+ Value::record(&[
1641+ ("files", files),
1642+ ("id", id.clone()),
1643+ ("labels", Value::list(vec![])),
1644+ ("message", Value::text("")),
1645+ ]),
1646+ ),
1647+ ])
1648+ };
1649+ let current_at_child = at_child(field(&field(current, "root")?, "files")?);
1650+ // with nothing snapshotted `current` is `loaded` itself: share one
1651+ // value, so a run that changes nothing is a no-op without comparing
1652+ // the whole repository
1653+ let loaded_at_child = match (loaded, current) {
1654+ (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b) => current_at_child.clone(),
1655+ _ => at_child(field(&root, "files")?),
4651656 };
466− crate::repo::by_id(&parent, &child_id)
467− .map_err(|c| (1, c.msg))?
468− .ok_or_else(|| (1, "internal: cannot refocus".to_string()))
469− .map(Some)
1657+ Ok(Some((loaded_at_child, current_at_child)))
4701658 }
4711659
472− /// Snapshot the working copy into the wc commit; returns the repo at the
473− /// snapshot operation (unpublished but written), or the pre-snapshot repo
474− /// if nothing changed.
1660+ /// Snapshot the working directory into the working-copy commit (§7.4);
1661+ /// return the repo at the snapshot's operation, written but not
1662+ /// published, or the repo as loaded when there is nothing to record.
4751663 async fn snapshot_repo(
4761664 &self,
4771665 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
4781666 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);
4801669 let mut locked_ws = ws_guard.start_working_copy_mutation()
4811670 .await
4821671 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
483− let options = SnapshotOptions {
484− base_ignores: GitIgnoreFile::empty(),
485− progress: None,
486− start_tracking_matcher: &EverythingMatcher,
487− force_tracking_matcher: &NothingMatcher,
488− max_new_file_size: u64::MAX,
489− };
490− let (new_tree, _stats) = locked_ws
491− .locked_wc()
492− .snapshot(&options)
1672+ let old_tree = locked_ws.locked_wc().old_tree().clone();
1673+ let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
4931674 .await
4941675 .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();
496− if !changed {
497− locked_ws
498− .finish(self.current_repo().operation().id().clone())
1676+ // the repo build_interp loaded at head from the workspace's own
1677+ // loader, whose store the scanned tree lives in
1678+ let head = self.current_repo();
1679+ let wc_commit = self.wc_commit(&head).await?;
1680+ let current = locked_ws.locked_wc().old_operation_id() == head.operation().id();
1681+ let tree = match marker_style(head.settings()) {
1682+ Ok(markers) => snapshot_tree(&root, markers, &wc_commit.tree(), &old_tree, current, &scanned).await,
1683+ Err(e) => Err(e),
1684+ }
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())
4991693 .await
5001694 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
501− return Ok((self.current_repo(), None));
1695+ return Ok((head, None));
5021696 }
503− // the snapshotted tree lives in the workspace's store; drive the
504− // transaction from the workspace's repo loader so stores match
505− let ws_repo = loader
506− .load_at_head()
507− .await
508− .map_err(|e| (2, format!("cannot reload the repository: {}", e)))?;
509− let mut tx = ws_repo.start_transaction();
1697+ // a directory come to hold the parent's files, as where it reverts
1698+ // the commit's change, is recorded with the parent's tree (§7.4), as
1699+ // persistence writes a commit with its parent's files (§7.5 step 4):
1700+ // the snapshot builds on the commit's tree, and so could make
1701+ // another tree for the parent's files, one jj takes for a change.
1702+ // Not before the check above, or a commit already holding such a
1703+ // tree would be rewritten by a directory that has not changed. A
1704+ // merge is immutable, so never folded into (§7.2), and its parents'
1705+ // trees are not merged for it
1706+ let tree = match wc_commit.parent_ids() {
1707+ [_] => parents_tree_if_same(head.as_ref(), wc_commit.parent_ids(), tree).await,
1708+ _ => tree,
1709+ };
1710+ let mut tx = head.start_transaction();
5101711 tx.set_is_snapshot(true);
511− let wc_commit = self.wc_commit(&ws_repo).await?;
5121712 let new_wc = tx
5131713 .repo_mut()
5141714 .rewrite_commit(&wc_commit)
515− .set_tree(new_tree)
1715+ .set_tree(tree)
5161716 .write()
5171717 .await
5181718 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
@@ -529,25 +1729,26 @@ impl JjBackend {
5291729 .write("snapshot working copy")
5301730 .await
5311731 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
532− let op_id = unpublished.operation().id().clone();
5331732 let new_repo = unpublished.leave_unpublished();
534− // the working-copy state tracks the snapshot tree, against the
535− // current (published) head; persist will finish against its own op
536− locked_ws
537− .finish(self.current_repo().operation().id().clone())
538− .await
539− .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
540− let _ = op_id;
1733+ // Release the lock *without* finishing: `finish` saves the scanned
1734+ // tree state to disk, which would record the working directory as
1735+ // already snapshotted while the published head still has the old wc
1736+ // tree. The next run's incremental snapshot would then find nothing
1737+ // to do and build the Repo from the stale tree — and a persisting run
1738+ // would check that stale tree back out, destroying the edits. The
1739+ // state is advanced only by `persist`'s checkout, which first scans
1740+ // the directory again and finishes against the operation that
1741+ // actually recorded the tree (§7.4/§7.7).
1742+ drop(locked_ws);
5411743 let pending = PendingSnapshot {
542− pre_repo: self.current_repo(),
543− new_wc_id: new_wc.id().clone(),
1744+ pre_repo: head,
5441745 tree: new_wc.tree().clone(),
1746+ fold_into_wc: true,
1747+ scanned,
5451748 };
5461749 Ok((new_repo, Some(pending)))
5471750 }
5481751
549−
550−
5511752 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
5521753 let id = repo
5531754 .view()
@@ -565,12 +1766,13 @@ impl JjBackend {
5651766 interp: &mut Interp,
5661767 cfg: &Config,
5671768 loaded: &Value,
568− _current: &Value,
1769+ current: &Value,
5691770 new: &Value,
5701771 text: &str,
571− ) -> Result<(), Crash> {
572− if crate::value::value_eq(loaded, new)? {
573− return Ok(());
1772+ ) -> Result<bool, Crash> {
1773+ // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
1774+ if crate::repo::same_repo(loaded, new)? {
1775+ return Ok(false);
5741776 }
5751777 *interp.old_repo.borrow_mut() = Some(loaded.clone());
5761778 crate::repo::validate_repo(interp, new)?;
@@ -590,7 +1792,7 @@ impl JjBackend {
5901792 // walk below compares against its output, so the snapshot change and
5911793 // the expression's edits become one rewrite
5921794 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) {
5941796 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
5951797 let c = block_on(
5961798 tx.repo_mut()
@@ -614,6 +1816,9 @@ impl JjBackend {
6141816 old_stored = Arc::new(vis);
6151817 }
6161818 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])?;
6171822 // shared across the whole persist walk so unchanged blobs are inflated once
6181823 let cache = BlobCache::default();
6191824 let entries = EntryCache::default();
@@ -637,33 +1842,35 @@ impl JjBackend {
6371842 }
6381843 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
6391844 .map_err(|e| Crash::new(e.msg))?;
640− if !crate::value::value_eq(
641− &Value::list(stored_files),
642− &Value::list(root_v.field("files")?.as_list()?.to_vec()),
643− )? {
1845+ if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
6441846 return Err(Crash::new("persistence: the root commit cannot be changed"));
6451847 }
6461848 }
6471849 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
6481850
6491851 // walk `new` top-down (§7.5 step 4), parents before children
650− let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
1852+ // a deque, not a Vec: `remove(0)` shifts everything after it, which
1853+ // is quadratic in the number of commits for a wide history
1854+ let mut queue: std::collections::VecDeque<(Value, String)> =
1855+ std::collections::VecDeque::new(); // (subtree value, parent change id)
6511856 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()));
6531858 }
654− while !queue.is_empty() {
655− let (subtree, parent_change) = queue.remove(0);
1859+ while let Some((subtree, parent_change)) = queue.pop_front() {
6561860 let commit_v = subtree.field("root")?;
6571861 let id = commit_id_of(&commit_v)?;
6581862 let parent_jj = written
6591863 .get(&parent_change)
6601864 .cloned()
6611865 .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)?;
6631869 let message = commit_v.field("message")?.as_text()?.to_string();
6641870 let new_id = match old_stored.commits.get(&id) {
6651871 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))?;
6671874 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
6681875 Crash::new(format!("persistence: `{}` is not a valid change id", id))
6691876 })?;
@@ -682,17 +1889,31 @@ impl JjBackend {
6821889 Some(stored) => {
6831890 let stored_commit = &stored.commit;
6841891 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
685− let files_changed = {
686− let stored_files =
687− block_on(tree_to_files(&store, &stored_commit.tree(), &cache, &entries))?;
688− !crate::value::value_eq(
689− &Value::list(stored_files),
690− &Value::list(files_v.as_list()?.to_vec()),
691− )?
692− };
1892+ let stored_tree = stored_commit.tree();
1893+ // loaded from the stored tree, the files are unchanged:
1894+ // reading every commit's to compare them made each
1895+ // persist read the files of the whole history
1896+ let files_changed = crate::repo::stored_tree(&files_v)
1897+ != Some(tree_name(&stored_tree).as_str())
1898+ && {
1899+ let stored_files =
1900+ block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
1901+ // a tree has no order: entries listed differently
1902+ // are not a change to rewrite (§7.3)
1903+ !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
1904+ };
6931905 let msg_changed = stored_commit.description() != message;
6941906 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)?
1914+ } else {
1915+ block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
1916+ };
6961917 let c = block_on(
6971918 tx.repo_mut()
6981919 .rewrite_commit(stored_commit)
@@ -711,7 +1932,7 @@ impl JjBackend {
7111932 };
7121933 written.insert(id.clone(), new_id);
7131934 for kid in subtree.field("children")?.as_list()?.iter() {
714− queue.push((kid.clone(), id.clone()));
1935+ queue.push_back((kid.clone(), id.clone()));
7151936 }
7161937 }
7171938
@@ -755,37 +1976,230 @@ impl JjBackend {
7551976 let desc = truncate_chars(text, 200);
7561977 let unpublished = block_on(tx.write(desc))
7571978 .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)))?;
7611979
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
7631981 let focus_commit = block_on(store.get_commit_async(&focus_jj))
7641982 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
765− block_on(self.checkout(&focus_commit, op_id))?;
766−
767− Ok(())
1983+ // the checkout starts from what the working directory holds: the
1984+ // tree the run's snapshot scanned, or, where it had nothing to
1985+ // record, the tree the working-copy state records, as
1986+ // `snapshot_repo` left it. That is the focus as loaded unless the
1987+ // state is stale (§7.4), when the checkout writes the focus over it;
1988+ // taking the directory to hold the focus instead, it would find it
1989+ // did not, describe the focus without writing it (a reset), and the
1990+ // next run would record the stale files into the focus.
1991+ let on_disk = match pending {
1992+ Some(p) => p.scanned,
1993+ None => {
1994+ let state = self.inner.workspace.lock().unwrap().working_copy().tree().cloned();
1995+ // a state that cannot be read fails the checkout, which
1996+ // reads it again when it locks
1997+ state.or_else(|_| block_on(self.wc_commit(&base)).map(|c| c.tree()).map_err(|e| Crash::new(e.1)))?
1998+ }
1999+ };
2000+ block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
2001+ Ok(true)
7682002 }
7692003
770− async fn checkout(&self, commit: &Commit, op_id: OperationId) -> Result<(), Crash> {
2004+ /// Check `commit` out: write its files to the working directory and
2005+ /// save the working copy's state as holding them (§7.4). `op`, the
2006+ /// operation that makes it the working-copy commit, is published first,
2007+ /// as jj does (§7.5 step 7); with none, `commit` is the head's own.
2008+ /// `on_disk` is the tree the directory holds, which the checkout starts
2009+ /// from: what a persisting run's snapshot scanned, or the tree the
2010+ /// working-copy state records where it scanned nothing to record, or
2011+ /// what `undo`'s look found; with none, the state as it is. A change
2012+ /// made to the directory since `on_disk` was scanned, where the
2013+ /// checkout writes, refuses the checkout before `op` is published
2014+ /// (`written_over`); one made once it has begun is written over, as by
2015+ /// jj. Where the commit adds a file, something `on_disk` does not track
2016+ /// standing there, but for empty directories and what the checkout
2017+ /// writes, refuses the checkout before `op` is published
2018+ /// (`look_where_added`), and one put there since fails it after, as it
2019+ /// makes jj skip the path. A checkout that fails, or is cut short, after
2020+ /// `op` is published leaves `op` recorded and the state stale, recording
2021+ /// `on_disk`: the next run takes what the checkout wrote for the
2022+ /// commit's own and carries on (`snapshot_tree`), as the crash says,
2023+ /// with `back` naming the way back; with no `back` it says nothing of
2024+ /// the operation (a clone that fails removes it).
2025+ async fn checkout(
2026+ &self,
2027+ commit: &Commit,
2028+ on_disk: Option<&MergedTree>,
2029+ op: Option<UnpublishedOperation>,
2030+ back: Option<&str>,
2031+ ) -> Result<(), Crash> {
7712032 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);
7722035 let mut locked_ws = ws_guard.start_working_copy_mutation()
7732036 .await
7742037 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
775− locked_ws
776− .locked_wc()
777− .check_out(commit)
778− .await
2038+ // A persisting run's snapshot released its lock without saving what
2039+ // it scanned, so the state just loaded may still describe the tree
2040+ // recorded before the edits, and jj checks out by diffing that tree
2041+ // against the new one: a path whose recorded content equals the
2042+ // focus's would keep the user's edit, a file created since would
2043+ // stay, and a file replaced by a directory (or back) would leave
2044+ // state contradicting the tree. Scan the directory again first, as
2045+ // the snapshot did, so the checkout diffs from what is actually on
2046+ // disk (§7.4). A scan keeps what the state knows of each file where
2047+ // a reset to the snapshot's tree would forget it: a conflict whose
2048+ // sides hold marker-like lines is materialized with longer markers,
2049+ // and a forgotten length makes the next snapshot read those markers
2050+ // back at 7 characters, fail to parse them and record the edited
2051+ // conflict as resolved text, markers and all (§7.4).
2052+ if let Some(on_disk) = on_disk {
2053+ let old_tree = locked_ws.locked_wc().old_tree().clone();
2054+ let reset = match scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()).await {
2055+ Ok(scanned) if scanned.tree_ids() == on_disk.tree_ids() => false,
2056+ // the directory changed while the program ran: where the
2057+ // checkout would write the focus over such a change, which
2058+ // no snapshot holds and jj's checkout does not look for,
2059+ // the run crashes before anything is recorded or written
2060+ // (§7.5 step 7)
2061+ Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
2062+ Ok(None) => true,
2063+ Ok(Some(path)) => {
2064+ return Err(Crash::new(format!(
2065+ "cannot check out the focus: `{}` changed in the working directory while the program ran, and the checkout would write over it; nothing was recorded, so run the command again",
2066+ path.as_internal_file_string()
2067+ )));
2068+ }
2069+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2070+ },
2071+ // or it can no longer be scanned (an entry appeared,
2072+ // vanished or cannot be read; a failed scan leaves the state
2073+ // as it was)
2074+ Err(_) => true,
2075+ };
2076+ // Otherwise the state describes `on_disk` instead. A reset
2077+ // writes no file; the paths it changes are re-read by the next
2078+ // run.
2079+ if reset {
2080+ locked_ws
2081+ .locked_wc()
2082+ .reset(&with_tree(commit, on_disk.clone()))
2083+ .await
2084+ .map_err(|e| Crash::new(format!("cannot reset the working copy: {}", e)))?;
2085+ }
2086+ }
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())
7792095 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
780− locked_ws
781− .finish(op_id)
782− .await
783− .map_err(|e| Crash::new(format!("cannot finish the checkout: {}", e)))?;
2096+ let clear = match on_disk {
2097+ None => Vec::new(),
2098+ Some(on_disk) => match look_where_added(&root, markers, on_disk, &commit.tree()).await {
2099+ Ok(Ok(clear)) => clear,
2100+ Ok(Err(in_the_way)) => {
2101+ return Err(Crash::new(format!("cannot check out the focus: {}", in_the_way_of(&in_the_way))));
2102+ }
2103+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2104+ },
2105+ };
2106+ // from here until the state is saved, a signal to stop waits
2107+ let _deferral = Deferral::start();
2108+ let published = op.is_some();
2109+ let op_id = match op {
2110+ None => self.current_repo().operation().id().clone(),
2111+ Some(op) => {
2112+ let repo = op
2113+ .publish()
2114+ .await
2115+ .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
2116+ let op_id = repo.operation().id().clone();
2117+ *self.inner.repo.lock().unwrap() = repo;
2118+ op_id
2119+ }
2120+ };
2121+ // what a crash from here on says: the state is left stale, which the
2122+ // next run carries on from (§7.5 step 7)
2123+ let carry_on = |what: &str| match back {
2124+ None => String::new(),
2125+ Some(back) if published => {
2126+ format!("; the operation is recorded, but {}: the next command carries on from there{}", what, back)
2127+ }
2128+ Some(_) => format!("; {}: the next command carries on from there", what),
2129+ };
2130+ // A checkout cut short leaves the directory holding `on_disk` but
2131+ // where it wrote, and the next run takes what it holds for changes
2132+ // to the state (`snapshot_tree`). So the state records `on_disk`
2133+ // first, at the operation it was saved at, which leaves it stale
2134+ // until the checkout completes: were it left recording the tree
2135+ // before the run's snapshot, the snapshot's changes, now in the
2136+ // operation, would be taken for new ones and replayed onto the
2137+ // focus too. It is saved only once `op` is published: a state
2138+ // recording the snapshot, beside a head that does not, would lose
2139+ // it (§7.4).
2140+ if on_disk.is_some_and(|t| t.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids()) {
2141+ let old_op = locked_ws.locked_wc().old_operation_id().clone();
2142+ let saved = match locked_ws.finish(old_op).await {
2143+ Ok(()) => ws_guard.start_working_copy_mutation().await.map_err(|e| e.to_string()),
2144+ Err(e) => Err(e.to_string()),
2145+ };
2146+ locked_ws = match saved {
2147+ Ok(locked) => locked,
2148+ Err(e) => {
2149+ let what = "the working directory was not updated";
2150+ return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2151+ }
2152+ };
2153+ }
2154+ // what is moved aside is put back where the checkout does not write,
2155+ // or cuts the write short, once it returns, however it returns
2156+ let mut aside = clear_the_way(&root, markers, &commit.tree(), &clear).await;
2157+ let from = locked_ws.locked_wc().old_tree().clone();
2158+ let what = "the working directory was only partly updated";
2159+ let stats = match locked_ws.locked_wc().check_out(commit).await {
2160+ Ok(stats) => stats,
2161+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}{}", e, carry_on(what)))),
2162+ };
2163+ // it cut no write short, so a start of a file moved aside, standing
2164+ // at its path, was put there meanwhile
2165+ aside.returned = true;
2166+ // Something the directory does not track, put where the focus adds a
2167+ // file since it was looked at, or a file holding what the checkout
2168+ // writes there that could not be moved aside, made jj skip the
2169+ // path, which it records as written. The state is left stale, as by
2170+ // a checkout that fails, so that no run reads what stands there
2171+ // (§7.5 step 7).
2172+ if stats.skipped_files > 0 {
2173+ let skipped = first_skipped(&root, markers, &from, &commit.tree()).await.ok().flatten();
2174+ let why = match (skipped, &aside.stuck) {
2175+ (Some(path), _) => format!(
2176+ "`{}` was not written, as something untracked in the working directory was in its way (move it aside)",
2177+ path.as_internal_file_string()
2178+ ),
2179+ (None, Some((path, e))) => format!(
2180+ "`{}` was not written, as the untracked file there, holding what it writes, could not be moved out of its way: {}",
2181+ path.as_internal_file_string(),
2182+ e
2183+ ),
2184+ (None, None) => format!(
2185+ "{} of its files were not written, as something untracked in the working directory was in their way",
2186+ stats.skipped_files
2187+ ),
2188+ };
2189+ return Err(Crash::new(format!("cannot check out the focus: {}{}", why, carry_on(what))));
2190+ }
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))));
2196+ }
7842197 Ok(())
7852198 }
7862199
7872200 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))?;
7892203 Ok(Signature {
7902204 name,
7912205 email,
@@ -878,6 +2292,7 @@ impl JjBackend {
8782292 }
8792293
8802294 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295+ let url = &origin_url(url)?;
8812296 self.take_lock();
8822297 let base = self.current_repo();
8832298 let origin = RemoteName::new("origin");
@@ -895,7 +2310,17 @@ impl JjBackend {
8952310 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
8962311 }
8972312
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)))
2320+ }
2321+
8982322 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323+ self.enter_workspace_root()?;
8992324 let origin = RemoteName::new("origin");
9002325 let base = self.head_repo()?;
9012326 let mut tx = base.start_transaction();
@@ -908,27 +2333,52 @@ impl JjBackend {
9082333 let _ = block_on(self.publish_tx(tx, "fetch"))?;
9092334 Ok(())
9102335 }
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()?;
9122338 let origin = RemoteName::new("origin");
9132339 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)?;
9152341 let records = parse_push_records(&value, &vis, &base)?;
916− check_push_records(&records, &vis, &base)?;
917− let _ = self.push_to_origin(&base, origin, &records, expr_text)?;
918− Ok(())
2342+ if records.is_empty() {
2343+ return Ok(PushOutcome::default());
2344+ }
2345+ let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
2346+ let mut outcome = PushOutcome::default();
2347+ for (name, target) in &records {
2348+ let before = base.view()
2349+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2350+ .target.as_resolved().cloned().unwrap_or(None);
2351+ if &before == target {
2352+ outcome.unchanged += 1;
2353+ } else if target.is_none() {
2354+ outcome.deleted += 1;
2355+ } else {
2356+ outcome.published += 1;
2357+ }
2358+ }
2359+ let (_, clean) = self.inspect_working_copy(&base, false)?;
2360+ if !clean {
2361+ crate::terminal::report(format_args!(
2362+ "j: warning: working copy has unrecorded changes; push sends recorded content only, so these edits will not be sent. Run `j id` to record them, then push again to include them."
2363+ ));
2364+ }
2365+ let description = format!("push {}", expr_text);
2366+ let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
2367+ Ok(outcome)
9192368 }
9202369
9212370 /// 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)
9232373 fn eval_push_expr(
9242374 &self,
925− cfg: &Config,
2375+ cfg: &mut Config,
9262376 expr_text: &str,
9272377 base: &Arc<ReadonlyRepo>,
928− ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
2378+ ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
9292379 let outer = Rc::new(cfg.global_names.clone());
9302380 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)))?;
9322382 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
9332383 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
9342384 let (prefix, candidates) = &failures[0];
@@ -939,15 +2389,30 @@ impl JjBackend {
9392389 }
9402390 })?;
9412391 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);
2398+ match &crash.def {
2399+ Some(name) => message.push_str(&format!("\n in {}, from push {}", name, expr_text)),
2400+ None => message.push_str(&format!("\n from push {}", expr_text)),
2401+ }
2402+ if let Some(hint) = crate::diagnostics::push_expression_hint(expr_text, cfg, &crash) {
2403+ message.push_str(&format!("\n {}", hint));
2404+ }
2405+ (1, message)
2406+ };
9422407 let env = interp.global_env();
9432408 let expr_rc = Rc::new(expr);
9442409 let mut v = interp
9452410 .eval(&expr_rc, &env)
946− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2411+ .map_err(&expression_error)?;
9472412 if matches!(v, Value::Fun(_)) {
9482413 v = interp
9492414 .apply(v, current)
950− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2415+ .map_err(expression_error)?;
9512416 }
9522417 let _ = base;
9532418 let vis = self
@@ -957,19 +2422,28 @@ impl JjBackend {
9572422 .unwrap()
9582423 .clone()
9592424 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
960− Ok((v, vis))
2425+ Ok((v, vis, immutable))
9612426 }
9622427
9632428 /// 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
2431+ /// other update
9652432 fn push_to_origin(
9662433 &self,
9672434 base: &Arc<ReadonlyRepo>,
9682435 origin: &RemoteName,
9692436 records: &[(String, Option<CommitId>)],
2437+ deletes_wait: bool,
9702438 description: &str,
9712439 ) -> 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());
2444+ }
9722445 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446+ let mut deletes = jj_lib::git::GitPushRefTargets::default();
9732447 for (name, after) in records {
9742448 let before = base
9752449 .view()
@@ -978,48 +2452,116 @@ impl JjBackend {
9782452 .as_resolved()
9792453 .cloned()
9802454 .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 };
2458+ batch
9822459 .bookmarks
9832460 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
9842461 }
9852462 let mut tx = base.start_transaction();
9862463 let subprocess_options = self.git_subprocess_options()?;
9872464 let mut callback = QuietGitCallback;
988− let stats = jj_lib::git::push_refs(
989− tx.repo_mut(),
990− subprocess_options,
991− origin,
992− &targets,
993− &mut callback,
994− &jj_lib::git::GitPushOptions::default(),
995− )
996− .map_err(|e| match e {
997− jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
998− other => (1, format!("push failed: {}", other)),
999− })?;
2465+ let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
2466+ jj_lib::git::push_refs(
2467+ tx.repo_mut(),
2468+ subprocess_options.clone(),
2469+ origin,
2470+ targets,
2471+ &mut callback,
2472+ &jj_lib::git::GitPushOptions::default(),
2473+ )
2474+ .map_err(|e| match e {
2475+ jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2476+ other => (1, format!("push failed: {}", other)),
2477+ })
2478+ };
2479+ let mut stats = push(&mut tx, &targets)?;
2480+ // git push is not atomic: the remote may accept some updates and
2481+ // reject others. When only a bookmark this push sets keeps a deleted
2482+ // bookmark's immutable commit reachable, the deletes go in a second
2483+ // push once the remote has accepted every update, and are not sent
2484+ // otherwise (§7.6)
2485+ let mut not_sent: Vec<String> = Vec::new();
2486+ let mut failed = None;
2487+ if !deletes.bookmarks.is_empty() {
2488+ if stats.all_ok() {
2489+ match push(&mut tx, &deletes) {
2490+ Ok(more) => {
2491+ stats.pushed.extend(more.pushed);
2492+ stats.rejected.extend(more.rejected);
2493+ stats.remote_rejected.extend(more.remote_rejected);
2494+ stats.unexported_bookmarks.extend(more.unexported_bookmarks);
2495+ }
2496+ Err(e) => failed = Some(e),
2497+ }
2498+ } else {
2499+ for (name, _) in &deletes.bookmarks {
2500+ not_sent.push(format!("refs/heads/{}", name.as_str()));
2501+ }
2502+ }
2503+ }
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();
2514+ }
2515+ if let Some((code, msg)) = failed {
2516+ return Err((code, format!("{}; the other bookmarks were pushed and recorded", msg)));
2517+ }
10002518 if !stats.all_ok() {
10012519 let mut names: Vec<String> = Vec::new();
10022520 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
10032521 names.push(name.as_str().to_string());
10042522 }
1005− return Err((1, format!("push rejected: {}", names.join(", "))));
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()));
2527+ }
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(", ")));
2531+ }
2532+ if stats.some_exported() {
2533+ msg.push_str("; the other bookmarks were pushed and recorded");
2534+ }
2535+ return Err((1, msg));
10062536 }
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();
10122537 Ok(repo)
10132538 }
10142539
1015− pub fn cmd_undo(&self, redo: bool) -> Result<(), OpenError> {
2540+ pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
10162541 let base = self.head_repo()?;
1017− self.check_wc_clean(&base)?;
2542+ let on_disk = self.check_wc_clean(&base)?;
10182543 let loader = self.repo_loader();
10192544 let mut cur = base.operation().clone();
10202545 let target_op_id: OperationId;
10212546 loop {
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)))?;
2556+ continue;
2557+ }
2558+ let Some(parent) = cur.parent_ids().first().cloned() else {
2559+ return Err((1, if redo { "nothing to redo" } else { "nothing to undo" }.to_string()));
2560+ };
2561+ target_op_id = parent;
2562+ break;
2563+ }
2564+ match (marker, redo) {
10232565 (None, false) => {
10242566 let Some(parent) = cur.parent_ids().first().cloned() else {
10252567 return Err((1, "nothing to undo".to_string()));
@@ -1045,11 +2587,25 @@ impl JjBackend {
10452587 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
10462588 }
10472589 (Some(m), true) if m.kind == "undo" => {
1048− target_op_id = m.id;
2590+ // An old j marker names the discarded normal operation,
2591+ // but its parent has the same view and retains any earlier
2592+ // undos. Modern redo markers must keep that navigation
2593+ // point so another redo can continue across the upgrade.
2594+ target_op_id = if m.authored_by_j {
2595+ cur.parent_ids().first().cloned()
2596+ .ok_or_else(|| (1, "nothing to redo".to_string()))?
2597+ } else {
2598+ m.id
2599+ };
10492600 break;
10502601 }
10512602 (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.
10532609 if !m.authored_by_j {
10542610 let Some(parent) = cur.parent_ids().first().cloned() else {
10552611 return Err((1, "nothing to undo".to_string()));
@@ -1057,23 +2613,57 @@ impl JjBackend {
10572613 target_op_id = parent;
10582614 break;
10592615 }
1060− cur = block_on(loader.load_operation(&m.id))
2616+ let undo_op = block_on(loader.load_operation(&m.id))
10612617 .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)))?
2622+ }
2623+ // cannot follow the marker: degrade to the undo itself
2624+ _ => undo_op,
2625+ };
10622626 }
10632627 (Some(m), true) => {
1064− // a redo: reverse the undo it reversed, following further
1065− // redos to the operation ultimately reapplied
1066− target_op_id = if m.authored_by_j {
1067− block_on(chain_through_redos(&loader, &m.id))?
1068− } else {
1069− m.id
2628+ // a redo: the undo it reversed is already accounted for,
2629+ // so keep looking outward from that undo's parent for an
2630+ // undo that has not been reversed yet (§7.7: the head must
2631+ // be an undo, "possibly reached through redos").
2632+ if !m.authored_by_j {
2633+ target_op_id = m.id;
2634+ break;
2635+ }
2636+ let Some(prev) = op_parent_id(&loader, &m.id)? else {
2637+ return Err((1, "nothing to redo".to_string()));
10702638 };
1071− break;
2639+ cur = block_on(loader.load_operation(&prev))
2640+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
10722641 }
10732642 }
10742643 }
1075− let target_op = block_on(loader.load_operation(&target_op_id))
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()));
2650+ }
2651+ let mut target_op = block_on(loader.load_operation(&target_op_id))
10762652 .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)
2658+ {
2659+ target_op = block_on(loader.load_operation(&m.id))
2660+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2661+ }
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);
10772667 let target_view = block_on(
10782668 loader
10792669 .op_store()
@@ -1097,56 +2687,94 @@ impl JjBackend {
10972687 },
10982688 ));
10992689 }
1100− let marker = if redo {
1101− format!("redo: {}", cur.id().hex())
1102− } else {
1103− format!("undo: {}", cur.id().hex())
1104− };
2690+ // Use jj's current marker so either CLI can continue this history.
2691+ // The older j and jj marker formats remain readable above.
2692+ let marker = format!(
2693+ "{}: restore to operation {}",
2694+ if redo { "redo" } else { "undo" },
2695+ target_op_id.hex()
2696+ );
11052697 let mut tx = base.start_transaction();
11062698 tx.repo_mut().set_view(target_view.clone());
11072699 let unpublished = block_on(tx.write(&marker))
11082700 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1109− let op_id = unpublished.operation().id().clone();
1110− let repo = block_on(unpublished.publish())
1111− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1112− *self.inner.repo.lock().unwrap() = repo.clone();
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))?;
1115− Ok(())
2701+ // the operation is published, then the restored focus checked out
2702+ // from what the directory holds (§7.7)
2703+ let wc_id = &target_view.wc_commit_ids[WorkspaceName::DEFAULT];
2704+ let wc_commit = block_on(base.store().get_commit_async(wc_id))
2705+ .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
2706+ let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
2707+ block_on(self.checkout(&wc_commit, Some(&on_disk), Some(unpublished), Some(back)))
2708+ .map_err(|c| (1, c.msg))?;
2709+ let focus_message = wc_commit.description().lines().next().unwrap_or("").trim();
2710+ Ok(format!(
2711+ "{} `{}`; restored focus @{} {}",
2712+ if redo { "Redid" } else { "Undid" },
2713+ affected_description,
2714+ wc_commit.change_id().reverse_hex(),
2715+ terminal_text(if focus_message.is_empty() { "(no description)" } else { focus_message }),
2716+ ))
2717+ }
2718+
2719+ /// Refuse, as `undo` and `redo` do before anything else (§7.7), when the
2720+ /// working directory holds changes the working-copy commit does not:
2721+ /// what a snapshot would record (§7.4), which their checkout would write
2722+ /// over. Otherwise return the tree the directory holds, as scanned,
2723+ /// which that checkout starts from: the commit's, but for the text
2724+ /// written for a conflict the state does not record, as one lost and
2725+ /// made again records none, or, where the working-copy state is stale,
2726+ /// the state's with any changes to it the commit already has, such as
2727+ /// what a checkout cut short wrote. Reserved commands never snapshot,
2728+ /// so nothing is recorded either way.
2729+ fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<MergedTree, OpenError> {
2730+ let (scanned, clean) = self.inspect_working_copy(base, true)?;
2731+ if !clean {
2732+ return Err((1, "working copy has changes not in @; run `j id` to record them or discard them. Recording creates a new operation: the next `j undo` undoes that recording, not the operation you originally wanted to undo; a new recording also clears pending redos. Use `j ops` to inspect the history".to_string()));
2733+ }
2734+ Ok(scanned)
11162735 }
11172736
1118− /// refuse if the working directory differs from the focused commit's
1119− /// files (§7.7): reserved commands never snapshot, so compare the locked
1120− /// working copy's recorded tree with the focus commit's tree
1121− fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<(), OpenError> {
1122− let dirty = (1, "working copy has changes not in @; run `j id` to record them or discard them".to_string());
1123− let wc_commit = block_on(self.wc_commit(base))?;
1124− // snapshot the working directory (without persisting anything) and
1125− // compare against the focused commit's files (§7.7)
2737+ /// Compare a scan with the recorded focus without publishing a snapshot.
2738+ /// Push only inspects: it must leave the saved working-copy state alone
2739+ /// so a later `j id` still sees every unrecorded edit.
2740+ fn inspect_working_copy(
2741+ &self,
2742+ base: &Arc<ReadonlyRepo>,
2743+ refresh_clean_state: bool,
2744+ ) -> Result<(MergedTree, bool), OpenError> {
2745+ let wc_tree = block_on(self.wc_commit(base))?.tree();
11262746 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);
11272749 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
11282750 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1129− let options = SnapshotOptions {
1130− base_ignores: GitIgnoreFile::empty(),
1131− progress: None,
1132− start_tracking_matcher: &EverythingMatcher,
1133− force_tracking_matcher: &NothingMatcher,
1134− max_new_file_size: u64::MAX,
1135− };
1136− let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
2751+ let old_tree = locked_ws.locked_wc().old_tree().clone();
2752+ let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
11372753 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1138− let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1139− // discard: finish against the current op so no state changes
1140− block_on(locked_ws.finish(base.operation().id().clone()))
1141− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1142− drop(ws_guard);
1143− if !same {
1144− return Err(dirty);
2754+ let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
2755+ let recorded = match marker_style(base.settings()) {
2756+ Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
2757+ Err(e) => Err(e),
11452758 }
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
2763+ // run (§7.7)
2764+ return Ok((scanned, false));
2765+ }
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)))?;
2771+ }
2772+ Ok((scanned, true))
11472773 }
11482774
1149− pub fn cmd_ops(&self) -> Result<(), OpenError> {
2775+ /// The operation log for `ops` (§7.7), one line per operation, newest
2776+ /// first; the caller writes it to stdout.
2777+ pub fn cmd_ops(&self) -> Result<String, OpenError> {
11502778 let base = self.head_repo()?;
11512779 let loader = self.repo_loader();
11522780 let head = base.operation().clone();
@@ -1159,8 +2787,8 @@ impl JjBackend {
11592787 let mut tag = "";
11602788 match op_kind(op.metadata()) {
11612789 OpKind::Normal => {}
1162− OpKind::Undo { .. } => tag = "undo",
1163− OpKind::Redo { .. } => tag = "redo",
2790+ OpKind::Undo => tag = "undo",
2791+ OpKind::Redo => tag = "redo",
11642792 }
11652793 if desc.is_empty() {
11662794 desc = "(no description)".to_string();
@@ -1183,8 +2811,7 @@ impl JjBackend {
11832811 None => None,
11842812 };
11852813 }
1186− print!("{}", out);
1187− Ok(())
2814+ Ok(out)
11882815 }
11892816}
11902817
@@ -1221,6 +2848,7 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
12212848 }
12222849 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
12232850 let backend = JjBackend {
2851+ replay_cache: Rc::new(ReplayCache::default()),
12242852 inner: Arc::new(JjInner {
12252853 workspace_root: cwd.clone(),
12262854 workspace: Mutex::new(workspace),
@@ -1229,23 +2857,43 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
12292857 visible: Mutex::new(None),
12302858 lock_guard: Mutex::new(None),
12312859 pending: Mutex::new(None),
2860+ folds_case: OnceLock::new(),
2861+ long_names: Mutex::new(HashMap::new()),
12322862 }),
12332863 };
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();
12342867 if had_git {
12352868 let import_options = backend.git_import_options()?;
1236− let mut tx = backend.current_repo().start_transaction();
12372869 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
12382870 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2871+ // HEAD too: it is the head the working-copy commit starts on, and a
2872+ // detached HEAD's commits are reachable from no ref
2873+ block_on(jj_lib::git::import_head(
2874+ tx.repo_mut(),
2875+ &backend.inner.workspace_name,
2876+ &cwd,
2877+ ))
2878+ .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
12392879 block_on(tx.repo_mut().rebase_descendants())
12402880 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1241− let _ = block_on(backend.publish_tx(tx, "import git refs"))?;
12422881 }
1243− create_initial_wc_commit(&backend, cfg, "init")?;
2882+ // §7.8: the current head is git's HEAD (none on an unborn branch)
2883+ let head = tx
2884+ .repo()
2885+ .view()
2886+ .git_head(&backend.inner.workspace_name)
2887+ .as_resolved()
2888+ .cloned()
2889+ .flatten();
2890+ create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
12442891 Ok(())
12452892}
12462893
12472894pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
12482895 let settings = user_settings_strict(cfg)?;
2896+ let url = &origin_url(url)?;
12492897 let dir_path = PathBuf::from(dir);
12502898 if dir_path.join(".jj").is_dir() {
12512899 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> {
12592907 } else if dir_path.exists() {
12602908 return Err((2, format!("{} already exists and is not empty", dir)));
12612909 }
1262− std::fs::create_dir_all(&dir_path)
1263− .map_err(|e| (2, format!("cannot create {}: {}", dir, e)))?;
2910+ let existed = dir_path.is_dir();
2911+ // the directory an existing DIR leads to, through a symlink DIR may be:
2912+ // a clone that fails empties it only while DIR still leads there (§7.8)
2913+ let found = if existed { dir_identity(&dir_path, true) } else { None };
2914+ // the directories this run creates, in order, each with its device and
2915+ // inode: a clone that fails removes DIR and those of its parents left
2916+ // empty, or an existing DIR's entries, and nothing else (§7.8).
2917+ // Each is made by `create_dir`, whose success proves it new. The checks
2918+ // above read DIR as written, before its missing parents exist; once
2919+ // they do, `new/..` or `new/../keep` names a directory that was there,
2920+ // so DIR's own `create_dir` failing, AlreadyExists included, refuses it
2921+ // as `git clone` does
2922+ let mut created: Vec<(&std::path::Path, Option<(u64, u64)>)> = Vec::new();
2923+ // those of them a failed clone leaves, as their paths no longer lead
2924+ // to them
2925+ let mut left: Vec<&std::path::Path> = Vec::new();
2926+ // from here until the clone has removed what it made or completed, a
2927+ // signal to stop waits, and fails a clone that has not yet made its
2928+ // working-copy commit (`unless_interrupted`, §1.3)
2929+ let _held = Deferral::start();
2930+ if !existed {
2931+ // DIR and its missing parents, DIR first ("" is the current
2932+ // directory, which exists)
2933+ let missing: Vec<&std::path::Path> = dir_path
2934+ .ancestors()
2935+ .enumerate()
2936+ .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
2937+ .map(|(_, p)| p)
2938+ .collect();
2939+ for p in missing.into_iter().rev() {
2940+ match std::fs::create_dir(p) {
2941+ Ok(()) => created.push((p, dir_identity(p, false))),
2942+ // a parent reached again through `..`
2943+ Err(e)
2944+ if e.kind() == std::io::ErrorKind::AlreadyExists
2945+ && p != dir_path
2946+ && p.is_dir() => {}
2947+ Err(e) => {
2948+ for &(p, id) in created.iter().rev() {
2949+ if !remove_created_dir(p, id, false) {
2950+ left.push(p);
2951+ }
2952+ }
2953+ let msg = format!("cannot create {}: {}", dir, e);
2954+ return Err((2, msg + &left_note(&left)));
2955+ }
2956+ }
2957+ }
2958+ }
2959+ let cloned = clone_into(cfg, &settings, url, &dir_path);
2960+ let Err((code, msg)) = cloned else {
2961+ return Ok(());
2962+ };
2963+ // like `git clone`, a clone that fails leaves nothing behind: its
2964+ // working-copy commit would claim files the checkout never wrote, and a
2965+ // later clone would refuse the directory (§7.8)
2966+ if !existed {
2967+ // DIR, the last one created, goes whole; a parent holds nothing of
2968+ // the clone's but DIR, so it goes only if left empty: what else was
2969+ // put there meanwhile, a sibling clone say, stays
2970+ for (i, &(p, id)) in created.iter().enumerate().rev() {
2971+ if !remove_created_dir(p, id, i + 1 == created.len()) {
2972+ left.push(p);
2973+ }
2974+ }
2975+ Err((code, msg + &left_note(&left)))
2976+ } else if found.is_some() && dir_identity(&dir_path, true) == found {
2977+ if let Ok(entries) = std::fs::read_dir(&dir_path) {
2978+ for entry in entries.flatten() {
2979+ let _ = match entry.file_type() {
2980+ Ok(t) if t.is_dir() => std::fs::remove_dir_all(entry.path()),
2981+ _ => std::fs::remove_file(entry.path()),
2982+ };
2983+ }
2984+ }
2985+ Err((code, msg))
2986+ } else if dir_path.symlink_metadata().is_ok() {
2987+ // a symlink put in place of DIR, or of a directory above it, would
2988+ // have `read_dir` list what that names
2989+ Err((
2990+ code,
2991+ format!(
2992+ "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
2993+ msg, dir
2994+ ),
2995+ ))
2996+ } else {
2997+ Err((code, msg))
2998+ }
2999+}
3000+
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)
3007+ } else {
3008+ std::fs::symlink_metadata(path)
3009+ };
3010+ meta.ok().filter(|m| m.is_dir()).map(|m| (m.dev(), m.ino()))
3011+}
3012+
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 {
3021+ let _ = if whole {
3022+ std::fs::remove_dir_all(path)
3023+ } else {
3024+ std::fs::remove_dir(path)
3025+ };
3026+ true
3027+ } else {
3028+ path.symlink_metadata().is_err()
3029+ }
3030+}
3031+
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(", ")
3037+ };
3038+ match left {
3039+ [] => String::new(),
3040+ [one] => format!(
3041+ "; left {}, as it is no longer the directory this clone created",
3042+ named(&[one])
3043+ ),
3044+ [init @ .., last] => format!(
3045+ "; left {} and {}, as they are no longer the directories this clone created",
3046+ named(init),
3047+ named(&[last])
3048+ ),
3049+ }
3050+}
3051+
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());
3063+ }
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());
3069+ }
3070+ let cwd = std::env::current_dir()
3071+ .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
3072+ parsed
3073+ .canonicalize(&cwd)
3074+ .map_err(|e| (2, format!("cannot resolve `{}`: {}", url, e)))?;
3075+ // `add_remote` takes UTF-8, which only the current directory's name or
3076+ // a symlink's target can break; stored as given, the path would reach
3077+ // the remote only from where it was typed
3078+ String::from_utf8(parsed.to_bstring().into())
3079+ .map_err(|_| (2, format!("cannot resolve `{}`: the path it names is not UTF-8", url)))
3080+}
3081+
3082+/// `clone`'s work once `dir_path` exists and is empty (§7.8)
3083+fn clone_into(
3084+ cfg: &Config,
3085+ settings: &UserSettings,
3086+ url: &str,
3087+ dir_path: &std::path::Path,
3088+) -> Result<(), OpenError> {
12643089 let (workspace, repo) = block_on(Workspace::init_colocated_git(
1265− &settings,
1266− &dir_path,
3090+ settings,
3091+ dir_path,
12673092 gix::hash::Kind::Sha1,
12683093 ))
12693094 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
12703095 let backend = JjBackend {
3096+ replay_cache: Rc::new(ReplayCache::default()),
12713097 inner: Arc::new(JjInner {
1272− workspace_root: dir_path.clone(),
3098+ workspace_root: dir_path.to_path_buf(),
12733099 workspace: Mutex::new(workspace),
12743100 repo: Mutex::new(repo),
12753101 workspace_name: WorkspaceName::DEFAULT.to_owned(),
12763102 visible: Mutex::new(None),
12773103 lock_guard: Mutex::new(None),
12783104 pending: Mutex::new(None),
3105+ folds_case: OnceLock::new(),
3106+ long_names: Mutex::new(HashMap::new()),
12793107 }),
12803108 };
12813109 let origin = RemoteName::new("origin");
12823110 let mut tx = backend.current_repo().start_transaction();
1283− jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
1284− .map_err(|e| (1, format!("cannot set the remote: {}", e)))?;
1285− backend.git_fetch_refs(tx.repo_mut(), origin)?;
3111+ // a signal to stop fails each step before the working-copy commit's
3112+ // (§1.3)
3113+ unless_interrupted(
3114+ jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3115+ .map_err(|e| (1, format!("cannot set the remote: {}", e))),
3116+ )?;
3117+ unless_interrupted(backend.git_fetch_refs(tx.repo_mut(), origin))?;
3118+ let default_branch = unless_interrupted(git_default_branch(
3119+ git_backend(tx.repo().store())?.git_repo_path(),
3120+ origin,
3121+ ))?;
12863122 let import_options = backend.git_import_options()?;
1287− block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1288− .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
1289− block_on(tx.repo_mut().rebase_descendants())
1290− .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
1291− let _ = block_on(backend.publish_tx(tx, &format!("clone {}", url)))?;
1292− create_initial_wc_commit(&backend, cfg, &format!("clone {}", url))?;
3123+ unless_interrupted(
3124+ block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3125+ .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
3126+ )?;
3127+ unless_interrupted(
3128+ block_on(tx.repo_mut().rebase_descendants())
3129+ .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
3130+ )?;
3131+ // §7.8: the default bookmark's target (none when the remote's HEAD names
3132+ // no branch, or a branch it does not have)
3133+ let head = default_branch.and_then(|name| {
3134+ tx.repo()
3135+ .view()
3136+ .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3137+ .target
3138+ .as_resolved()
3139+ .cloned()
3140+ .flatten()
3141+ });
3142+ // the fetch and the working-copy commit are one operation, the one undo
3143+ // never undoes (§7.7)
3144+ create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
12933145 Ok(())
12943146}
12953147
1296−/// §7.8: after init/clone, create a working-copy commit as a child of the
1297−/// current head (a bookmark target if there is one, otherwise the root
1298−/// commit) and check it out.
3148+/// §7.8: after init/clone, create a working-copy commit as a child of
3149+/// `head` (the root commit when there is none), record it in `tx` with
3150+/// what the command has imported, and check it out. The commit holds its
3151+/// parent's files less any a checkout cannot create (`checkout_tree`), so
3152+/// it is empty unless the parent has one. `adopt` says the working
3153+/// directory already holds a checkout of the parent (init over an existing
3154+/// git repository): the working copy is then reset to the commit instead of
3155+/// written, so no file is touched and what the directory holds beyond the
3156+/// parent is the next snapshot's change. `back` is what a checkout that
3157+/// fails says (`JjBackend::checkout`): none for a clone, which then removes
3158+/// what it made.
12993159fn create_initial_wc_commit(
13003160 backend: &JjBackend,
13013161 cfg: &Config,
3162+ mut tx: jj_lib::transaction::Transaction,
13023163 description: &str,
3164+ head: Option<CommitId>,
3165+ adopt: bool,
3166+ back: Option<&str>,
13033167) -> Result<(), OpenError> {
1304− let base = backend.current_repo();
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()) {
1309− parent = id;
1310− break 'outer;
1311− }
1312− }
1313− if parent == *store.root_commit_id() {
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()) {
1316− parent = id;
1317− break;
1318− }
1319− }
1320− }
3168+ let store = tx.base_repo().store().clone();
3169+ let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
13213170 // jj's own init already created a working-copy commit; if it is already
13223171 // a child of the target parent, keep it and just check it out
13233172 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()) {
13253175 if existing.parent_ids().first() == Some(&parent) {
1326− let op_id = base.operation().id().clone();
1327− block_on(backend.checkout(&existing, op_id)).map_err(|c| (1, c.msg))?;
3176+ // jj's commit is an empty child of the root; record only what
3177+ // was imported, if anything
3178+ let op = if tx.repo().has_changes() {
3179+ Some(
3180+ block_on(tx.write(truncate_chars(description, 200)))
3181+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
3182+ )
3183+ } else {
3184+ None
3185+ };
3186+ block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
13283187 return Ok(());
13293188 }
13303189 if existing.id() != store.root_commit_id() {
13313190 to_abandon = Some(existing.id().clone());
13323191 }
13333192 }
3193+ let parent_tree = block_on(store.get_commit_async(&parent))
3194+ .map_err(|e| (1, format!("cannot read the parent commit: {}", e)))?
3195+ .tree();
3196+ let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
13343197 let user_sig = backend
13353198 .user_signature(cfg)
13363199 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1337− let mut tx = base.start_transaction();
13383200 if let Some(old_wc) = &to_abandon {
13393201 tx.repo_mut().record_abandoned_commit_with_parents(
13403202 old_wc.clone(),
@@ -1343,7 +3205,7 @@ fn create_initial_wc_commit(
13433205 }
13443206 let wc = block_on(
13453207 tx.repo_mut()
1346− .new_commit(vec![parent], store.empty_merged_tree())
3208+ .new_commit(vec![parent], wc_tree)
13473209 .set_author(user_sig.clone())
13483210 .set_committer(user_sig)
13493211 .write(),
@@ -1356,19 +3218,49 @@ fn create_initial_wc_commit(
13563218 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
13573219 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
13583220 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1359− let op_id = unpublished.operation().id().clone();
1360− let repo = block_on(unpublished.publish())
1361− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1362− *backend.inner.repo.lock().unwrap() = repo;
1363− block_on(backend.checkout(&wc, op_id)).map_err(|c| (1, c.msg))?;
3221+ // an adopted directory is reset to the commit, not checked out over:
3222+ // checking out from the empty tree jj's init recorded would recreate
3223+ // the files the user deleted there. Anything else is written, once the
3224+ // operation is published (§7.5 step 7).
3225+ let on_disk = if adopt { Some(wc.tree()) } else { None };
3226+ block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
13643227 Ok(())
13653228}
13663229
3230+/// `tree` without the files at a path a checkout cannot create (§7.5 step
3231+/// 1), which a git branch can hold: a `.jj` directory committed by mistake,
3232+/// a name the workspace's filesystem does not hold. jj's checkout stops at
3233+/// the first such path, failing the whole clone, so the working-copy commit
3234+/// init or clone starts on leaves them out, and their removal is its change
3235+/// (§7.8).
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
3239+ .entries()
3240+ .map(|(path, _)| path)
3241+ .filter(|path| {
3242+ path.components().any(|c| {
3243+ crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3244+ })
3245+ })
3246+ .collect();
3247+ if refused.is_empty() {
3248+ return Ok(tree);
3249+ }
3250+ let mut builder = MergedTreeBuilder::new(tree);
3251+ for path in refused {
3252+ builder.set_or_remove(path, Merge::absent());
3253+ }
3254+ builder
3255+ .write_tree()
3256+ .await
3257+ .map_err(|e| (1, format!("cannot write a tree: {}", e)))
3258+}
3259+
13673260/// user settings for init/clone: a missing or malformed `user` is a
13683261/// configuration error (exit 3) (§7.9)
13693262fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1370− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
1371− .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3263+ let (name, email) = validated_identity(cfg)?;
13723264 let mut config = StackedConfig::with_defaults();
13733265 let text = format!(
13743266 "[user]\nname = {}\nemail = {}\n",
@@ -1382,6 +3274,39 @@ fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
13823274 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
13833275}
13843276
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)))
3283+}
3284+
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() }
3288+ }).collect()
3289+}
3290+
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)?;
3295+ Ok((name, email))
3296+}
3297+
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")
3301+ {
3302+ return Err((3, format!(
3303+ "config.j: user identity is not configured ({} <{}>); set `user.name` and `user.email` in the `user` record before creating or recording commits",
3304+ terminal_text(name), terminal_text(email),
3305+ )));
3306+ }
3307+ Ok(())
3308+}
3309+
13853310// ----------------------------------------------------------------------
13863311// reserved-command helpers
13873312// ----------------------------------------------------------------------
@@ -1445,41 +3370,72 @@ fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Resul
14453370 if out.status.success() {
14463371 Ok(())
14473372 } else {
1448− Err((
1449− 1,
1450− format!(
1451− "fetch failed: {}",
1452− String::from_utf8_lossy(&out.stderr).trim()
1453− ),
1454− ))
3373+ Err((1, format!("fetch failed: {}", git_failure(&out))))
3374+ }
3375+}
3376+
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;
3381+ let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
3382+ match (said.is_empty(), out.status.signal()) {
3383+ (false, _) => said,
3384+ (true, Some(sig)) => format!("git was stopped by signal {}", sig),
3385+ (true, None) => format!("git exited with status {}", out.status.code().unwrap_or(-1)),
3386+ }
3387+}
3388+
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)
3398+ .arg("--git-dir")
3399+ .arg(git_dir)
3400+ .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401+ .stdin(std::process::Stdio::null())
3402+ .output()
3403+ .map_err(|e| (1, format!("cannot run git ls-remote: {}", e)))?;
3404+ if !out.status.success() {
3405+ return Err((1, format!("cannot read the remote's default branch: {}", git_failure(&out))));
14553406 }
3407+ // `ref: refs/heads/<branch>\tHEAD`
3408+ Ok(String::from_utf8_lossy(&out.stdout).lines().find_map(|line| {
3409+ let branch = line.strip_prefix("ref: refs/heads/")?.strip_suffix("\tHEAD")?;
3410+ Some(branch.to_string())
3411+ }))
14563412}
14573413
14583414enum OpKind {
14593415 Normal,
1460− /// the operation this undo discarded the view of
1461− Undo { #[allow(dead_code)] undid: OperationId },
1462− /// the undo this redo reversed
1463− Redo { redid: OperationId },
3416+ Undo,
3417+ Redo,
14643418}
14653419
14663420/// a marker parsed from an operation description (§7.7 kinds)
14673421struct OpMarker {
14683422 kind: &'static str,
14693423 id: OperationId,
1470− /// whether the description is j's own exact marker (`undo: <id>` /
1471− /// `redo: <id>`); only those are reliably loadable for the walk
3424+ /// jj 0.45 names the operation whose view was restored, rather than
3425+ /// the operation undone or the undo reversed.
3426+ restores_view: bool,
3427+ /// Whether a legacy marker is j's exact `undo: <id>` / `redo: <id>`
3428+ /// format, whose discarded-operation links the walk can follow.
14723429 authored_by_j: bool,
14733430}
14743431
1475−/// §7.7 kinds: `j` marks undo/redo operations as `undo: <opid>` /
1476−/// `redo: <opid>` in the description (0.45's Transaction cannot write
1477−/// metadata tags); jj's own `undo operation <id>` / `redo operation <id>`
1478−/// markers are treated the same way.
3432+/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
3433+/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
3434+/// `redo operation <id>` descriptions remain readable too.
14793435fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
14803436 match op_marker(meta) {
1481− Some(OpMarker { kind: "undo", id, .. }) => OpKind::Undo { undid: id },
1482− Some(OpMarker { kind: "redo", id, .. }) => OpKind::Redo { redid: id },
3437+ Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
3438+ Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
14833439 _ => OpKind::Normal,
14843440 }
14853441}
@@ -1487,20 +3443,33 @@ fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
14873443fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
14883444 let desc = meta.description.trim();
14893445 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 });
3449+ }
3450+ }
14903451 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
14913452 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 });
14933454 }
14943455 }
14953456 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
14963457 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 });
14983459 }
14993460 }
15003461 }
15013462 None
15023463}
15033464
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 ")
3471+}
3472+
15043473/// a full-length (or prefix) hex operation id
15053474fn parse_op_id(s: &str) -> Option<OperationId> {
15063475 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
@@ -1526,21 +3495,39 @@ fn op_parent_id(
15263495 Ok(op.parent_ids().first().cloned())
15273496}
15283497
1529−/// follow a chain of redos to the operation that was ultimately reapplied
1530−async fn chain_through_redos(
3498+/// A restore operation represents an earlier view. Follow that view to a
3499+/// normal operation for useful confirmation text without changing undo's
3500+/// navigation or its jj-compatible metadata.
3501+fn restored_operation_description(
15313502 loader: &jj_lib::repo::RepoLoader,
1532− first: &OperationId,
1533−) -> Result<OperationId, OpenError> {
1534− let mut id = first.clone();
1535− loop {
1536− let op = loader
1537− .load_operation(&id)
1538− .await
1539− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1540− match op_kind(op.metadata()) {
1541− OpKind::Redo { redid } => id = redid,
1542− _ => return Ok(id),
1543− }
3503+ op: &jj_lib::operation::Operation,
3504+) -> String {
3505+ let mut represented = op.clone();
3506+ let mut seen = BTreeSet::new();
3507+ while seen.insert(represented.id().clone()) {
3508+ let Some(marker) = op_marker(represented.metadata()) else { break };
3509+ let target = if marker.restores_view {
3510+ Some(marker.id)
3511+ } else if marker.authored_by_j {
3512+ // A legacy undo names what it discarded; a legacy redo names
3513+ // the undo it reversed. Either restored its target's parent.
3514+ op_parent_id(loader, &marker.id).ok().flatten()
3515+ } else {
3516+ // Unknown legacy jj history is best identified by its actual
3517+ // description instead of guessing which operation it applied.
3518+ None
3519+ };
3520+ let Some(target) = target else { break };
3521+ // Missing historical metadata must not prevent an otherwise valid
3522+ // recovery operation merely because its summary cannot be expanded.
3523+ let Ok(target_op) = block_on(loader.load_operation(&target)) else { break };
3524+ represented = target_op;
3525+ }
3526+ let description = represented.metadata().description.lines().next().unwrap_or("").trim();
3527+ if description.is_empty() {
3528+ format!("operation {}", truncate_chars(&represented.id().hex(), 12))
3529+ } else {
3530+ terminal_text(description)
15443531 }
15453532}
15463533
@@ -1610,6 +3597,11 @@ fn parse_push_records(
16103597 let rec = vis.commits.get(&change_id).ok_or_else(|| {
16113598 (1, format!("push: `@{}` is not a visible commit", change_id))
16123599 })?;
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()));
3604+ }
16133605 out.push((name, Some(rec.commit.id().clone())));
16143606 }
16153607 None => {
@@ -1629,68 +3621,190 @@ fn parse_push_records(
16293621 Ok(out)
16303622}
16313623
1632−/// §7.6: refuse to send commits with unresolved files or empty descriptions,
1633−/// and refuse to move/delete a bookmark whose current target is immutable
1634−/// away from a descendant of that target
3624+/// §7.6: refuse to move a bookmark whose current target is immutable to a
3625+/// commit that does not descend from that target, or to delete it while no
3626+/// bookmark left on `origin` after the push reaches that target; and refuse
3627+/// to send commits with unresolved files or empty descriptions. Returns
3628+/// whether such a delete is allowed only because a record's new target
3629+/// reaches it, so the deletes must wait for the updates (push_to_origin),
3630+/// and refuses a push whose deletes would wait for a bookmark git cannot
3631+/// create while one of them is still there
16353632fn check_push_records(
16363633 records: &[(String, Option<CommitId>)],
1637− vis: &VisibleRepo,
16383634 repo: &Arc<ReadonlyRepo>,
1639−) -> Result<(), OpenError> {
3635+ vis: &VisibleRepo,
3636+ immutable: &BTreeSet<String>,
3637+) -> Result<bool, OpenError> {
16403638 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();
16413648 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);
1649− }
1650− }
1651− if let Some(current) = repo
3649+ let Some(current) = repo
16523650 .view()
1653− .get_remote_bookmark(RefName::new(name).to_remote_symbol(RemoteName::new("origin")))
3651+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
16543652 .target
16553653 .as_resolved()
16563654 .cloned()
16573655 .unwrap_or(None)
1658− {
1659− let commit = block_on(store.get_commit_async(¤t))
1660− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1661− let change_id = commit.change_id().reverse_hex();
1662− if vis.commits.contains_key(&change_id) {
1663− seeds.push(change_id);
1664− }
1665− }
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");
1669− for (_path, value) in rec.commit.tree().entries() {
1670− let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
1671− if !value.is_resolved() {
3656+ else {
3657+ continue;
3658+ };
3659+ let change_id = block_on(store.get_commit_async(¤t))
3660+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
3661+ .change_id()
3662+ .reverse_hex();
3663+ // the immutable set holds visible commits: a target rewritten here
3664+ // since the last fetch or push is hidden, and not in it although the
3665+ // visible rewrite carries its change id (and its label)
3666+ let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == ¤t);
3667+ if !visible || !immutable.contains(&change_id) {
3668+ continue;
3669+ }
3670+ match target {
3671+ None => {
3672+ // a delete loses no history while another bookmark still
3673+ // reaches the commit: a merged feature, a second name. When
3674+ // only a bookmark this push sets reaches it (`rename`), the
3675+ // remote may reject that update and accept the delete, so
3676+ // the delete waits for the update to be accepted
3677+ let unnamed = unnamed.get_or_insert_with(|| {
3678+ let named: BTreeSet<&str> = records.iter().map(|(n, _)| n.as_str()).collect();
3679+ repo.view()
3680+ .remote_bookmarks(origin)
3681+ .filter(|(n, _)| !named.contains(n.as_str()))
3682+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3683+ .cloned()
3684+ .collect()
3685+ });
3686+ let reached = |heads: &[CommitId]| {
3687+ ResolvedRevsetExpression::commits(vec![current.clone()])
3688+ .intersection(&ResolvedRevsetExpression::commits(heads.to_vec()).ancestors())
3689+ .evaluate(repo.as_ref())
3690+ .and_then(|r| r.is_empty())
3691+ .map(|empty| !empty)
3692+ .map_err(|e| (2, format!("cannot read the index: {}", e)))
3693+ };
3694+ if !reached(unnamed)? {
3695+ if !reached(&new_targets)? {
3696+ return Err((
3697+ 1,
3698+ format!(
3699+ "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3700+ name, change_id
3701+ ),
3702+ ));
3703+ }
3704+ waiting.push(name);
3705+ }
3706+ }
3707+ Some(new) => {
3708+ let descends = block_on(repo.index().is_ancestor(¤t, new))
3709+ .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3710+ if !descends {
3711+ return Err((
3712+ 1,
3713+ format!(
3714+ "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
3715+ name, change_id
3716+ ),
3717+ ));
3718+ }
3719+ }
3720+ }
3721+ }
3722+ // git cannot hold a bookmark and one under its name at once (`master`
3723+ // and `master/legacy`), so while the deletes wait, a first push that
3724+ // creates a name nesting with a deleted one is rejected and the deletes
3725+ // are never sent. With a bookmark no record names on the waiting
3726+ // delete's commit, nothing waits and it is one git push (§7.6)
3727+ let deletes_wait = !waiting.is_empty();
3728+ if deletes_wait {
3729+ let nests = |a: &str, b: &str| b.strip_prefix(a).is_some_and(|rest| rest.starts_with('/'));
3730+ for (deleted, _) in records.iter().filter(|(_, t)| t.is_none()) {
3731+ for (set, _) in records.iter().filter(|(_, t)| t.is_some()) {
3732+ if nests(deleted, set) || nests(set, deleted) {
3733+ let keep = if waiting.contains(&deleted.as_str()) { deleted.as_str() } else { waiting[0] };
3734+ let literal = crate::show::text_literal(keep);
3735+ // the temporary label: the first of `tmp`, `tmp-1`, ...
3736+ // that is no bookmark, here or on origin, no record
3737+ // names, and nests with none of those, so pushing and
3738+ // deleting it moves none of the user's bookmarks, and
3739+ // this push, which names nothing nesting with it, no
3740+ // longer waits on `keep`'s commit
3741+ let taken: Vec<&str> = repo
3742+ .view()
3743+ .bookmarks()
3744+ .map(|(n, _)| n.as_str())
3745+ .chain(records.iter().map(|(n, _)| n.as_str()))
3746+ .collect();
3747+ let tmp = std::iter::once("tmp".to_string())
3748+ .chain((1..).map(|i| format!("tmp-{i}")))
3749+ .find(|t| !taken.iter().any(|n| n == t || nests(n, t) || nests(t, n)))
3750+ .expect("finitely many names are taken");
3751+ let tmp = crate::show::text_literal(&tmp);
16723752 return Err((
16733753 1,
16743754 format!(
1675− "push: commit `@{}` has unresolved files",
1676− rec.change_id
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})`"
16773756 ),
16783757 ));
16793758 }
16803759 }
1681− if rec.commit.description().trim().is_empty() && rec.commit.id() != store.root_commit_id() {
1682− return Err((
1683− 1,
1684− format!("push: commit `@{}` has an empty description", rec.change_id),
1685− ));
3760+ }
3761+ }
3762+ // the commits that would be sent: ancestors of the new targets that no
3763+ // bookmark on the remote already reaches; a delete sends none
3764+ if new_targets.is_empty() {
3765+ return Ok(deletes_wait);
3766+ }
3767+ let on_remote: Vec<CommitId> = repo
3768+ .view()
3769+ .remote_bookmarks(origin)
3770+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3771+ .cloned()
3772+ .collect();
3773+ let sent = ResolvedRevsetExpression::commits(on_remote)
3774+ .range(&ResolvedRevsetExpression::commits(new_targets))
3775+ .evaluate(repo.as_ref())
3776+ .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3777+ let sent: Vec<Commit> = block_on(sent.stream().commits(&store).try_collect())
3778+ .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3779+ for commit in &sent {
3780+ let change_id = commit.change_id().reverse_hex();
3781+ let mut unresolved_paths = Vec::new();
3782+ for (path, value) in commit.tree().entries() {
3783+ let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3784+ if !value.is_resolved() {
3785+ unresolved_paths.push(path.as_internal_file_string().to_string());
3786+ }
3787+ }
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));
16863794 }
3795+ return Err((1, format!(
3796+ "push: commit `@{change_id}` has unresolved files: {paths}; run `j conflicts` to inspect conflicts, then `j by @{change_id}` to edit this commit. Resolve the listed files, run `j id` to record the resolution, and check `j conflicts` before pushing again"
3797+ )));
3798+ }
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)));
16873801 }
16883802 }
1689− Ok(())
3803+ Ok(deletes_wait)
16903804}
16913805
16923806/// 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)
16943808fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
16953809 let mut seen = BTreeSet::new();
16963810 let mut stack: Vec<String> = seeds.to_vec();
@@ -1711,23 +3825,11 @@ fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<Stri
17113825
17123826/// git-check-ref-format --branch rules
17133827fn is_valid_git_branch_name(name: &str) -> bool {
1714− if name.is_empty()
1715− || 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("@{")
1723− || name == "@"
1724− {
3828+ if name.starts_with('-') {
17253829 return false;
17263830 }
1727− name.chars().all(|c| {
1728− !(c.is_ascii_control()
1729− || matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
1730− })
3831+ let full_name = format!("refs/heads/{name}");
3832+ gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
17313833}
17323834
17333835// ----------------------------------------------------------------------
@@ -1735,6 +3837,12 @@ fn is_valid_git_branch_name(name: &str) -> bool {
17353837// ----------------------------------------------------------------------
17363838
17373839impl 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))
3844+ }
3845+
17383846 fn visible_ids(&self) -> Vec<String> {
17393847 match self.inner.visible.lock().unwrap().as_ref() {
17403848 Some(v) => v.commits.keys().cloned().collect(),
@@ -1763,28 +3871,99 @@ impl Backend for JjBackend {
17633871 .unwrap_or(false)
17643872 }
17653873
3874+ fn has_conflict(&self, id: &str) -> Option<bool> {
3875+ let vis = self.inner.visible.lock().unwrap().clone();
3876+ // O(1): a commit is conflicted iff its tree id is an unresolved merge
3877+ vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
3878+ }
3879+
3880+ fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
3881+ let vis = self.inner.visible.lock().unwrap().clone();
3882+ // O(1): empty iff the tree id equals the parent's tree id
3883+ vis.and_then(|v| {
3884+ let rec = v.commits.get(id)?;
3885+ let parent = v.commits.get(parent)?;
3886+ Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3887+ })
3888+ }
3889+
3890+ fn folds_case(&self) -> bool {
3891+ // jj's checkout refuses a name whose file identity is that of
3892+ // `.git` or `.jj` in the same directory (local_working_copy.rs), so
3893+ // where `.JJ` is the workspace's own `.jj` it refuses `.GIT` too.
3894+ // Where each spelling of a name gets its own inode number
3895+ // (exfat-fuse) the identities differ and the checkout writes
3896+ // `.GIT/x` into `.git`, so folding is told by how names resolve:
3897+ // `.JJ` resolves, but the directory does not list it under that
3898+ // spelling. A directory that cannot be listed is taken to fold
3899+ // case, the answer that refuses more.
3900+ *self.inner.folds_case.get_or_init(|| {
3901+ use jj_lib::file_util::FileIdentity;
3902+ let root = &self.inner.workspace_root;
3903+ let Ok(upper) = FileIdentity::from_symlink_path(root.join(".JJ")) else {
3904+ return false;
3905+ };
3906+ FileIdentity::from_symlink_path(root.join(".jj")).is_ok_and(|lower| lower == upper)
3907+ || std::fs::read_dir(root).map_or(true, |mut entries| {
3908+ !entries.any(|e| e.is_ok_and(|e| e.file_name() == ".JJ"))
3909+ })
3910+ })
3911+ }
3912+
3913+ fn name_fits(&self, name: &str) -> bool {
3914+ if name.len() <= crate::repo::NAME_MAX {
3915+ return true;
3916+ }
3917+ // a component, never a path: one with `/` would be created elsewhere
3918+ if name.contains(['/', '\0']) {
3919+ return false;
3920+ }
3921+ // no call tells whether the limit counts bytes (ext4) or UTF-16
3922+ // units or characters (exFAT, NTFS, APFS), so the name is tried:
3923+ // created in a directory of its own under `.jj`, which is then
3924+ // removed, once per name and run
3925+ let mut known = self.inner.long_names.lock().unwrap();
3926+ if let Some(fits) = known.get(name) {
3927+ return *fits;
3928+ }
3929+ let fits = match tempfile::Builder::new()
3930+ .prefix("name-")
3931+ .tempdir_in(self.inner.workspace_root.join(".jj"))
3932+ {
3933+ Ok(dir) => {
3934+ let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3935+ drop(dir);
3936+ fits
3937+ }
3938+ Err(_) => false,
3939+ };
3940+ known.insert(name.to_string(), fits);
3941+ fits
3942+ }
3943+
17663944 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
17673945 let vis = self.inner.visible.lock().unwrap().clone();
17683946 let Some(vis) = vis else {
17693947 return BTreeSet::new();
17703948 };
1771− let mut seen = BTreeSet::new();
1772− let mut stack: Vec<String> = ids.iter().cloned().collect();
1773− while let Some(id) = stack.pop() {
1774− if seen.insert(id.clone()) {
1775− if let Some(rec) = vis.commits.get(&id) {
1776− if !rec.first_parent.is_empty() {
1777− stack.push(rec.first_parent.clone());
1778− }
1779− }
1780− }
1781− }
1782− seen
3949+ // every jj parent, not just the first: rewriting a commit reached
3950+ // only through a merge's second parent rewrites the merge too
3951+ // (§7.5 step 3)
3952+ let seeds: Vec<String> = ids.iter().cloned().collect();
3953+ ancestors_of_change_ids(&vis, &seeds)
17833954 }
17843955
17853956 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)
3962+ }
3963+
3964+ fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
17863965 let store = self.current_repo().store().clone();
1787− block_on(jj_replay(&store, onto, from, to))
3966+ block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
17883967 }
17893968}
17903969
@@ -1792,30 +3971,197 @@ impl Backend for JjBackend {
17923971// jj three-way tree merge for `replay` (§7.3)
17933972// ----------------------------------------------------------------------
17943973
3974+/// Replay sees the same lists repeatedly: a parent's old and new files
3975+/// become each child's bases. Retain their validated trees by list identity,
3976+/// including its offset, without keeping discarded snapshots alive.
3977+#[derive(Default)]
3978+struct ReplayCache {
3979+ trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980+ empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981+ next_sweep: Cell<usize>,
3982+ blobs: BlobCache,
3983+ entries: EntryCache,
3984+}
3985+
3986+struct ReplayTree {
3987+ storage: Weak<Vec<Value>>,
3988+ tree: MergedTree,
3989+ /// A list read from a resolved tree, already in canonical path order.
3990+ canonical: bool,
3991+}
3992+
3993+impl ReplayCache {
3994+ fn remember(&self, list: &ListVal, tree: MergedTree, canonical: bool) {
3995+ let mut trees = self.trees.borrow_mut();
3996+ trees.insert(list.identity(), ReplayTree {
3997+ storage: list.weak_storage(),
3998+ tree,
3999+ canonical,
4000+ });
4001+ if trees.len() >= self.next_sweep.get().max(256) {
4002+ trees.retain(|_, t| t.storage.strong_count() != 0);
4003+ self.next_sweep.set((trees.len() * 2).max(256));
4004+ }
4005+ }
4006+
4007+ async fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<(MergedTree, bool), Crash> {
4008+ let Value::List(list) = files else {
4009+ files.as_list()?;
4010+ unreachable!();
4011+ };
4012+ if let Some(cached) = self.trees.borrow().get(&list.identity()) {
4013+ // The weak reference keeps its allocation reserved, so another
4014+ // list cannot reuse this address while the key is cached.
4015+ if cached.storage.strong_count() != 0 {
4016+ return Ok((cached.tree.clone(), cached.canonical));
4017+ }
4018+ }
4019+ // Validate every new list before caching it, even when from == to:
4020+ // an unchanged change must still reject malformed snapshots.
4021+ let tree = build_tree(store, files).await?;
4022+ self.remember(list, tree.clone(), false);
4023+ Ok((tree, false))
4024+ }
4025+
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);
4031+ }
4032+ Ok(Value::List(list))
4033+ }
4034+
4035+ /// Flattening a Git tree omits empty directories. Reusing such a tree
4036+ /// would let a replay conflict with a directory absent from its input
4037+ /// snapshot. Inspect directories once per tree, without rebuilding files.
4038+ async fn has_empty_subtrees(&self, tree: &MergedTree) -> Result<bool, Crash> {
4039+ let Some(root) = tree.tree_ids().as_resolved() else { return Ok(true) };
4040+ if let Some(found) = self.empty_subtrees.borrow().get(root) {
4041+ return Ok(*found);
4042+ }
4043+ let store = tree.store();
4044+ let mut pending = vec![(RepoPathBuf::root(), root.clone())];
4045+ let mut visited = BTreeSet::new();
4046+ while let Some((dir, id)) = pending.pop() {
4047+ if !visited.insert(id.clone()) {
4048+ continue;
4049+ }
4050+ let known = self.empty_subtrees.borrow().get(&id).copied();
4051+ if let Some(found) = known {
4052+ if found {
4053+ self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4054+ return Ok(true);
4055+ }
4056+ continue;
4057+ }
4058+ let loaded = store.get_tree(dir.clone(), &id).await
4059+ .map_err(|e| Crash::new(format!("cannot read a tree: {}", e)))?;
4060+ for entry in loaded.entries_non_recursive() {
4061+ if let TreeValue::Tree(child) = entry.value() {
4062+ if child == store.empty_tree_id() {
4063+ self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4064+ return Ok(true);
4065+ }
4066+ pending.push((dir.join(entry.name()), child.clone()));
4067+ }
4068+ }
4069+ }
4070+ // Every subtree visited was nonempty all the way down. Sharing this
4071+ // answer also avoids rescanning unchanged directories of later trees.
4072+ self.empty_subtrees.borrow_mut().extend(visited.into_iter().map(|id| (id, false)));
4073+ Ok(false)
4074+ }
4075+}
4076+
17954077async fn jj_replay(
17964078 store: &Arc<Store>,
1797− onto: &[Value],
1798− from: &[Value],
1799− to: &[Value],
1800−) -> Result<Vec<Value>, Crash> {
1801− let onto_tree = build_tree(store, &Value::list(onto.to_vec())).await?;
1802− let from_tree = build_tree(store, &Value::list(from.to_vec())).await?;
1803− let to_tree = build_tree(store, &Value::list(to.to_vec())).await?;
1804− let merge = Merge::from_removes_adds(
1805− vec![from_tree.tree_ids().as_resolved().cloned().unwrap()],
1806− vec![
1807− onto_tree.tree_ids().as_resolved().cloned().unwrap(),
1808− to_tree.tree_ids().as_resolved().cloned().unwrap(),
1809− ],
1810− );
1811− // tree_merge::merge_trees resolves file contents line-level and keeps
1812− // unresolvable paths as conflicts (tree_merge.rs:78)
1813− let merged = jj_lib::tree_merge::merge_trees(store, merge)
1814− .await
1815− .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4079+ cache: &ReplayCache,
4080+ onto: &Value,
4081+ from: &Value,
4082+ to: &Value,
4083+) -> Result<Value, Crash> {
4084+ let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
4085+ let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
4086+ let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
4087+ // any of the three may itself be conflicted (a snapshot with unresolved
4088+ // blobs, §7.3), so merge the trees the way jj's rebase does:
4089+ // MergedTree::merge flattens conflicted inputs into one merge, simplifies
4090+ // it, and resolves file contents line-level with tree_merge::merge_trees,
4091+ // keeping unresolvable paths as conflicts. Empty labels leave the result
4092+ // unlabeled; the labels would not survive tree_to_files anyway.
4093+ let merged = MergedTree::merge(Merge::from_removes_adds(
4094+ vec![(from_tree.clone(), String::new())],
4095+ vec![(onto_tree.clone(), String::new()), (to_tree.clone(), String::new())],
4096+ ))
4097+ .await
4098+ .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4099+ refuse_file_directory_clash(&merged, &onto_tree, &from_tree, &to_tree).await?;
4100+ // Reusing a resolved tree's list preserves replay's sorted output. A
4101+ // constructed list may have another order, and conflicts may simplify
4102+ // on reading, so neither can take this shortcut.
4103+ for (files, tree, canonical) in [
4104+ (onto, &onto_tree, onto_canonical),
4105+ (to, &to_tree, to_canonical),
4106+ (from, &from_tree, from_canonical),
4107+ ] {
4108+ if canonical && merged.tree_ids() == tree.tree_ids() {
4109+ return Ok(files.clone());
4110+ }
4111+ }
18164112 // conflicts stay as unresolved blobs (§7.3)
1817− let merged = MergedTree::new(store.clone(), merged, jj_lib::conflict_labels::ConflictLabels::unlabeled());
1818− tree_to_files(store, &merged, &BlobCache::default(), &EntryCache::default()).await
4113+ cache.files_of(store, &merged).await
4114+}
4115+
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,
4125+ onto: &MergedTree,
4126+ from: &MergedTree,
4127+ to: &MergedTree,
4128+) -> Result<(), Crash> {
4129+ let value_at = async |tree: &MergedTree, path: &RepoPath| {
4130+ tree.path_value(path).await.map_err(|e| Crash::new(format!("replay: {}", e)))
4131+ };
4132+ // walks only the unresolved part of the tree: nothing when it is resolved
4133+ for (path, value) in merged.conflicts() {
4134+ let value = value.map_err(|e| Crash::new(format!("replay: {}", e)))?;
4135+ // sides that cancel are no sides
4136+ let value = value.simplify();
4137+ let has = |dir: bool| {
4138+ value
4139+ .adds()
4140+ .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4141+ };
4142+ if !has(false) || !has(true) {
4143+ continue;
4144+ }
4145+ let held = [
4146+ value_at(onto, &path).await?,
4147+ value_at(from, &path).await?,
4148+ value_at(to, &path).await?,
4149+ ];
4150+ // an input's conflict at the path, as opposed to one inside the
4151+ // directory there, which leaves the path a directory in the input
4152+ if held.iter().any(|h| !h.is_resolved() && !h.is_tree()) {
4153+ continue;
4154+ }
4155+ let side = |dir: &MergedTreeValueT| {
4156+ dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
4157+ };
4158+ if side(&held[0]) || side(&held[2]) {
4159+ let comps: Vec<String> =
4160+ path.components().map(|c| c.as_internal_str().to_string()).collect();
4161+ return Err(crate::domain::file_directory_clash(&comps));
4162+ }
4163+ }
4164+ Ok(())
18194165}
18204166
18214167// ----------------------------------------------------------------------
@@ -1836,6 +4182,107 @@ impl ConflictTreeValue {
18364182 }
18374183}
18384184
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>>,
4198+}
4199+
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);
4209+ }
4210+ }
4211+ }
4212+ }
4213+ Ok(LoadedTrees { visible, lists })
4214+ }
4215+
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)))
4223+ }
4224+
4225+ fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4226+ let loaded = match files {
4227+ Value::Thunk(t) => Some(t),
4228+ Value::List(list) => self.lists.get(&list.identity()),
4229+ _ => None,
4230+ };
4231+ if let Some(t) = loaded {
4232+ let stored = t.origin().zip(self.visible).and_then(|(id, vis)| vis.commits.get(id));
4233+ if let Some(tree) = stored.map(|rec| rec.commit.tree()) {
4234+ if t.tree() == Some(tree_name(&tree).as_str()) {
4235+ return Ok(tree);
4236+ }
4237+ }
4238+ }
4239+ block_on(build_tree(store, &files.forced()?))
4240+ }
4241+}
4242+
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,
4264+ }
4265+ }
4266+ let Ok(parents_tree) = jj_lib::rewrite::merge_commit_trees(repo, &commits).await else {
4267+ return tree;
4268+ };
4269+ // the same terms: the parents' tree, with jj's labels for them
4270+ if parents_tree.tree_ids() == tree.tree_ids() {
4271+ return parents_tree;
4272+ }
4273+ if parents_tree.tree_ids().is_resolved() {
4274+ return tree;
4275+ }
4276+ let mut diff = parents_tree.diff_stream(&tree, &EverythingMatcher);
4277+ while let Some(entry) = diff.next().await {
4278+ match entry.values {
4279+ Ok(Diff { before, after }) if value_as_read(&before) == value_as_read(&after) => {}
4280+ _ => return tree,
4281+ }
4282+ }
4283+ parents_tree
4284+}
4285+
18394286async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
18404287 let entries = files
18414288 .as_list()
@@ -1895,19 +4342,41 @@ async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<Merge
18954342 return Err(Crash::new("persistence: malformed conflict blob"));
18964343 }
18974344 // jj order: [add, (remove, add)*] → Merge with interleaved adds
1898− // 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
18994348 let mut values: Vec<Option<TreeValue>> = Vec::new();
19004349 for side in sides {
1901− let mut slice: &[u8] = side.as_ref();
1902− let id = store
1903− .write_file(&RepoPathBuf::root(), &mut slice)
1904− .await
1905− .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
1906− values.push(Some(TreeValue::File {
1907− id,
1908− executable: b.kind == BlobKind::Executable,
1909− copy_id: CopyId::placeholder(),
1910− }));
4350+ let Some(side) = side else {
4351+ values.push(None);
4352+ continue;
4353+ };
4354+ let value = if let Some(hex) = &side.tree {
4355+ TreeValue::Tree(
4356+ TreeId::try_from_hex(hex)
4357+ .ok_or_else(|| Crash::new("persistence: bad tree id"))?,
4358+ )
4359+ } else if side.kind == BlobKind::Symlink {
4360+ let target = std::str::from_utf8(&side.bytes)
4361+ .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4362+ let id = store
4363+ .write_symlink(&RepoPathBuf::root(), target)
4364+ .await
4365+ .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366+ TreeValue::Symlink(id)
4367+ } else {
4368+ let mut slice: &[u8] = side.bytes.as_ref();
4369+ let id = store
4370+ .write_file(&RepoPathBuf::root(), &mut slice)
4371+ .await
4372+ .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4373+ TreeValue::File {
4374+ id,
4375+ executable: side.kind == BlobKind::Executable,
4376+ copy_id: CopyId::placeholder(),
4377+ }
4378+ };
4379+ values.push(Some(value));
19114380 }
19124381 let mut adds = Vec::new();
19134382 let mut removes = Vec::new();
@@ -1937,34 +4406,49 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
19374406// jj trees -> values
19384407// ----------------------------------------------------------------------
19394408
1940−/// blob contents keyed by file id, shared across the whole repo load: most
1941−/// files are unchanged between commits, so inflating each blob once (instead
1942−/// of once per commit that references it) keeps loading linear in the
1943−/// repository's unique data rather than commits × files
1944−#[derive(Default)]
1945−struct BlobCache(RefCell<HashMap<FileId, Rc<Vec<u8>>>>);
4409+/// Blob contents keyed by file id, shared while snapshots hold their bytes.
4410+/// Weak entries let discarded replay results release their conflict sides.
4411+#[derive(Clone, Default)]
4412+struct BlobCache {
4413+ bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
4414+ next_sweep: Rc<Cell<usize>>,
4415+}
19464416
19474417impl BlobCache {
19484418 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
1949− self.0.borrow().get(id).cloned()
4419+ self.bytes.borrow().get(id).and_then(Weak::upgrade)
19504420 }
19514421 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
1952− 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));
4427+ }
19534428 }
19544429}
19554430
1956−/// whole file *entries* ({path, content}) keyed by (path, blob identity),
1957−/// shared across the whole repo load: an unchanged file is the same Value in
1958−/// every commit, so it is built once and cloned (an Rc bump) thereafter
1959−#[derive(Default)]
1960−struct EntryCache(RefCell<HashMap<String, Value>>);
4431+/// Whole file entries keyed by (path, blob identity), shared by loaded
4432+/// snapshots and replays. Weak references let transient replay results go
4433+/// away while unchanged entries still held by a snapshot are reused.
4434+#[derive(Clone, Default)]
4435+struct EntryCache {
4436+ entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437+ next_sweep: Rc<Cell<usize>>,
4438+}
19614439
19624440impl EntryCache {
19634441 fn get(&self, key: &str) -> Option<Value> {
1964− self.0.borrow().get(key).cloned()
4442+ self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
19654443 }
19664444 fn insert(&self, key: String, entry: Value) {
1967− self.0.borrow_mut().insert(key, entry);
4445+ let Value::Record(record) = entry else { return };
4446+ let mut entries = self.entries.borrow_mut();
4447+ entries.insert(key, Rc::downgrade(&record));
4448+ if entries.len() >= self.next_sweep.get().max(256) {
4449+ entries.retain(|_, entry| entry.strong_count() != 0);
4450+ self.next_sweep.set((entries.len() * 2).max(256));
4451+ }
19684452 }
19694453}
19704454
@@ -1992,20 +4476,64 @@ async fn read_file_bytes(
19924476 Ok(buf)
19934477}
19944478
1995−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(
19964482 store: &Arc<Store>,
19974483 path: &RepoPath,
19984484 v: &Option<TreeValue>,
19994485 cache: &BlobCache,
2000−) -> Result<Rc<Vec<u8>>, Crash> {
4486+) -> Result<Option<ConflictSide>, Crash> {
20014487 match v {
2002− Some(TreeValue::File { id, .. }) => read_file_bytes(store, path, id, cache).await,
2003− Some(TreeValue::Symlink(id)) => store
2004− .read_symlink(path, id)
2005− .await
2006− .map(|s| Rc::new(s.into_bytes()))
2007− .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e))),
2008− _ => Ok(Rc::new(Vec::new())),
4488+ None => Ok(None),
4489+ Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490+ kind: if *executable {
4491+ BlobKind::Executable
4492+ } else {
4493+ BlobKind::Regular
4494+ },
4495+ bytes: read_file_bytes(store, path, id, cache).await?,
4496+ tree: None,
4497+ })),
4498+ Some(TreeValue::Symlink(id)) => {
4499+ let target = store
4500+ .read_symlink(path, id)
4501+ .await
4502+ .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
4503+ Ok(Some(ConflictSide {
4504+ kind: BlobKind::Symlink,
4505+ bytes: Rc::new(target.into_bytes()),
4506+ tree: None,
4507+ }))
4508+ }
4509+ // a directory side, in a conflict between a file and a directory,
4510+ // reads as empty content but keeps its tree, so writing the conflict
4511+ // back cannot put an empty file where the directory was
4512+ Some(TreeValue::Tree(id)) => Ok(Some(ConflictSide {
4513+ kind: BlobKind::Regular,
4514+ bytes: Rc::new(Vec::new()),
4515+ tree: Some(id.hex()),
4516+ })),
4517+ // a submodule side reads as an empty file, as a resolved one does in
4518+ // tree_value_to_blob (§12)
4519+ Some(TreeValue::GitSubmodule(_)) => Ok(Some(ConflictSide::regular(&[]))),
4520+ }
4521+}
4522+
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),
20094537 }
20104538}
20114539
@@ -2015,6 +4543,9 @@ async fn merged_value_to_blob(
20154543 value: &jj_lib::backend::MergedTreeValue,
20164544 cache: &BlobCache,
20174545) -> 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();
20184549 if let Some(v) = value.as_resolved() {
20194550 return match v {
20204551 Some(tv) => tree_value_to_blob(store, path, tv).await,
@@ -2025,11 +4556,11 @@ async fn merged_value_to_blob(
20254556 };
20264557 }
20274558 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
2028− let mut sides: Vec<Rc<Vec<u8>>> = Vec::new();
4559+ let mut sides: Vec<Option<ConflictSide>> = Vec::new();
20294560 for (i, add) in value.adds().enumerate() {
2030− sides.push(side_to_bytes(store, path, add, cache).await?);
4561+ sides.push(conflict_side(store, path, add, cache).await?);
20314562 if let Some(remove) = value.get_remove(i) {
2032− sides.push(side_to_bytes(store, path, remove, cache).await?);
4563+ sides.push(conflict_side(store, path, remove, cache).await?);
20334564 }
20344565 }
20354566 Ok(Value::Blob(Rc::new(BlobVal {
@@ -2081,8 +4612,9 @@ async fn tree_value_to_blob(
20814612 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
20824613 })))
20834614 }
2084− // a conflicted directory is emitted without its children by
2085− // MergedTree::entries; §12 puts such repairs out of scope
4615+ // MergedTree::entries recurses into a resolved directory, and a
4616+ // directory side of a conflict goes through conflict_side, so this is
4617+ // a submodule, which §12 puts out of scope
20864618 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
20874619 kind: BlobKind::Regular,
20884620 content: BlobContent::Resolved(Rc::new(Vec::new())),
@@ -2096,9 +4628,27 @@ async fn tree_to_files(
20964628 cache: &BlobCache,
20974629 entries: &EntryCache,
20984630) -> Result<Vec<Value>, Crash> {
4631+ tree_to_files_inner(store, tree, cache, entries).await.map(|(files, _)| files)
4632+}
4633+
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>,
4639+ tree: &MergedTree,
4640+ cache: &BlobCache,
4641+ entries: &EntryCache,
4642+) -> Result<(Vec<Value>, bool), Crash> {
20994643 let mut out = Vec::new();
4644+ let mut reusable = tree.tree_ids().is_resolved();
21004645 for (path, value) in tree.entries() {
21014646 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4647+ reusable &= match value.as_resolved() {
4648+ Some(Some(TreeValue::File { copy_id, .. })) => *copy_id == CopyId::placeholder(),
4649+ Some(Some(TreeValue::Symlink(_))) => true,
4650+ _ => false,
4651+ };
21024652 // cache key: the entry is fully determined by its path and the
21034653 // identity of its (resolved) content; conflicts are not cached
21044654 let key = match value.as_resolved() {
@@ -2129,13 +4679,22 @@ async fn tree_to_files(
21294679 };
21304680 out.push(entry);
21314681 }
2132− Ok(out)
4682+ Ok((out, reusable))
21334683}
21344684
21354685// ----------------------------------------------------------------------
21364686// value helpers
21374687// ----------------------------------------------------------------------
21384688
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();
4695+ ids.join(",")
4696+}
4697+
21394698fn commit_id_of(commit: &Value) -> Result<String, Crash> {
21404699 match commit.field("id")? {
21414700 Value::Id(i) => Ok(i.to_string()),
@@ -2163,15 +4722,13 @@ fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
21634722// building the Repo value from the visible map (§7.2)
21644723// ----------------------------------------------------------------------
21654724
2166−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> {
21674726 let root_rec = vis
21684727 .commits
21694728 .get(ROOT_ID)
21704729 .ok_or_else(|| (2, "no root commit".to_string()))?;
21714730 let store = root_rec.commit.store().clone();
2172− let cache = BlobCache::default();
2173− let entries = EntryCache::default();
2174− let mut subtree = build_subtree(vis, root_rec, &store, &cache, &entries)?;
4731+ let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
21754732 sort_subtree(&mut subtree, vis);
21764733 let top = Value::record(&[
21774734 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
@@ -2188,11 +4745,22 @@ fn build_subtree(
21884745 vis: &VisibleRepo,
21894746 rec: &StoredCommit,
21904747 store: &Arc<Store>,
2191− cache: &BlobCache,
2192− entries: &EntryCache,
4748+ replay_cache: &Rc<ReplayCache>,
21934749) -> Result<Value, OpenError> {
2194− let files =
2195− block_on(tree_to_files(store, &rec.commit.tree(), cache, entries)).map_err(|c| (2, c.msg))?;
4750+ // the files list is lazy (§7.2): most commits are never inspected, so
4751+ // their file entries are materialized only on first `field("files")`.
4752+ // Tagged with the change id, it tells the renderer when the O(1)
4753+ // `has_conflict`/`is_empty` answers still describe the commit's files;
4754+ // named by its tree, it lets persistence compare it unread (§7.5)
4755+ let files_thunk = {
4756+ let store = store.clone();
4757+ let tree = rec.commit.tree();
4758+ let name = Some(tree_name(&tree));
4759+ let replay_cache = replay_cache.clone();
4760+ Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
4761+ block_on(replay_cache.files_of(&store, &tree))
4762+ })))
4763+ };
21964764 let labels: Vec<Value> = vis
21974765 .labels
21984766 .get(&rec.change_id)
@@ -2202,7 +4770,7 @@ fn build_subtree(
22024770 .map(Value::text)
22034771 .collect();
22044772 let commit_v = Value::record(&[
2205− ("files", Value::list(files)),
4773+ ("files", files_thunk),
22064774 ("id", Value::Id(Rc::new(rec.change_id.clone()))),
22074775 ("labels", Value::list(labels)),
22084776 ("message", Value::text(rec.commit.description())),
@@ -2211,7 +4779,7 @@ fn build_subtree(
22114779 if let Some(kids) = vis.children.get(&rec.change_id) {
22124780 for kid in kids {
22134781 if let Some(krec) = vis.commits.get(kid) {
2214− children.push(build_subtree(vis, krec, store, cache, entries)?);
4782+ children.push(build_subtree(vis, krec, store, replay_cache)?);
22154783 }
22164784 }
22174785 }
@@ -2246,3 +4814,165 @@ fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
22464814 let root = subtree.field("root").expect("subtree has a root");
22474815 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
22484816}
4817+
4818+#[cfg(test)]
4819+mod tests {
4820+ use super::*;
4821+ use jj_lib::backend::SymlinkId;
4822+ use jj_lib::conflict_labels::ConflictLabels;
4823+
4824+ fn replay_store() -> (tempfile::TempDir, Arc<Store>) {
4825+ let dir = tempfile::tempdir().unwrap();
4826+ let (_, repo) = block_on(Workspace::init_colocated_git(
4827+ &user_settings(None), dir.path(), gix::hash::Kind::Sha1,
4828+ )).unwrap();
4829+ (dir, repo.store().clone())
4830+ }
4831+
4832+ fn file(path: &str, content: &str) -> Value {
4833+ Value::record(&[
4834+ ("path", Value::list(vec![Value::text(path)])),
4835+ ("content", BlobVal::text_blob(content)),
4836+ ])
4837+ }
4838+
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()
4842+ }
4843+
4844+ #[test]
4845+ fn replay_reuses_loaded_snapshots_and_unchanged_entries() {
4846+ let (_dir, store) = replay_store();
4847+ let cache = ReplayCache::default();
4848+ let base = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "base b")]);
4849+ let same = block_on(jj_replay(&store, &cache, &base, &base, &base)).unwrap();
4850+ let (Value::List(before), Value::List(after)) = (&base, &same) else { panic!() };
4851+ assert_eq!(before.identity(), after.identity(), "no-op replay must retain the snapshot");
4852+
4853+ let onto = loaded_files(&store, &cache, vec![file("a", "onto"), file("b", "base b")]);
4854+ let to = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "to")]);
4855+ let merged = block_on(jj_replay(&store, &cache, &onto, &base, &to)).unwrap();
4856+ let expected = Value::list(vec![file("a", "onto"), file("b", "to")]);
4857+ assert!(crate::value::value_eq(&merged, &expected).unwrap());
4858+ for (result, input) in [
4859+ (&merged.as_list().unwrap()[0], &onto.as_list().unwrap()[0]),
4860+ (&merged.as_list().unwrap()[1], &to.as_list().unwrap()[1]),
4861+ ] {
4862+ let (Value::Record(result), Value::Record(input)) = (result, input) else { panic!() };
4863+ assert!(Rc::ptr_eq(result, input), "unchanged file entries must be shared");
4864+ }
4865+ }
4866+
4867+ #[test]
4868+ fn replay_cache_preserves_validation_ordering_and_list_offsets() {
4869+ let (_dir, store) = replay_store();
4870+ let cache = ReplayCache::default();
4871+ let unsorted = Value::list(vec![file("b", "b"), file("a", "a")]);
4872+ let empty = Value::list(vec![]);
4873+ let sorted = block_on(jj_replay(&store, &cache, &unsorted, &empty, &empty)).unwrap();
4874+ assert!(crate::value::value_eq(&sorted, &Value::list(vec![file("a", "a"), file("b", "b")])).unwrap());
4875+
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);
4881+
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);
4885+ }
4886+
4887+ #[test]
4888+ fn replay_ignores_empty_directories_not_represented_by_snapshots() {
4889+ let (_dir, store) = replay_store();
4890+ let cache = ReplayCache::default();
4891+ let entry = |id| jj_lib::backend::Tree::from_sorted_entries(vec![
4892+ (RepoPathComponentBuf::new("d".to_string()).unwrap(), TreeValue::Tree(id)),
4893+ ]);
4894+ let direct = block_on(store.write_tree(RepoPath::root(), entry(store.empty_tree_id().clone()))).unwrap();
4895+ let nested = block_on(store.write_tree(RepoPath::root(), entry(direct.id().clone()))).unwrap();
4896+ let empty = Value::list(vec![]);
4897+ let to = Value::list(vec![file("d", "file")]);
4898+ for tree in [direct, nested] {
4899+ let tree = MergedTree::resolved(store.clone(), tree.id().clone());
4900+ let onto = block_on(cache.files_of(&store, &tree)).unwrap();
4901+ assert!(onto.as_list().unwrap().is_empty());
4902+ let result = block_on(jj_replay(&store, &cache, &onto, &empty, &to)).unwrap();
4903+ assert!(crate::value::value_eq(&result, &to).unwrap());
4904+ }
4905+ }
4906+
4907+ #[test]
4908+ fn replay_cache_does_not_keep_discarded_snapshots_alive() {
4909+ let (_dir, store) = replay_store();
4910+ let cache = ReplayCache::default();
4911+ let files = loaded_files(&store, &cache, vec![file("a", "a")]);
4912+ let Value::List(list) = &files else { panic!() };
4913+ let storage = list.weak_storage();
4914+ let Value::Record(entry) = &files.as_list().unwrap()[0] else { panic!() };
4915+ let entry = Rc::downgrade(entry);
4916+ drop(files);
4917+ assert!(storage.upgrade().is_none());
4918+ assert!(entry.upgrade().is_none());
4919+
4920+ let bytes = Rc::new(vec![1, 2, 3]);
4921+ let weak_bytes = Rc::downgrade(&bytes);
4922+ let id = FileId::new(vec![1; 20]);
4923+ cache.blobs.insert(id.clone(), bytes.clone());
4924+ assert!(Rc::ptr_eq(&cache.blobs.get(&id).unwrap(), &bytes));
4925+ drop(bytes);
4926+ assert!(weak_bytes.upgrade().is_none());
4927+ assert!(cache.blobs.get(&id).is_none());
4928+ }
4929+
4930+ #[test]
4931+ fn user_settings_preserve_every_ascii_control_character() {
4932+ let identity: String = (0u8..=127).map(char::from).collect();
4933+ let text = format!(
4934+ "[user]\nname = {}\nemail = {}\n",
4935+ toml_string(&identity),
4936+ toml_string(&identity)
4937+ );
4938+ let mut config = StackedConfig::with_defaults();
4939+ config.add_layer(ConfigLayer::parse(ConfigSource::User, &text).unwrap());
4940+ let settings = UserSettings::from_config(config).unwrap();
4941+ assert_eq!(settings.user_name(), identity);
4942+ assert_eq!(settings.user_email(), identity);
4943+ }
4944+
4945+ #[test]
4946+ fn a_description_names_its_terms_whatever_their_labels_and_the_sides_that_cancel() {
4947+ // what a checkout writes for a symlink/file conflict of five terms,
4948+ // and for the three it simplifies to, which a snapshot may write
4949+ // without writing the file again (§7.4)
4950+ let link = Some(TreeValue::Symlink(SymlinkId::new(vec![1; 20])));
4951+ let file = |n: u8| {
4952+ let id = FileId::new(vec![n; 20]);
4953+ Some(TreeValue::File { id, executable: false, copy_id: CopyId::placeholder() })
4954+ };
4955+ let five = Merge::from_vec(vec![link.clone(), link.clone(), file(2), file(1), link.clone()]);
4956+ let three = five.simplify();
4957+ assert_eq!(three.num_sides(), 2);
4958+ let unlabeled = ConflictLabels::unlabeled();
4959+ let written = five.describe(&unlabeled);
4960+ let simpler = three.describe(&unlabeled);
4961+ assert_ne!(written, simpler);
4962+ let terms = described_terms(simpler.as_bytes());
4963+ assert!(terms.is_some());
4964+ assert_eq!(described_terms(written.as_bytes()), terms);
4965+ let names = ["left (one)", "base", "middle", "base 2", "right"];
4966+ let labels = ConflictLabels::from_vec(names.iter().map(|n| n.to_string()).collect());
4967+ let labelled = five.describe(&labels);
4968+ assert!(labelled.contains(" (left (one))\n"), "{}", labelled);
4969+ assert_eq!(described_terms(labelled.as_bytes()), terms);
4970+ // other sides, and text that is no description, are no such conflict
4971+ let other = Merge::from_vec(vec![link.clone(), file(1), file(3)]).describe(&unlabeled);
4972+ assert!(described_terms(other.as_bytes()).is_some_and(|t| Some(t) != terms));
4973+ assert_eq!(described_terms(b"resolved\n"), None);
4974+ assert_eq!(described_terms(written.replace("Conflict:", "CONFLICT:").as_bytes()), None);
4975+ assert_eq!(described_terms(written.trim_end().as_bytes()), None);
4976+ assert_eq!(described_terms(format!("{} Adding file with id 0101 and more\n", written).as_bytes()), None);
4977+ }
4978+}