@@ -6,33 +6,38 @@
6use crate::config::Config;
6use crate::config::Config;
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
7use crate::domain::{Backend, MetaInfo, ROOT_ID};
8use crate::eval::Interp;
8use crate::eval::Interp;
9−use crate::value::{BlobContent, BlobKind, BlobVal, Crash, LazyBlob, Value};
9+use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, LazyBlob, ListVal, ThunkVal, Value};
10+use futures::{StreamExt, TryStreamExt};
10use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
11use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
11use jj_lib::commit::Commit;
12use jj_lib::commit::Commit;
12use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
13use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14+use jj_lib::conflicts::ConflictMarkerStyle;
13use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
15use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
14use jj_lib::gitignore::GitIgnoreFile;
16use jj_lib::gitignore::GitIgnoreFile;
17+use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
15−use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
18+use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
16use jj_lib::merge::{Diff, Merge};
19use jj_lib::merge::{Diff, Merge};
17use jj_lib::merged_tree::MergedTree;
20use jj_lib::merged_tree::MergedTree;
18use jj_lib::merged_tree_builder::MergedTreeBuilder;
21use jj_lib::merged_tree_builder::MergedTreeBuilder;
19−use jj_lib::backend::{CommitId, FileId};
22+use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
20use jj_lib::object_id::ObjectId;
23use jj_lib::object_id::ObjectId;
21use jj_lib::op_store::OperationId;
24use jj_lib::op_store::OperationId;
22use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
25use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
23use jj_lib::repo::{ReadonlyRepo, Repo};
26use jj_lib::repo::{ReadonlyRepo, Repo};
24−use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponentBuf};
27+use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent, RepoPathComponentBuf};
28+use jj_lib::revset::{ResolvedRevsetExpression, RevsetStreamExt};
25use jj_lib::settings::UserSettings;
29use jj_lib::settings::UserSettings;
26use jj_lib::store::Store;
30use jj_lib::store::Store;
31+use jj_lib::transaction::UnpublishedOperation;
27−use jj_lib::working_copy::SnapshotOptions;
32+use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
28use jj_lib::workspace::Workspace;
33use jj_lib::workspace::Workspace;
29−use std::cell::RefCell;
34+use std::cell::{Cell, RefCell};
30use std::collections::{BTreeMap, BTreeSet, HashMap};
35use std::collections::{BTreeMap, BTreeSet, HashMap};
31use std::future::Future;
36use std::future::Future;
32use std::os::unix::io::AsRawFd;
37use std::os::unix::io::AsRawFd;
33use std::path::PathBuf;
38use std::path::PathBuf;
34−use std::rc::Rc;
39+use std::rc::{Rc, Weak};
35−use std::sync::{Arc, Mutex};
40+use std::sync::{Arc, Mutex, OnceLock};
37fn block_on<F: Future>(f: F) -> F::Output {
42fn block_on<F: Future>(f: F) -> F::Output {
38 pollster::block_on(f)
43 pollster::block_on(f)
@@ -40,6 +45,15 @@ fn block_on<F: Future>(f: F) -> F::Output {
41type OpenError = (u8, String);
46type OpenError = (u8, String);
48+/// The successful effects of a push, after duplicate requests are merged.
49+/// All zeroes means no bookmarks were selected and nothing was sent or saved.
50+#[derive(Clone, Debug, Default, PartialEq, Eq)]
51+pub struct PushOutcome {
52+ pub published: usize,
54+ pub unchanged: usize,
43// ----------------------------------------------------------------------
57// ----------------------------------------------------------------------
44// locating and opening the repository (§7.1)
58// locating and opening the repository (§7.1)
45// ----------------------------------------------------------------------
59// ----------------------------------------------------------------------
@@ -64,6 +78,7 @@ fn toml_string(s: &str) -> String {
64 '\n' => out.push_str("\\n"),
78 '\n' => out.push_str("\\n"),
65 '\r' => out.push_str("\\r"),
79 '\r' => out.push_str("\\r"),
66 '\t' => out.push_str("\\t"),
80 '\t' => out.push_str("\\t"),
81+ c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
67 c => out.push(c),
82 c => out.push(c),
@@ -252,31 +267,1166 @@ pub struct JjInner {
252 repo: Mutex<Arc<ReadonlyRepo>>,
267 repo: Mutex<Arc<ReadonlyRepo>>,
253 workspace_name: WorkspaceNameBuf,
268 workspace_name: WorkspaceNameBuf,
254 /// visible map backing the `Backend` lookups during evaluation; filled by
269 /// visible map backing the `Backend` lookups during evaluation; filled by
255− /// build_interp with the post-snapshot map
270+ /// build_interp with the post-snapshot map, or the loaded one when the
271+ /// snapshot goes into a new child of an immutable focus (§7.2)
256 visible: Mutex<Option<Arc<VisibleRepo>>>,
272 visible: Mutex<Option<Arc<VisibleRepo>>>,
257 lock_guard: Mutex<Option<FileLock>>,
273 lock_guard: Mutex<Option<FileLock>>,
258− /// the snapshot transaction and working-copy lock, held between
274+ /// the snapshot's unpublished rewrite, kept between build_interp and
259− /// build_interp and persist so the snapshot and the expression's edits
275+ /// persist so the snapshot and the expression's edits form one jj
260− /// form one jj operation (§1.2); dropped uncommitted by printing runs
276+ /// operation (§1.2); dropped uncommitted by printing runs. The
277+ /// working-copy lock is not held: its saved state is still the tree
278+ /// recorded before the snapshot until `persist`'s checkout scans the
261 pending: Mutex<Option<PendingSnapshot>>,
280 pending: Mutex<Option<PendingSnapshot>>,
281+ /// whether the workspace's filesystem folds case, probed on first use
282+ /// (`Backend::folds_case`)
283+ folds_case: OnceLock<bool>,
284+ /// whether the workspace's filesystem holds each name longer than
285+ /// `NAME_MAX` asked about so far (`Backend::name_fits`)
286+ long_names: Mutex<HashMap<String, bool>>,
264/// holding the file keeps the flock
289/// holding the file keeps the flock
265pub struct FileLock(#[allow(dead_code)] std::fs::File);
290pub struct FileLock(#[allow(dead_code)] std::fs::File);
268struct PendingSnapshot {
292struct PendingSnapshot {
269 /// repo as loaded at head, before the snapshot
293 /// repo as loaded at head, before the snapshot
270 pre_repo: Arc<ReadonlyRepo>,
294 pre_repo: Arc<ReadonlyRepo>,
271− /// the snapshot's rewritten wc commit and tree, to be folded into the
295+ /// the snapshot's tree for the wc commit, to be folded into the
272 /// persisting operation (§1.2 step 8, §7.7)
296 /// persisting operation (§1.2 step 8, §7.7)
273− new_wc_id: CommitId,
274 tree: MergedTree,
297 tree: MergedTree,
298+ /// false when the wc commit is immutable: the snapshot then belongs to
299+ /// the focus's new child (§7.2), and the wc commit is kept as stored
301+ /// the tree the scan found in the working directory, which `persist`'s
302+ /// checkout starts from: `tree` itself, unless the working copy was
303+ /// stale and `tree` merges the directory's changes onto the commit
304+ /// (`snapshot_tree`)
305+ scanned: MergedTree,
308+/// How every scan of the working directory reads it (§7.4): each file
309+/// not ignored by the directory's own `.gitignore`s is tracked, whatever
310+/// its size. `persist`'s checkout rescans with the same options and
311+/// relies on getting the snapshot's tree back.
312+fn snapshot_options() -> SnapshotOptions<'static> {
314+ base_ignores: GitIgnoreFile::empty(),
316+ start_tracking_matcher: &EverythingMatcher,
317+ force_tracking_matcher: &NothingMatcher,
318+ max_new_file_size: u64::MAX,
322+/// Scan the working directory with `options`, as `wc.snapshot` does, where
323+/// `base` is the tree the working copy's state records, as `saved` holds
324+/// it; return the tree scanned. jj's scan loses a regular file found where
325+/// `base` has a conflict it cannot write a file into, one with a side that
326+/// is no file, and keeps the conflict, so the file reaches no tree (§7.4):
328+/// - at a conflicted path, where a checkout wrote jj's description of the
329+/// sides and a file written since holds something else
330+/// (`FilesOnConflicts::at`). The state is first reset to a resolved file
331+/// there, so that the scan reads the file whole, as it reads any tracked
332+/// file; the path stays tracked where a `.gitignore` has come to match it.
333+/// - above one, where `base` has a directory holding a conflict, whose
334+/// differing sides the scan keeps at the path while it removes the
335+/// entries below (and a debug build fails an assertion). Such a file is
336+/// first left untracked, which scans it as if it had been moved away and
337+/// records the directory's removal; a second scan then records the file
338+/// where nothing is, as a later run would once it was put back.
340+ wc: &mut dyn LockedWorkingCopy,
341+ root: &std::path::Path,
344+ options: &SnapshotOptions<'_>,
345+) -> Result<MergedTree, String> {
346+ let files = files_on_conflicts(root, base, Some(saved))?;
347+ let mut state = base.clone();
348+ if !files.at.is_empty() {
349+ // the empty file: a reset records the file at each path it changes
350+ // as an empty one, not executable, modified at the epoch, so the
351+ // scan reads any other file there, and keeps this one for such a file
352+ let store = base.store();
353+ let id = store.write_file(RepoPath::root(), &mut &[][..]).await.map_err(|e| e.to_string())?;
354+ let copy_id = CopyId::placeholder();
355+ let empty = Merge::normal(TreeValue::File { id, executable: false, copy_id });
356+ state = reset_paths(wc, &state, files.at.into_iter().map(|path| (path, empty.clone()))).await?;
358+ if !files.over.is_empty() {
359+ let untracked = FilesMatcher::new(&files.over);
360+ let start = DifferenceMatcher::new(options.start_tracking_matcher, &untracked);
361+ let first = SnapshotOptions { start_tracking_matcher: &start, ..options.clone() };
362+ state = scan_pass(wc, root, &state, &first).await?;
363+ // a state that agrees with its tree tracks every entry below the
364+ // directory, so none is left; were a conflict left below, the
365+ // second scan would lose the file after all
366+ if let Some(path) = files_on_conflicts(root, &state, None)?.over.first() {
368+ "cannot record the file `{}` in place of a directory holding a conflict",
369+ path.as_internal_file_string()
373+ scan_pass(wc, root, &state, options).await
376+/// `wc.snapshot(options)`, where `state` is the tree the working copy's
377+/// state records; return the tree scanned. jj's scan reads a directory a
378+/// `.gitignore` ignores only at the paths the state tracks there, and where
379+/// it cannot look at one of them because a directory above it has been
380+/// replaced by a file, it fails instead of taking the path for deleted, as
381+/// it takes one it does not find. The paths below a file are then dropped
382+/// from the state, which records them as deleted, and the directory is
383+/// scanned again (§7.4). A scan fails so while it walks the directory,
384+/// before it changes anything in the state.
386+ wc: &mut dyn LockedWorkingCopy,
387+ root: &std::path::Path,
389+ options: &SnapshotOptions<'_>,
390+) -> Result<MergedTree, String> {
391+ let err = match wc.snapshot(options).await {
392+ Ok((scanned, _stats)) => return Ok(scanned),
395+ let not_a_dir = |e: &std::io::Error| e.kind() == std::io::ErrorKind::NotADirectory;
396+ let SnapshotError::Other { err: source, .. } = &err else {
397+ return Err(err.to_string());
399+ if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400+ return Err(err.to_string());
402+ // one `lstat` per path the state tracks, only once a scan has failed
403+ let mut gone = Vec::new();
404+ for (path, value) in state.entries() {
405+ value.map_err(|e| e.to_string())?;
406+ let Ok(disk) = path.to_fs_path(root) else {
409+ if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410+ gone.push((path, Merge::absent()));
413+ if gone.is_empty() {
414+ return Err(err.to_string());
416+ reset_paths(wc, state, gone).await?;
417+ let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
421+/// Give the working copy's state, which records `state`, the tree `state`
422+/// with `values` at their paths, and return it; nothing in the working
423+/// directory is read or written. A reset takes each path it changes for one
424+/// the next scan is to read again.
425+async fn reset_paths(
426+ wc: &mut dyn LockedWorkingCopy,
428+ values: impl IntoIterator<Item = (RepoPathBuf, jj_lib::backend::MergedTreeValue)>,
429+) -> Result<MergedTree, String> {
430+ let mut builder = MergedTreeBuilder::new(state.clone());
431+ for (path, value) in values {
432+ builder.set_or_remove(path, value);
434+ let tree = builder.write_tree().await.map_err(|e| e.to_string())?;
435+ // a reset reads only the commit's tree
436+ let commit = with_tree(&state.store().root_commit(), tree.clone());
437+ wc.reset(&commit).await.map_err(|e| e.to_string())?;
441+/// Where the working directory holds a regular file that jj's scan reads
442+/// into no tree, as `tree` has a conflict there it cannot write a file into
443+struct FilesOnConflicts {
444+ /// the conflicted paths with a side that is no file (a directory, a
445+ /// symlink, a submodule), which a checkout writes as jj's description of
446+ /// the sides, whose file was written since and describes no such
448+ at: Vec<RepoPathBuf>,
449+ /// the directories holding a conflict, that is those above a conflicted
450+ /// path: a directory's sides differ exactly when a conflict is below
451+ /// it, as jj writes no tree whose sides differ where every path resolves
452+ over: Vec<RepoPathBuf>,
455+/// Where the working copy saves its state, to read back what the state a
456+/// scan's lock loaded records of a file (`files_on_conflicts`). Nothing is
457+/// read until a scan asks.
462+ settings: UserSettings,
466+ /// Where `ws` saves its working copy's state; taken before the lock,
467+ /// which borrows `ws`
468+ fn of(ws: &Workspace) -> SavedState {
469+ let root = ws.workspace_root().to_owned();
470+ let path = match ws.working_copy().downcast_ref::<LocalWorkingCopy>() {
471+ Some(wc) => wc.state_path().to_owned(),
472+ None => root.join(".jj").join("working_copy"),
474+ SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
477+ /// The state as saved, where it records `tree`, with the time it was
478+ /// saved; None where it cannot be read or records another tree. A state
479+ /// file that is not there is not read, as jj would write one to read.
480+ fn load(&self, tree: &MergedTree) -> Option<(TreeState, i64)> {
481+ let meta = self.path.join("tree_state").symlink_metadata().ok()?;
482+ let saved_at = millis(meta.modified().ok()?)?;
483+ let settings = TreeStateSettings::try_from_user_settings(&self.settings).ok()?;
484+ let state = TreeState::load(self.store.clone(), self.root.clone(), self.path.clone(), &settings).ok()?;
485+ (state.current_tree().tree_ids() == tree.tree_ids()).then_some((state, saved_at))
489+/// A modification time in milliseconds since the epoch, as jj's working
491+fn millis(time: std::time::SystemTime) -> Option<i64> {
492+ match time.duration_since(std::time::UNIX_EPOCH) {
493+ Ok(d) => i64::try_from(d.as_millis()).ok(),
494+ Err(e) => i64::try_from(e.duration().as_millis()).ok().map(|m| -m),
498+/// Whether the working copy's state, saved at `saved_at`, records `state`
499+/// for the regular file `meta` describes as it is now: jj's own test of a
500+/// file left as it was (`FileState::is_clean`, the executable bit aside),
501+/// under which it reads nothing. A file modified in the millisecond the
502+/// state was saved, or later, may have been written after it was.
503+fn left_as_recorded(state: &FileState, meta: &std::fs::Metadata, saved_at: i64) -> bool {
504+ matches!(state.file_type, FileType::Normal { .. })
505+ && state.size == meta.len()
506+ && meta.modified().ok().and_then(millis) == Some(state.mtime.0)
507+ && state.mtime.0 < saved_at
510+/// The terms of a conflict that `text`, jj's description of one, names:
511+/// those removed and those added, each without the label a checkout may
512+/// write after it, with each removed that is also added cancelled against
513+/// it, as simplifying the conflict does, sorted; None for text that is no
514+/// description. Two descriptions naming the same terms describe one
515+/// conflict, whatever its labels or the sides it has lost since.
516+fn described_terms(text: &[u8]) -> Option<(Vec<&str>, Vec<&str>)> {
517+ let lines = std::str::from_utf8(text).ok()?.strip_prefix("Conflict:\n")?;
518+ if !lines.is_empty() && !lines.ends_with('\n') {
521+ let (mut removes, mut adds) = (Vec::new(), Vec::new());
522+ for line in lines.split_terminator('\n') {
523+ let (terms, term) = match (line.strip_prefix(" Removing "), line.strip_prefix(" Adding ")) {
524+ (Some(term), _) => (&mut removes, term),
525+ (_, Some(term)) => (&mut adds, term),
528+ // a term ends with its object's id, which holds no space
529+ let id = term.find(" with id ")? + " with id ".len();
530+ let end = term[id..].find(' ').map_or(term.len(), |n| id + n);
531+ let (term, label) = term.split_at(end);
532+ if !label.is_empty() && !(label.starts_with(" (") && label.ends_with(')')) {
537+ removes.retain(|term| match adds.iter().position(|t| t == term) {
539+ adds.swap_remove(n);
544+ removes.sort_unstable();
545+ adds.sort_unstable();
546+ Some((removes, adds))
549+/// Look on disk at `tree`'s conflicts for `FilesOnConflicts`; with no
550+/// `saved`, for `over` only. Nothing is looked at when `tree` is resolved;
551+/// each directory above a conflict at most once, with one `lstat`; and a
552+/// conflict with a side that is no file below directories, with one more,
553+/// and a read where its file is as long as its description. Only a file
554+/// that holds something else is looked up in the saved state, which is read
555+/// once, and read itself where the state records it otherwise.
556+fn files_on_conflicts(
557+ root: &std::path::Path,
559+ saved: Option<&SavedState>,
560+) -> Result<FilesOnConflicts, String> {
561+ let mut files = FilesOnConflicts { at: Vec::new(), over: Vec::new() };
562+ // whether each directory looked at so far is one on disk
563+ let mut dirs: HashMap<RepoPathBuf, bool> = HashMap::new();
564+ // the state as saved, read once a file differs from its description,
565+ // and what it records of the files, taken once
566+ let loaded: std::cell::OnceCell<Option<(TreeState, i64)>> = std::cell::OnceCell::new();
567+ let recorded: std::cell::OnceCell<Option<(FileStates<'_>, i64)>> = std::cell::OnceCell::new();
568+ for (path, value) in tree.conflicts() {
569+ let value = value.map_err(|e| e.to_string())?;
570+ let parent = path.parent().expect("a conflicted path has a parent");
571+ let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
572+ // from the top down, stopping at the first that is not a directory
573+ let below_dirs = above.into_iter().rev().all(|dir| match dirs.get(dir) {
574+ Some(&is_dir) => is_dir,
576+ let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
577+ if meta.as_ref().is_some_and(|m| m.is_file()) {
578+ files.over.push(dir.to_owned());
580+ let is_dir = meta.is_some_and(|m| m.is_dir());
581+ dirs.insert(dir.to_owned(), is_dir);
585+ let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
588+ let Some((disk, meta)) = path.to_fs_path(root).ok().and_then(|disk| {
589+ let meta = disk.symlink_metadata().ok()?;
590+ meta.is_file().then_some((disk, meta))
594+ // what jj's checkout writes for it (§7.4)
595+ let written = value.describe(tree.labels());
596+ let mut bytes = None;
597+ if meta.len() == written.len() as u64 {
598+ match std::fs::read(&disk) {
599+ Ok(read) if read == written.as_bytes() => continue,
600+ Ok(read) => bytes = Some(read),
604+ // the file a checkout wrote, left as it was: it describes the
605+ // conflict as the checkout had it, which the state may since
606+ // record otherwise with no file written, as a scan that drops a
607+ // pair of sides that cancel, or a reset that only relabels, does
608+ let states = recorded.get_or_init(|| {
609+ let (state, saved_at) = loaded.get_or_init(|| saved.load(tree)).as_ref()?;
610+ Some((state.file_states(), *saved_at))
612+ if states.as_ref().is_some_and(|(file_states, saved_at)| {
613+ file_states.get(&path).is_some_and(|state| left_as_recorded(&state, &meta, *saved_at))
617+ // written since, or not known to be left: the conflict is kept
618+ // while the file describes it, as when an editor saves it unchanged
619+ let Some(bytes) = bytes.or_else(|| std::fs::read(&disk).ok()) else {
622+ let unlabelled = value.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
623+ let terms = described_terms(unlabelled.as_bytes());
624+ if terms.is_none() || described_terms(&bytes) != terms {
625+ files.at.push(path);
631+/// The tree a snapshot records in the working-copy commit, whose tree is
632+/// `wc`, where the working copy's state records `state` and a scan of the
633+/// working directory found `scanned` (§7.4): `scanned` itself where the
634+/// state is the commit's, and also where it is `current`, saved at the
635+/// operation the repository is at, which jj takes for the files last
636+/// checked out whatever it records: one lost, which jj makes again
637+/// recording no file, or one put in its place. Such a state may record none
638+/// of the commit's conflicts, so a file holding what a checkout wrote for
639+/// one is read back as it there (`conflicts_written`). Otherwise the state
640+/// is stale (a checkout that failed or was cut short after its operation
641+/// was recorded, a `fetch` that rebased the commit, jj run with
642+/// `--ignore-working-copy` or from another workspace), and what the
643+/// directory holds is a change to `state` rather than to the commit: that
644+/// change is replayed onto the commit, a three-way merge with `state` as
645+/// the base, in which a path both changed differently is a conflict, but
646+/// for one setting a directory against anything else, which takes what the
647+/// working directory holds there, as where the state is current. What a
648+/// checkout cut short wrote is the commit's own content, which the merge
649+/// takes as it is: where the directory holds what the commit does, the base
650+/// is taken to hold it too, so that a conflict the state records there,
651+/// whose sides would not cancel against the commit's, does not make one of
652+/// a change both made. That includes a conflict the checkout wrote, which
653+/// the scan read as its text (`conflicts_written`).
654+async fn snapshot_tree(
655+ root: &std::path::Path,
656+ markers: ConflictMarkerStyle,
660+ scanned: &MergedTree,
661+) -> Result<MergedTree, String> {
662+ if state.tree_ids() == wc.tree_ids() {
663+ return Ok(scanned.clone());
666+ return conflicts_written(root, markers, wc, state, scanned).await;
668+ if scanned.tree_ids() == state.tree_ids() {
669+ return Ok(wc.clone());
671+ let scanned = &conflicts_written(root, markers, wc, state, scanned).await?;
672+ let err = |e: jj_lib::backend::BackendError| e.to_string();
673+ // the paths where the directory holds something other than the commit
674+ let mut apart = std::collections::HashSet::new();
675+ let mut diff = wc.diff_stream(scanned, &EverythingMatcher);
676+ while let Some(entry) = diff.next().await {
677+ entry.values.map_err(err)?;
678+ apart.insert(entry.path);
680+ // the base: the state, but for what the directory changed as the
682+ let mut base = MergedTreeBuilder::new(state.clone());
683+ let mut same = false;
684+ let mut diff = state.diff_stream(scanned, &EverythingMatcher);
685+ while let Some(entry) = diff.next().await {
686+ let Diff { after, .. } = entry.values.map_err(err)?;
687+ if !apart.contains(&entry.path) {
688+ base.set_or_remove(entry.path, after);
692+ let base = if same { base.write_tree().await.map_err(err)? } else { state.clone() };
693+ let merged = MergedTree::merge(Merge::from_removes_adds(
694+ vec![(base.clone(), "last checkout".to_string())],
696+ (wc.clone(), "working-copy commit".to_string()),
697+ (scanned.clone(), "working directory".to_string()),
702+ // A conflict setting a directory against a file, a symlink or nothing,
703+ // which jj keeps at the path itself, cannot be a blob that lists the
704+ // directory's entries (§7.3), and a checkout would write jj's
705+ // description of it in place of what the directory holds there,
706+ // removing a directory's files: where the directory changed the path,
707+ // take what it holds, as a snapshot over a current state does. The
708+ // commit's change there is left in the operation before. Where the
709+ // directory holds what the base does, the merge took the commit's value
710+ // as it is, so such a conflict is one the commit already has, and it
711+ // stays. A tree with no conflict is not walked.
712+ let mut kept = MergedTreeBuilder::new(merged.clone());
713+ let mut mixed = false;
714+ for (path, value) in merged.conflicts() {
715+ if value.map_err(err)?.iter().any(|term| matches!(term, Some(TreeValue::Tree(_)))) {
716+ let held = scanned.path_value(&path).await.map_err(err)?;
717+ if held == base.path_value(&path).await.map_err(err)? {
720+ kept.set_or_remove(path, held);
727+ kept.write_tree().await.map_err(err)
730+/// `scanned` with the commit's conflict at each path the working-copy state
731+/// records something else at, where the file holds what jj's checkout
732+/// writes for that conflict (`holds_written`): its markers, or its
733+/// description where a side is no file (§7.4). A checkout records the
734+/// conflict in the state as it writes the file, so a scan reads such a file
735+/// back as the conflict; one cut short, or one over a stale state, records
736+/// nothing, nor does a state made again where it was lost, and the scan
737+/// reads the file as the text it holds. Each of the commit's conflicts is
738+/// looked at, and a file read, only where neither the state nor the scan
739+/// has the conflict.
740+async fn conflicts_written(
741+ root: &std::path::Path,
742+ markers: ConflictMarkerStyle,
745+ scanned: &MergedTree,
746+) -> Result<MergedTree, String> {
747+ let err = |e: jj_lib::backend::BackendError| e.to_string();
748+ let mut tree = MergedTreeBuilder::new(scanned.clone());
749+ let mut found = false;
750+ for (path, value) in wc.conflicts() {
751+ let value = value.map_err(err)?;
752+ if state.path_value(&path).await.map_err(err)? == value
753+ || scanned.path_value(&path).await.map_err(err)? == value
757+ let Ok(disk) = path.to_fs_path(root) else {
760+ if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761+ tree.set_or_remove(path, value);
766+ return Ok(scanned.clone());
768+ tree.write_tree().await.map_err(err)
771+/// Whether `disk`, the entry at `path`, holds what a checkout writes there
772+/// for `value`: a file's content or a symlink's target, whatever its
773+/// executable bit, or, for a conflict, exactly its markers, compared byte
774+/// for byte (j's settings convert no line endings), or, where a side is no
775+/// file, a description of the same sides, whatever their labels and less
776+/// any it both removes and adds, as a scan reads one back
777+/// (`files_on_conflicts`); nothing is written to the store
778+async fn holds_written(
781+ value: jj_lib::backend::MergedTreeValue,
782+ labels: &jj_lib::conflict_labels::ConflictLabels,
783+ markers: ConflictMarkerStyle,
784+ disk: &std::path::Path,
785+) -> Result<bool, String> {
786+ use jj_lib::conflicts::{self, MaterializedTreeValue};
787+ let symlink = matches!(value.as_normal(), Some(TreeValue::Symlink(_)));
788+ let Some(meta) = disk.symlink_metadata().ok().filter(|m| if symlink { m.is_symlink() } else { m.is_file() })
792+ let written = conflicts::materialize_tree_value(store, path, value, labels)
794+ .map_err(|e| e.to_string())?;
796+ MaterializedTreeValue::Symlink { target, .. } => {
797+ Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
799+ MaterializedTreeValue::File(mut file) => {
800+ let bytes = file.read_all(path).await.map_err(|e| e.to_string())?;
801+ Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
803+ MaterializedTreeValue::FileConflict(file) => {
804+ let options = conflicts::ConflictMaterializeOptions {
805+ marker_style: markers,
806+ marker_len: Some(conflicts::choose_materialized_conflict_marker_len(&file.contents)),
807+ merge: store.merge_options().clone(),
809+ let bytes: Vec<u8> =
810+ conflicts::materialize_merge_result_to_bytes(&file.contents, &file.labels, &options).into();
811+ Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
813+ MaterializedTreeValue::OtherConflict { id, .. } => {
814+ let unlabelled = id.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
815+ let terms = described_terms(unlabelled.as_bytes());
816+ Ok(terms.is_some() && std::fs::read(disk).is_ok_and(|b| described_terms(&b) == terms))
822+/// How the working copy writes a conflict, as `settings` say
823+fn marker_style(settings: &UserSettings) -> Result<ConflictMarkerStyle, String> {
824+ Ok(TreeStateSettings::try_from_user_settings(settings).map_err(|e| e.to_string())?.conflict_marker_style)
827+/// The first of the directories between the root and `path`, from the top
828+/// down, that is no directory on disk, with the type of what stands there
829+/// (none where nothing does, or it cannot be looked at), or none if each
830+/// is: a symlink is not one, and is not followed, as no directory below
831+/// one is looked at. Each directory not looked at before costs one
832+/// `lstat`, and what it found is kept in `dirs`.
834+ root: &std::path::Path,
836+ dirs: &mut HashMap<RepoPathBuf, Option<std::fs::FileType>>,
837+) -> Option<(RepoPathBuf, Option<std::fs::FileType>)> {
838+ let parent = path.parent()?;
839+ let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
840+ for dir in above.into_iter().rev() {
841+ let stands = match dirs.get(dir) {
842+ Some(&stands) => stands,
844+ let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
845+ let stands = meta.map(|m| m.file_type());
846+ dirs.insert(dir.to_owned(), stands);
850+ if !stands.is_some_and(|t| t.is_dir()) {
851+ return Some((dir.to_owned(), stands));
857+/// The first path where the working directory, scanned again as
858+/// `rescanned`, no longer holds what it held as `on_disk`, the run's
859+/// snapshot of it, and a checkout from `on_disk` to `to` writes or removes
860+/// a file: a change made while the program ran, which no snapshot holds and
861+/// jj's checkout would write over without looking (§7.5 step 7). Paths are
862+/// compared file by file, as a directory is by what it holds; one lookup
863+/// in `to` per path the directory changed at.
864+async fn written_over(
865+ on_disk: &MergedTree,
866+ rescanned: &MergedTree,
868+) -> Result<Option<RepoPathBuf>, String> {
869+ let err = |e: jj_lib::backend::BackendError| e.to_string();
870+ let mut diff = on_disk.diff_stream(rescanned, &EverythingMatcher);
871+ while let Some(entry) = diff.next().await {
872+ let Diff { before, .. } = entry.values.map_err(err)?;
873+ // a diff has no tree on either side
874+ let focus = to.path_value(&entry.path).await.map_err(err)?;
875+ let focus = if focus.is_tree() { Merge::absent() } else { focus };
876+ if focus != before {
877+ return Ok(Some(entry.path));
883+/// What stands in the way of a path a checkout adds: `holder`, in the
884+/// working directory, which does not track it, at `path`, above it, or in
885+/// a directory at it
888+ holder: RepoPathBuf,
892+ /// Whether it is a directory at `path`, holding `holder`
893+ fn is_dir(&self) -> bool {
894+ self.holder != self.path && self.holder.starts_with(&self.path)
897+ /// What a refused checkout says of it among others: a directory by its
898+ /// own path, with what it holds, anything else by its path
899+ fn named(&self) -> String {
902+ "`{}` (a directory holding untracked files, such as `{}`)",
903+ self.path.as_internal_file_string(),
904+ self.holder.as_internal_file_string()
907+ format!("`{}`", self.holder.as_internal_file_string())
912+/// Why a checkout is refused over `in_the_way`, which is not empty, as
913+/// git names each untracked file in the way: one with the path the focus
914+/// has there, more by listing the first ten and counting the rest
915+fn in_the_way_of(in_the_way: &[InTheWay]) -> String {
916+ const LISTED: usize = 10;
918+ [one] if one.is_dir() => format!(
919+ "it has `{0}`, and `{0}` in the working directory is a directory holding untracked files, such as `{1}`; move it aside",
920+ one.path.as_internal_file_string(),
921+ one.holder.as_internal_file_string()
924+ "it has `{}`, and an untracked `{}` in the working directory is in its way; move it aside",
925+ one.path.as_internal_file_string(),
926+ one.holder.as_internal_file_string()
929+ let mut named: Vec<String> = in_the_way.iter().take(LISTED).map(InTheWay::named).collect();
930+ let last = match in_the_way.len() - named.len() {
931+ 0 => named.pop().unwrap_or_default(),
932+ more => format!("{} more", more),
935+ "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
943+/// What stands in the working directory where a checkout from `from`, the
944+/// tree the directory holds as scanned, to `to` adds a file (or a symlink,
945+/// or a conflict): at each path where `to` has one and `from` has none.
946+/// jj's checkout writes nothing over anything already there, and skips
947+/// the path without failing, recording it as written, so the next scan
948+/// would read what stands there into the focus, an ignored file's content
949+/// in place of the focus's, or take the focus's file for deleted (§7.4).
950+/// What `from` tracks there the checkout removes first; trees of empty
951+/// directories, which no scan sees, and a file holding just what the
952+/// checkout writes there (`holds_written`), as a checkout cut short leaves
953+/// at an ignored path, are cleared for it (`clear_the_way`). The result is
954+/// the paths where nothing else stands, or, walking the whole diff all the
955+/// same, each thing standing where anything else does (`InTheWay`), in
956+/// path order, what stands above several paths once. No symlink is
957+/// followed. Each path costs one `lstat`, and each directory above one not
958+/// looked at before one more; a file standing at a path is read, and a
959+/// directory read until something untracked is found in it, each entry in
960+/// it that is no directory looked up in `from`.
961+async fn look_where_added(
962+ root: &std::path::Path,
963+ markers: ConflictMarkerStyle,
966+) -> Result<Result<Vec<RepoPathBuf>, Vec<InTheWay>>, String> {
967+ let err = |e: jj_lib::backend::BackendError| e.to_string();
968+ let mut dirs = HashMap::new();
969+ // each thing that is no directory looked up in `from`, above a path
970+ let mut looked_up = std::collections::HashSet::new();
971+ let mut clear = Vec::new();
972+ let mut in_the_way = Vec::new();
973+ let mut diff = from.diff_stream(to, &EverythingMatcher);
974+ while let Some(entry) = diff.next().await {
975+ let Diff { before, after } = entry.values.map_err(err)?;
976+ // a submodule's directory is the submodule's own, which jj keeps
977+ if before.is_present() || after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
980+ let path = entry.path;
981+ if let Some((dir, stands)) = first_not_a_dir(root, &path, &mut dirs) {
982+ // where nothing stands above the path, or what `from` tracks
983+ // and the checkout removes, nothing stands at it
984+ if stands.is_some() && looked_up.insert(dir.clone()) {
985+ let is_tracked = from.path_value(&dir).await.map_err(err)?.is_present();
987+ in_the_way.push(InTheWay { path, holder: dir });
992+ let Ok(disk) = path.to_fs_path(root) else {
995+ let Ok(meta) = disk.symlink_metadata() else {
998+ let holder = if meta.is_dir() {
999+ untracked_below(&disk, &path, from)
1000+ } else if holds_written(to.store(), &path, after, to.labels(), markers, &disk).await? {
1006+ Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007+ None => clear.push(path),
1010+ if in_the_way.is_empty() {
1013+ Ok(Err(in_the_way))
1017+/// Something that is no directory in the directory `disk`, standing at
1018+/// `path`, or in a directory in it, that `from` does not track, or a
1019+/// directory that cannot be read; none where it holds nothing else. Entries
1020+/// are told apart without following a symlink, and `from` is read below
1021+/// `path` only once there is something to look up.
1022+fn untracked_below(disk: &std::path::Path, path: &RepoPath, from: &MergedTree) -> Option<RepoPathBuf> {
1023+ let mut tracked: Option<std::collections::HashSet<RepoPathBuf>> = None;
1024+ let mut dirs = vec![(disk.to_owned(), path.to_owned())];
1025+ while let Some((dir, at)) = dirs.pop() {
1026+ let Ok(entries) = std::fs::read_dir(&dir) else {
1029+ for entry in entries {
1030+ let Ok(entry) = entry else {
1033+ let name = entry.file_name();
1034+ // a name no path of a tree holds
1035+ let Some(below) = name.to_str().and_then(|n| RepoPathComponent::new(n).ok()).map(|c| at.join(c)) else {
1038+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039+ dirs.push((entry.path(), below));
1042+ let tracked = tracked.get_or_insert_with(|| {
1043+ let under = jj_lib::matchers::PrefixMatcher::new([path]);
1044+ from.entries_matching(&under).filter(|(_, value)| value.is_ok()).map(|(p, _)| p).collect()
1046+ if !tracked.contains(&below) {
1047+ return Some(below);
1054+/// Clear each of `paths`, where a checkout from the tree the working
1055+/// directory holds to `to` adds a file (`look_where_added`): a file there
1056+/// is moved aside while it holds what the checkout writes
1057+/// (`holds_written`), and in a directory there every directory holding no
1058+/// file is removed, deepest first, and it too once empty. A path below
1059+/// anything that is no directory on disk, a symlink included, is left
1060+/// alone, and no symlink in one is followed; a directory is removed only
1061+/// while empty, so a file put there meanwhile stays, as does one changed
1062+/// meanwhile, and the checkout skips the path (§7.5 step 7). So does one
1063+/// that cannot be moved, which the result names.
1064+async fn clear_the_way(
1065+ root: &std::path::Path,
1066+ markers: ConflictMarkerStyle,
1068+ paths: &[RepoPathBuf],
1070+ fn clear(dir: &std::path::Path) {
1071+ if let Ok(entries) = std::fs::read_dir(dir) {
1072+ for entry in entries.flatten() {
1073+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1074+ clear(&entry.path());
1078+ let _ = std::fs::remove_dir(dir);
1080+ let mut aside = Aside { root: root.to_owned(), dir: None, stuck: None, returned: false };
1081+ let mut dirs = HashMap::new();
1082+ for path in paths {
1083+ if first_not_a_dir(root, path, &mut dirs).is_some() {
1086+ let Ok(disk) = path.to_fs_path(root) else {
1089+ let Ok(meta) = disk.symlink_metadata() else {
1096+ let Ok(value) = to.path_value(path).await else {
1099+ if holds_written(to.store(), path, value, to.labels(), markers, &disk).await.unwrap_or(false) {
1100+ if let Err(e) = aside.take(path, &disk) {
1101+ aside.stuck.get_or_insert((path.clone(), e));
1108+/// The start of the name of each directory in `.jj` that `Aside` moves
1110+const ASIDE: &str = "aside";
1112+/// The files `clear_the_way` moved out of a checkout's way, each holding
1113+/// what the checkout writes where it stood, in a directory in `.jj` that
1114+/// holds each at its path in the working directory `root`, which no scan
1115+/// reads; the first file it could not move, with why; and whether the
1116+/// checkout returned rather than failing, and so wrote in full each file
1117+/// it wrote. Dropped, once the checkout has written what it could, it puts
1118+/// each back (`put_back`) where nothing stands now, as where a checkout
1119+/// that failed or skipped paths did not get to write, and, unless the
1120+/// checkout returned, where a start of it does, as where it was cut short
1121+/// writing it, and removes the directory with the rest: each holds just
1122+/// what the focus has at its path, which the repository keeps, and what
1123+/// stands there, what the checkout wrote or something put there since,
1124+/// stays (§7.5 step 7). One a run killed meanwhile left, the next puts
1125+/// back (`put_back_left`).
1127+ root: std::path::PathBuf,
1128+ dir: Option<tempfile::TempDir>,
1129+ stuck: Option<(RepoPathBuf, std::io::Error)>,
1134+ /// Move `disk`, the file at `path`, into the directory, made on first
1135+ /// use; renamed, it is neither copied nor followed if a symlink
1136+ fn take(&mut self, path: &RepoPath, disk: &std::path::Path) -> std::io::Result<()> {
1137+ let dir = match &mut self.dir {
1139+ none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
1141+ let to = path.to_fs_path(dir.path()).map_err(std::io::Error::other)?;
1142+ if let Some(parent) = to.parent() {
1143+ std::fs::create_dir_all(parent)?;
1145+ std::fs::rename(disk, to)
1149+impl Drop for Aside {
1150+ fn drop(&mut self) {
1151+ if let Some(dir) = self.dir.take() {
1152+ put_back(&self.root, dir.path(), !self.returned);
1157+/// Put each file in `aside`, where `Aside` moved it, back at its path in
1158+/// the working directory `root` where nothing stands, making the
1159+/// directories above it that are missing, and, where the checkout may have
1160+/// been `cut` short, where a start of it stands, less than all of it, as a
1161+/// checkout cut short while writing it leaves (`cut_short`), which it
1162+/// replaces; one that cannot be put back, as something else stands there
1163+/// or in place of a directory above it, stays in `aside`. No symlink is
1165+fn put_back(root: &std::path::Path, aside: &std::path::Path, cut: bool) {
1166+ let missing = |p: &std::path::Path| {
1167+ matches!(p.symlink_metadata(), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
1169+ // whether each directory above `path` in `root` is one, or is made
1170+ let dirs_above = |path: &std::path::Path| {
1171+ let mut at = root.to_owned();
1172+ for name in path.parent().into_iter().flat_map(|p| p.components()) {
1174+ let made = match at.symlink_metadata() {
1175+ Ok(meta) => meta.is_dir(),
1176+ Err(e) => e.kind() == std::io::ErrorKind::NotFound && std::fs::create_dir(&at).is_ok(),
1184+ let mut dirs = vec![std::path::PathBuf::new()];
1185+ while let Some(dir) = dirs.pop() {
1186+ let Ok(entries) = std::fs::read_dir(aside.join(&dir)) else {
1189+ for entry in entries.flatten() {
1190+ let path = dir.join(entry.file_name());
1191+ if entry.file_type().is_ok_and(|t| t.is_dir()) {
1195+ let disk = root.join(&path);
1196+ // the directories above are looked at first, so that nothing
1197+ // is looked at through a symlink
1198+ if dirs_above(&path) && (missing(&disk) || (cut && cut_short(&disk, &entry.path()))) {
1199+ let _ = std::fs::rename(entry.path(), disk);
1205+/// Whether `disk` is a file holding a start of `whole`, a file moved
1206+/// aside, and less than all of it, or, where `whole` is a symlink, an
1207+/// empty file: what jj's checkout leaves where it was cut short (a full
1208+/// disk, a run killed) while it wrote `whole` there, as it makes a file
1209+/// empty before it writes it, and tries a symlink's path with an empty
1210+/// file first. Reads no more of either than `disk` holds. No symlink is
1212+fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1214+ use std::os::unix::fs::OpenOptionsExt;
1215+ let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1218+ if !stands.is_file() {
1221+ if copy.is_symlink() {
1222+ return stands.len() == 0;
1224+ if !copy.is_file() || stands.len() >= copy.len() {
1227+ let stands_file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW).open(disk);
1228+ let (Ok(mut stands_file), Ok(mut copy_file)) = (stands_file, std::fs::File::open(whole)) else {
1231+ let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232+ let mut rest = stands.len();
1234+ let n = rest.min(a.len() as u64) as usize;
1235+ let same = stands_file.read_exact(&mut a[..n]).is_ok()
1236+ && copy_file.read_exact(&mut b[..n]).is_ok()
1237+ && a[..n] == b[..n];
1246+/// Put back what a run killed while its checkout ran left moved aside in
1247+/// `.jj` (`Aside`), over a write the kill may have cut short too, and
1248+/// remove the directory it left there. A run does this once it holds the
1249+/// repository's lock, before it looks at the working directory, so no
1250+/// other run is using such a directory.
1251+fn put_back_left(root: &std::path::Path) {
1252+ let Ok(entries) = std::fs::read_dir(root.join(".jj")) else {
1255+ for entry in entries.flatten() {
1256+ let named = entry.file_name().to_str().is_some_and(|n| n.starts_with(ASIDE));
1257+ if named && entry.file_type().is_ok_and(|t| t.is_dir()) {
1258+ put_back(root, &entry.path(), true);
1259+ let _ = std::fs::remove_dir_all(entry.path());
1264+/// The first path a checkout from `from` to `to` that skipped some left
1265+/// holding something other than what it writes there: with no directory
1266+/// above it, or a directory at it, or, where it adds the path, anything but
1267+/// what it writes (`holds_written`); none if no such path is found. Only
1268+/// once a checkout has skipped a path is the directory looked at again,
1269+/// reading the files it added until one differs.
1270+async fn first_skipped(
1271+ root: &std::path::Path,
1272+ markers: ConflictMarkerStyle,
1275+) -> Result<Option<RepoPathBuf>, String> {
1276+ let mut dirs = HashMap::new();
1277+ let mut diff = from.diff_stream(to, &EverythingMatcher);
1278+ while let Some(entry) = diff.next().await {
1279+ let Diff { before, after } = entry.values.map_err(|e| e.to_string())?;
1280+ if after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
1283+ let path = entry.path;
1284+ if let Some((_, stands)) = first_not_a_dir(root, &path, &mut dirs) {
1285+ if stands.is_some() {
1286+ return Ok(Some(path));
1290+ let Ok(disk) = path.to_fs_path(root) else {
1293+ let Ok(meta) = disk.symlink_metadata() else {
1297+ || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1299+ return Ok(Some(path));
1305+/// Save the working copy's state as a scan that found `scanned` left it:
1306+/// at `head`, the operation the repository is at, where that is the
1307+/// working-copy commit's tree `wc`; otherwise at the operation it was
1308+/// saved at before, so that it stays stale for jj too, which takes a state
1309+/// saved at the head for a current one and would record the directory
1310+/// into the commit whole (§7.4)
1312+ mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313+ scanned: &MergedTree,
1315+ head: &OperationId,
1316+) -> Result<(), String> {
1317+ let op_id = if scanned.tree_ids() == wc.tree_ids() {
1320+ locked_ws.locked_wc().old_operation_id().clone()
1322+ locked_ws.finish(op_id).await.map_err(|e| e.to_string())
1325+/// The signal that asked the run to stop while its checkout was not to be
1326+/// cut short (`Deferral`), or 0
1327+static DEFERRED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1329+extern "C" fn defer(sig: libc::c_int) {
1330+ DEFERRED.store(sig, std::sync::atomic::Ordering::SeqCst);
1333+/// While it lives, SIGINT, SIGTERM, SIGHUP and SIGQUIT, whichever thread
1334+/// gets them, only note that they came: it lives from the operation's
1335+/// publishing until the working copy's state is saved, so that a checkout
1336+/// the user interrupts completes, leaving the working copy current, and
1337+/// through a clone, which one that comes before the working-copy commit is
1338+/// made fails (`unless_interrupted`), so that it removes what it made
1339+/// (§1.3). A signal the process ignores stays ignored, as it does for the
1340+/// git a clone runs (`nohup j clone`, a background job). Dropping it gives
1341+/// each signal back what it did before, and `raise_deferred` then acts on
1344+ previous: Vec<(libc::c_int, libc::sigaction)>,
1348+ fn start() -> Deferral {
1349+ let mut previous = Vec::new();
1350+ for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1351+ // SAFETY: the handler only stores to an atomic, which is
1352+ // async-signal-safe; both actions are initialised before use
1354+ let mut old: libc::sigaction = std::mem::zeroed();
1355+ if libc::sigaction(sig, std::ptr::null(), &mut old) != 0
1356+ || old.sa_sigaction == libc::SIG_IGN
1360+ let mut action: libc::sigaction = std::mem::zeroed();
1361+ action.sa_sigaction = defer as extern "C" fn(libc::c_int) as libc::sighandler_t;
1362+ action.sa_flags = libc::SA_RESTART;
1363+ libc::sigemptyset(&mut action.sa_mask);
1364+ if libc::sigaction(sig, &action, std::ptr::null_mut()) == 0 {
1365+ previous.push((sig, old));
1369+ Deferral { previous }
1373+impl Drop for Deferral {
1374+ fn drop(&mut self) {
1375+ for (sig, old) in &self.previous {
1376+ // SAFETY: `old` is the action `sigaction` gave back for `sig`
1377+ unsafe { libc::sigaction(*sig, old, std::ptr::null_mut()) };
1382+/// Act on a signal a checkout or a clone deferred (§1.3) as it would have
1383+/// acted then: with its default action, stopping the process (`Deferral`
1384+/// defers no signal the process ignores). Called once the run has reported
1386+pub fn raise_deferred() {
1387+ let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
1389+ // a process a signal stops does not flush what it printed
1390+ use std::io::Write;
1391+ let _ = std::io::stdout().flush();
1392+ // SAFETY: raising a signal whose action `Deferral` gave back
1393+ unsafe { libc::raise(sig) };
1397+/// `done`, the outcome of a step of a clone, unless a signal to stop came
1398+/// while `cmd_clone`'s `Deferral` held it off: the clone then fails, before
1399+/// it records anything, so that it removes what it made (§1.3, §7.8). A
1400+/// Ctrl-C at the terminal stops the git the step runs too, which fails the
1401+/// step itself: `j` may take the signal only after that, and then reports
1402+/// the step's failure.
1403+fn unless_interrupted<T>(done: Result<T, OpenError>) -> Result<T, OpenError> {
1404+ if DEFERRED.load(std::sync::atomic::Ordering::SeqCst) != 0 {
1405+ Err((1, "clone interrupted".to_string()))
1411+/// `commit` with `tree` in place of its own, for the working copy to read:
1412+/// a checkout and a reset read only a commit's tree. It is never written.
1413+fn with_tree(commit: &Commit, tree: MergedTree) -> Commit {
1414+ let (tree_ids, labels) = tree.into_tree_ids_and_labels();
1416+ commit.store().clone(),
1417+ commit.id().clone(),
1418+ Arc::new(jj_lib::backend::Commit {
1419+ root_tree: tree_ids,
1420+ conflict_labels: labels.into_merge(),
1421+ ..commit.store_commit().as_ref().clone()
277#[derive(Clone)]
1426#[derive(Clone)]
278pub struct JjBackend {
1427pub struct JjBackend {
279 inner: Arc<JjInner>,
1428 inner: Arc<JjInner>,
1429+ replay_cache: Rc<ReplayCache>,
282impl JjBackend {
1432impl JjBackend {
@@ -284,7 +1434,7 @@ impl JjBackend {
284 let root = find_repo_root().ok_or_else(|| {
1434 let root = find_repo_root().ok_or_else(|| {
287− "no jj repository found in this directory or any parent".to_string(),
1437+ "no jj repository found in this directory or any parent; run `j init` here, `j clone URL`, or `j help`".to_string(),
290 let settings = user_settings(cfg);
1440 let settings = user_settings(cfg);
@@ -305,10 +1455,22 @@ impl JjBackend {
1458+ // Snapshot and checkout both apply patterns saved by `jj sparse`,
1459+ // independently of our SnapshotOptions. Accepting a partial working
1460+ // copy would silently omit disk edits and skip files during checkout.
1461+ let sparse = workspace.working_copy().sparse_patterns()
1462+ .map_err(|e| (2, format!("cannot read the working copy's sparse patterns: {}", e)))?;
1463+ if !sparse.iter().any(|path| path.is_root()) {
1466+ "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
308 let workspace_name = workspace.workspace_name().to_owned();
1469 let workspace_name = workspace.workspace_name().to_owned();
309 let repo = block_on(workspace.repo_loader().load_at_head())
1470 let repo = block_on(workspace.repo_loader().load_at_head())
310 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
1471 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
311 Ok(JjBackend {
1472 Ok(JjBackend {
1473+ replay_cache: Rc::new(ReplayCache::default()),
312 inner: Arc::new(JjInner {
1474 inner: Arc::new(JjInner {
313 workspace_root: root,
1475 workspace_root: root,
314 workspace: Mutex::new(workspace),
1476 workspace: Mutex::new(workspace),
@@ -317,6 +1479,8 @@ impl JjBackend {
317 visible: Mutex::new(None),
1479 visible: Mutex::new(None),
318 lock_guard: Mutex::new(None),
1480 lock_guard: Mutex::new(None),
319 pending: Mutex::new(None),
1481 pending: Mutex::new(None),
1482+ folds_case: OnceLock::new(),
1483+ long_names: Mutex::new(HashMap::new()),
@@ -336,21 +1500,22 @@ impl JjBackend {
337 Ok(f) => f,
1501 Ok(f) => f,
338 Err(e) => {
1502 Err(e) => {
339− eprintln!("j: cannot create {}: {}", lock_path.display(), e);
1503+ crate::terminal::report_error(format_args!("j: cannot create {}: {}", lock_path.display(), e));
340 std::process::exit(2);
1504 std::process::exit(2);
343 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
1507 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
344 if rc != 0 {
1508 if rc != 0 {
345− eprintln!("j: another j is running in this repository");
1509+ crate::terminal::report_error(format_args!("j: another j is running in this repository"));
346 std::process::exit(2);
1510 std::process::exit(2);
348 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1512 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513+ put_back_left(&self.inner.workspace_root);
351 pub fn build_interp(
1516 pub fn build_interp(
353− cfg: &Config,
1518+ cfg: &mut Config,
354 _text: &str,
1519 _text: &str,
355 snapshot: bool,
1520 snapshot: bool,
356 ) -> Result<(Interp, Value, Value), OpenError> {
1521 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -359,8 +1524,8 @@ impl JjBackend {
359 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
1524 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
360 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
1525 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
361 *self.inner.repo.lock().unwrap() = head_repo;
1526 *self.inner.repo.lock().unwrap() = head_repo;
362− let loaded_visible = block_on(read_visible(&self.current_repo()))?;
1527+ let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
363− let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id)?;
1528+ let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
365 let current = if snapshot {
1530 let current = if snapshot {
366 // §7.4: snapshot the working directory into the wc commit; the
1531 // §7.4: snapshot the working directory into the wc commit; the
@@ -371,19 +1536,19 @@ impl JjBackend {
371 // no working-copy change: the repo is the one already loaded,
1536 // no working-copy change: the repo is the one already loaded,
372 // so reuse its value instead of rebuilding it
1537 // so reuse its value instead of rebuilding it
373 None => {
1538 None => {
374− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1539+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
375 loaded.clone()
1540 loaded.clone()
377 Some(pending) => {
1542 Some(pending) => {
378 *self.inner.pending.lock().unwrap() = Some(pending);
1543 *self.inner.pending.lock().unwrap() = Some(pending);
379 let vis = block_on(read_visible(&post))?;
1544 let vis = block_on(read_visible(&post))?;
380− let cur = build_repo_value(&vis, &vis.wc_change_id)?;
1545+ let cur = build_repo_value(&vis, &vis.wc_change_id, &self.replay_cache)?;
381 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
1546 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
385 } else {
1550 } else {
386− *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1551+ *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
387 loaded.clone()
1552 loaded.clone()
@@ -392,27 +1557,52 @@ impl JjBackend {
392 cfg.shapes.clone(),
1557 cfg.shapes.clone(),
393 crate::value::Env::empty(),
1558 crate::value::Env::empty(),
1560+ // §1.2.5: resolve the config's `@prefix` literals now that the visible
1561+ // ids are known, and before anything evaluates a definition
1562+ if let Some((def, prefix, _)) =
1563+ crate::config::resolve_config_ids(cfg, &interp).into_iter().next()
1568+ "config.j: `@{}` in `{}` does not resolve to a unique commit",
395 // §7.2: if the focused commit is in the immutable set, the snapshot
1573 // §7.2: if the focused commit is in the immutable set, the snapshot
396 // cannot go into it; the focus is a new empty child of it holding the
1574 // cannot go into it; the focus is a new empty child of it holding the
397 // snapshot, and persisting records that child
1575 // snapshot, and persisting records that child
398− match self.focus_if_immutable(&mut interp, cfg, ¤t)? {
1576+ match self.focus_if_immutable(&mut interp, cfg, &loaded, ¤t)? {
399− Some(adjusted) => {
1577+ Some((loaded, current)) => {
400− // the persisted child is part of the loaded value too, so a
1578+ // the snapshot's rewrite of the wc commit (and its rebase of
401− // run that changes nothing is a no-op
1579+ // the descendants) is not the repository's: lookups answer
402− Ok((interp, adjusted.clone(), adjusted))
1580+ // from the stored commits, and persisting leaves the wc
1581+ // commit as stored rather than folding the snapshot into it
1582+ *self.inner.visible.lock().unwrap() = Some(loaded_visible);
1583+ if let Some(p) = self.inner.pending.lock().unwrap().as_mut() {
1584+ p.fold_into_wc = false;
1586+ Ok((interp, loaded, current))
404 None => Ok((interp, loaded, current)),
1588 None => Ok((interp, loaded, current)),
408− /// If the focus of `current` is immutable, return a repo refocused on a
1592+ /// If the focus of `current` is immutable, return the loaded and current
409− /// new empty child holding the focus's files (§7.2).
1593+ /// repos refocused on a new empty child of it (§7.2). Both are `loaded`
1594+ /// with that one child added: the snapshot goes into neither the focus
1595+ /// nor, through a rebase, its descendants. The child holds the focus's
1596+ /// stored files in the loaded repo and the snapshot in the current one,
1597+ /// so a dirty `j id` differs from the loaded value and records the child
410 fn focus_if_immutable(
1599 fn focus_if_immutable(
412 interp: &mut Interp,
1601 interp: &mut Interp,
413 cfg: &Config,
1602 cfg: &Config,
414 current: &Value,
1604 current: &Value,
415− ) -> Result<Option<Value>, OpenError> {
1605+ ) -> Result<Option<(Value, Value)>, OpenError> {
416 // Evaluate the config to compute the immutable set. This uses the
1606 // Evaluate the config to compute the immutable set. This uses the
417 // interpreter the run will go on to use, rather than a throwaway one,
1607 // interpreter the run will go on to use, rather than a throwaway one,
418 // so that the `immutable` (and the `trunk` inside it) computed here is
1608 // so that the `immutable` (and the `trunk` inside it) computed here is
@@ -428,89 +1618,101 @@ impl JjBackend {
428 if !immutable.contains(&focus_id) {
1618 if !immutable.contains(&focus_id) {
429 return Ok(None);
1619 return Ok(None);
431− // build: new empty child of the focus with the focus's files, minted id
1621+ // the focus's location in `loaded`, pre-snapshot; the child is its
432− let root = current.field("root").map_err(|c| (1, c.msg))?;
1622+ // last child, so every existing child sits to the left of it
433− let files = root.field("files").map_err(|c| (1, c.msg))?;
1623+ let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
434− let child = crate::value::Value::record(&[
1624+ let root = field(loaded, "root")?;
436− ("message", crate::value::Value::text("")),
437− ("labels", crate::value::Value::list(vec![])),
440− crate::value::Value::Id(Rc::new(interp.mint_id())),
443− let child_subtree = crate::value::Value::record(&[
1625+ let frame = Value::record(&[
444− ("root", child.clone()),
1626+ ("left", field(loaded, "children")?),
1627+ ("parent", root.clone()),
445− ("children", crate::value::Value::list(vec![])),
1628+ ("right", Value::list(vec![])),
447− let mut new_children: Vec<Value> = current
1630+ let mut frames = vec![frame];
448− .field("children")
1631+ frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
449− .map_err(|c| (1, c.msg))?
1632+ let context = Value::list(frames);
450− .as_list()
1633+ let id = Value::Id(Rc::new(interp.mint_id()));
451− .map_err(|c| (1, c.msg))?
1634+ let at_child = |files: Value| {
452− .to_vec();
1635+ Value::record(&[
453− new_children.push(child_subtree);
1636+ ("children", Value::list(vec![])),
454− let parent = crate::value::Value::record(&[
1637+ ("context", context.clone()),
455− ("children", crate::value::Value::list(new_children)),
1638+ (
456− ("context", current.field("context").map_err(|c| (1, c.msg))?),
1639+ "root",
457− ("root", root),
1640+ Value::record(&[
458− ]);
1641+ ("files", files),
459− // refocus onto the child (it is the last child)
1642+ ("id", id.clone()),
460− let child_id = match child.field("id").map_err(|c| (1, c.msg))? {
1643+ ("labels", Value::list(vec![])),
461− Value::Id(i) => i.to_string(),
1644+ ("message", Value::text("")),
462− _ => unreachable!(),
1645+ ]),
464− crate::repo::by_id(&parent, &child_id)
1649+ let current_at_child = at_child(field(&field(current, "root")?, "files")?);
465− .map_err(|c| (1, c.msg))?
1650+ // with nothing snapshotted `current` is `loaded` itself: share one
466− .ok_or_else(|| (1, "internal: cannot refocus".to_string()))
1651+ // value, so a run that changes nothing is a no-op without comparing
467− .map(Some)
1652+ // the whole repository
1653+ let loaded_at_child = match (loaded, current) {
1654+ (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b) => current_at_child.clone(),
1655+ _ => at_child(field(&root, "files")?),
1657+ Ok(Some((loaded_at_child, current_at_child)))
470− /// Snapshot the working copy into the wc commit; returns the repo at the
1660+ /// Snapshot the working directory into the working-copy commit (§7.4);
471− /// snapshot operation (unpublished but written), or the pre-snapshot repo
1661+ /// return the repo at the snapshot's operation, written but not
472− /// if nothing changed.
1662+ /// published, or the repo as loaded when there is nothing to record.
473 async fn snapshot_repo(
1663 async fn snapshot_repo(
475 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
1665 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
476 let mut ws_guard = self.inner.workspace.lock().unwrap();
1666 let mut ws_guard = self.inner.workspace.lock().unwrap();
477− let loader = ws_guard.repo_loader().clone();
1667+ let root = ws_guard.workspace_root().to_owned();
1668+ let saved = SavedState::of(&ws_guard);
478 let mut locked_ws = ws_guard.start_working_copy_mutation()
1669 let mut locked_ws = ws_guard.start_working_copy_mutation()
480 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1671 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
481− let options = SnapshotOptions {
1672+ let old_tree = locked_ws.locked_wc().old_tree().clone();
482− base_ignores: GitIgnoreFile::empty(),
1673+ let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
484− start_tracking_matcher: &EverythingMatcher,
485− force_tracking_matcher: &NothingMatcher,
486− max_new_file_size: u64::MAX,
488− let (new_tree, _stats) = locked_ws
492 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1675 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
493− let changed = new_tree.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids();
1676+ // the repo build_interp loaded at head from the workspace's own
494− if !changed {
1677+ // loader, whose store the scanned tree lives in
495− locked_ws
1678+ let head = self.current_repo();
496− .finish(self.current_repo().operation().id().clone())
1679+ let wc_commit = self.wc_commit(&head).await?;
1680+ let current = locked_ws.locked_wc().old_operation_id() == head.operation().id();
1681+ let tree = match marker_style(head.settings()) {
1682+ Ok(markers) => snapshot_tree(&root, markers, &wc_commit.tree(), &old_tree, current, &scanned).await,
1685+ .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1686+ if tree.tree_ids() == wc_commit.tree().tree_ids() {
1687+ // nothing to record: the directory holds the commit's files, or,
1688+ // where the state is stale, the state's, or changes to them the
1689+ // commit already has, such as what a checkout cut short wrote.
1690+ // What was scanned is saved, at the operation the state is
1691+ // stale since where it still is.
1692+ save_scan(locked_ws, &scanned, &wc_commit.tree(), head.operation().id())
498 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1694 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
499− return Ok((self.current_repo(), None));
1695+ return Ok((head, None));
501− // the snapshotted tree lives in the workspace's store; drive the
1697+ // a directory come to hold the parent's files, as where it reverts
502− // transaction from the workspace's repo loader so stores match
1698+ // the commit's change, is recorded with the parent's tree (§7.4), as
503− let ws_repo = loader
1699+ // persistence writes a commit with its parent's files (§7.5 step 4):
504− .load_at_head()
1700+ // the snapshot builds on the commit's tree, and so could make
505− .await
1701+ // another tree for the parent's files, one jj takes for a change.
506− .map_err(|e| (2, format!("cannot reload the repository: {}", e)))?;
1702+ // Not before the check above, or a commit already holding such a
1703+ // tree would be rewritten by a directory that has not changed. A
1704+ // merge is immutable, so never folded into (§7.2), and its parents'
1705+ // trees are not merged for it
1706+ let tree = match wc_commit.parent_ids() {
1707+ [_] => parents_tree_if_same(head.as_ref(), wc_commit.parent_ids(), tree).await,
507− let mut tx = ws_repo.start_transaction();
1710+ let mut tx = head.start_transaction();
508 tx.set_is_snapshot(true);
1711 tx.set_is_snapshot(true);
509− let wc_commit = self.wc_commit(&ws_repo).await?;
510 let new_wc = tx
1712 let new_wc = tx
511 .repo_mut()
1713 .repo_mut()
512 .rewrite_commit(&wc_commit)
1714 .rewrite_commit(&wc_commit)
513− .set_tree(new_tree)
1715+ .set_tree(tree)
514 .write()
1716 .write()
516 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
1718 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
@@ -527,7 +1729,6 @@ impl JjBackend {
527 .write("snapshot working copy")
1729 .write("snapshot working copy")
529 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
1731 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
530− let op_id = unpublished.operation().id().clone();
531 let new_repo = unpublished.leave_unpublished();
1732 let new_repo = unpublished.leave_unpublished();
532 // Release the lock *without* finishing: `finish` saves the scanned
1733 // Release the lock *without* finishing: `finish` saves the scanned
533 // tree state to disk, which would record the working directory as
1734 // tree state to disk, which would record the working directory as
@@ -535,20 +1736,19 @@ impl JjBackend {
535 // tree. The next run's incremental snapshot would then find nothing
1736 // tree. The next run's incremental snapshot would then find nothing
536 // to do and build the Repo from the stale tree — and a persisting run
1737 // to do and build the Repo from the stale tree — and a persisting run
537 // would check that stale tree back out, destroying the edits. The
1738 // would check that stale tree back out, destroying the edits. The
538− // state is advanced only by `persist`'s checkout, which finishes
1739+ // state is advanced only by `persist`'s checkout, which first scans
1740+ // the directory again and finishes against the operation that
539− // against the operation that actually recorded the tree (§7.4/§7.7).
1741+ // actually recorded the tree (§7.4/§7.7).
540 drop(locked_ws);
1742 drop(locked_ws);
542 let pending = PendingSnapshot {
1743 let pending = PendingSnapshot {
543− pre_repo: self.current_repo(),
1744+ pre_repo: head,
544− new_wc_id: new_wc.id().clone(),
545 tree: new_wc.tree().clone(),
1745 tree: new_wc.tree().clone(),
1746+ fold_into_wc: true,
547 Ok((new_repo, Some(pending)))
1749 Ok((new_repo, Some(pending)))
552 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
1752 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
553 let id = repo
1753 let id = repo
@@ -566,12 +1766,13 @@ impl JjBackend {
566 interp: &mut Interp,
1766 interp: &mut Interp,
567 cfg: &Config,
1767 cfg: &Config,
568 loaded: &Value,
1768 loaded: &Value,
569− _current: &Value,
1769+ current: &Value,
570 new: &Value,
1770 new: &Value,
571 text: &str,
1771 text: &str,
572− ) -> Result<(), Crash> {
1772+ ) -> Result<bool, Crash> {
1773+ // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
573− if crate::value::value_eq(loaded, new)? {
1774+ if crate::repo::same_repo(loaded, new)? {
574− return Ok(());
1775+ return Ok(false);
576 *interp.old_repo.borrow_mut() = Some(loaded.clone());
1777 *interp.old_repo.borrow_mut() = Some(loaded.clone());
577 crate::repo::validate_repo(interp, new)?;
1778 crate::repo::validate_repo(interp, new)?;
@@ -591,7 +1792,7 @@ impl JjBackend {
591 // walk below compares against its output, so the snapshot change and
1792 // walk below compares against its output, so the snapshot change and
592 // the expression's edits become one rewrite
1793 // the expression's edits become one rewrite
593 let mut snapshot_wc: Option<Commit> = None;
1794 let mut snapshot_wc: Option<Commit> = None;
594− if let Some(p) = &pending {
1795+ if let Some(p) = pending.as_ref().filter(|p| p.fold_into_wc) {
595 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
1796 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
596 let c = block_on(
1797 let c = block_on(
597 tx.repo_mut()
1798 tx.repo_mut()
@@ -615,6 +1816,9 @@ impl JjBackend {
615 old_stored = Arc::new(vis);
1816 old_stored = Arc::new(vis);
617 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
1818 let mut written: BTreeMap<String, CommitId> = BTreeMap::new();
1819+ // the stored trees the program's commits were loaded with
1820+ let visible = self.inner.visible.lock().unwrap().clone();
1821+ let loaded_trees = LoadedTrees::new(visible.as_deref(), &[current, new])?;
618 // shared across the whole persist walk so unchanged blobs are inflated once
1822 // shared across the whole persist walk so unchanged blobs are inflated once
619 let cache = BlobCache::default();
1823 let cache = BlobCache::default();
620 let entries = EntryCache::default();
1824 let entries = EntryCache::default();
@@ -638,33 +1842,35 @@ impl JjBackend {
639 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
1843 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
640 .map_err(|e| Crash::new(e.msg))?;
1844 .map_err(|e| Crash::new(e.msg))?;
641− if !crate::value::value_eq(
642− &Value::list(stored_files),
1845+ if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
643− &Value::list(root_v.field("files")?.as_list()?.to_vec()),
645 return Err(Crash::new("persistence: the root commit cannot be changed"));
1846 return Err(Crash::new("persistence: the root commit cannot be changed"));
648 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
1849 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
650 // walk `new` top-down (§7.5 step 4), parents before children
1851 // walk `new` top-down (§7.5 step 4), parents before children
1852+ // a deque, not a Vec: `remove(0)` shifts everything after it, which
1853+ // is quadratic in the number of commits for a wide history
1854+ let mut queue: std::collections::VecDeque<(Value, String)> =
651− let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
1855+ std::collections::VecDeque::new(); // (subtree value, parent change id)
652 for kid in top.field("children")?.as_list()?.iter() {
1856 for kid in top.field("children")?.as_list()?.iter() {
653− queue.push((kid.clone(), ROOT_ID.to_string()));
1857+ queue.push_back((kid.clone(), ROOT_ID.to_string()));
655− while !queue.is_empty() {
656− let (subtree, parent_change) = queue.remove(0);
1859+ while let Some((subtree, parent_change)) = queue.pop_front() {
657 let commit_v = subtree.field("root")?;
1860 let commit_v = subtree.field("root")?;
658 let id = commit_id_of(&commit_v)?;
1861 let id = commit_id_of(&commit_v)?;
659 let parent_jj = written
1862 let parent_jj = written
660 .get(&parent_change)
1863 .get(&parent_change)
661 .cloned()
1864 .cloned()
662 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
1865 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
663− 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)?;
664 let message = commit_v.field("message")?.as_text()?.to_string();
1869 let message = commit_v.field("message")?.as_text()?.to_string();
665 let new_id = match old_stored.commits.get(&id) {
1870 let new_id = match old_stored.commits.get(&id) {
666 None => {
1871 None => {
667− 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))?;
668 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
1874 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
669 Crash::new(format!("persistence: `{}` is not a valid change id", id))
1875 Crash::new(format!("persistence: `{}` is not a valid change id", id))
@@ -683,17 +1889,31 @@ impl JjBackend {
683 Some(stored) => {
1889 Some(stored) => {
684 let stored_commit = &stored.commit;
1890 let stored_commit = &stored.commit;
685 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
1891 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
1892+ let stored_tree = stored_commit.tree();
1893+ // loaded from the stored tree, the files are unchanged:
1894+ // reading every commit's to compare them made each
1895+ // persist read the files of the whole history
686− let files_changed = {
1896+ let files_changed = crate::repo::stored_tree(&files_v)
1897+ != Some(tree_name(&stored_tree).as_str())
687− let stored_files =
1899+ let stored_files =
688− block_on(tree_to_files(&store, &stored_commit.tree(), &cache, &entries))?;
1900+ block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
689− !crate::value::value_eq(
1901+ // a tree has no order: entries listed differently
1902+ // are not a change to rewrite (§7.3)
690− &Value::list(stored_files),
1903+ !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
691− &Value::list(files_v.as_list()?.to_vec()),
694 let msg_changed = stored_commit.description() != message;
1905 let msg_changed = stored_commit.description() != message;
695 if parent_changed || files_changed || msg_changed {
1906 if parent_changed || files_changed || msg_changed {
696− let tree = block_on(build_tree(&store, &files_v))?;
1907+ // files the stored tree lists are written as it is,
1908+ // but where they are the parent's, as the files of a
1909+ // commit moved onto another tree for them come to be,
1910+ // with the parent's tree (§7.5 step 4)
1911+ let parents = std::slice::from_ref(&parent_jj);
1912+ let tree = if files_changed {
1913+ loaded_trees.tree_to_write(tx.repo(), &files_v, parents)?
1915+ block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
697 let c = block_on(
1917 let c = block_on(
698 tx.repo_mut()
1918 tx.repo_mut()
699 .rewrite_commit(stored_commit)
1919 .rewrite_commit(stored_commit)
@@ -712,7 +1932,7 @@ impl JjBackend {
713 written.insert(id.clone(), new_id);
1933 written.insert(id.clone(), new_id);
714 for kid in subtree.field("children")?.as_list()?.iter() {
1934 for kid in subtree.field("children")?.as_list()?.iter() {
715− queue.push((kid.clone(), id.clone()));
1935+ queue.push_back((kid.clone(), id.clone()));
@@ -756,37 +1976,230 @@ impl JjBackend {
756 let desc = truncate_chars(text, 200);
1976 let desc = truncate_chars(text, 200);
757 let unpublished = block_on(tx.write(desc))
1977 let unpublished = block_on(tx.write(desc))
758 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
1978 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
759− let op_id = unpublished.operation().id().clone();
760− let _new_repo = block_on(unpublished.publish())
761− .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
763− // §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
764 let focus_commit = block_on(store.get_commit_async(&focus_jj))
1981 let focus_commit = block_on(store.get_commit_async(&focus_jj))
765 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
1982 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
1983+ // the checkout starts from what the working directory holds: the
1984+ // tree the run's snapshot scanned, or, where it had nothing to
1985+ // record, the tree the working-copy state records, as
1986+ // `snapshot_repo` left it. That is the focus as loaded unless the
1987+ // state is stale (§7.4), when the checkout writes the focus over it;
1988+ // taking the directory to hold the focus instead, it would find it
1989+ // did not, describe the focus without writing it (a reset), and the
1990+ // next run would record the stale files into the focus.
1991+ let on_disk = match pending {
1992+ Some(p) => p.scanned,
1994+ let state = self.inner.workspace.lock().unwrap().working_copy().tree().cloned();
1995+ // a state that cannot be read fails the checkout, which
1996+ // reads it again when it locks
1997+ state.or_else(|_| block_on(self.wc_commit(&base)).map(|c| c.tree()).map_err(|e| Crash::new(e.1)))?
766− block_on(self.checkout(&focus_commit, op_id))?;
2000+ block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
768− Ok(())
2001+ Ok(true)
2004+ /// Check `commit` out: write its files to the working directory and
2005+ /// save the working copy's state as holding them (§7.4). `op`, the
2006+ /// operation that makes it the working-copy commit, is published first,
2007+ /// as jj does (§7.5 step 7); with none, `commit` is the head's own.
2008+ /// `on_disk` is the tree the directory holds, which the checkout starts
2009+ /// from: what a persisting run's snapshot scanned, or the tree the
2010+ /// working-copy state records where it scanned nothing to record, or
2011+ /// what `undo`'s look found; with none, the state as it is. A change
2012+ /// made to the directory since `on_disk` was scanned, where the
2013+ /// checkout writes, refuses the checkout before `op` is published
2014+ /// (`written_over`); one made once it has begun is written over, as by
2015+ /// jj. Where the commit adds a file, something `on_disk` does not track
2016+ /// standing there, but for empty directories and what the checkout
2017+ /// writes, refuses the checkout before `op` is published
2018+ /// (`look_where_added`), and one put there since fails it after, as it
2019+ /// makes jj skip the path. A checkout that fails, or is cut short, after
2020+ /// `op` is published leaves `op` recorded and the state stale, recording
2021+ /// `on_disk`: the next run takes what the checkout wrote for the
2022+ /// commit's own and carries on (`snapshot_tree`), as the crash says,
2023+ /// with `back` naming the way back; with no `back` it says nothing of
2024+ /// the operation (a clone that fails removes it).
771− async fn checkout(&self, commit: &Commit, op_id: OperationId) -> Result<(), Crash> {
2025+ async fn checkout(
2028+ on_disk: Option<&MergedTree>,
2029+ op: Option<UnpublishedOperation>,
2030+ back: Option<&str>,
2031+ ) -> Result<(), Crash> {
772 let mut ws_guard = self.inner.workspace.lock().unwrap();
2032 let mut ws_guard = self.inner.workspace.lock().unwrap();
2033+ let root = ws_guard.workspace_root().to_owned();
2034+ let saved = SavedState::of(&ws_guard);
773 let mut locked_ws = ws_guard.start_working_copy_mutation()
2035 let mut locked_ws = ws_guard.start_working_copy_mutation()
775 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
2037 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
776− locked_ws
2038+ // A persisting run's snapshot released its lock without saving what
777− .locked_wc()
2039+ // it scanned, so the state just loaded may still describe the tree
778− .check_out(commit)
2040+ // recorded before the edits, and jj checks out by diffing that tree
2041+ // against the new one: a path whose recorded content equals the
2042+ // focus's would keep the user's edit, a file created since would
2043+ // stay, and a file replaced by a directory (or back) would leave
2044+ // state contradicting the tree. Scan the directory again first, as
2045+ // the snapshot did, so the checkout diffs from what is actually on
2046+ // disk (§7.4). A scan keeps what the state knows of each file where
2047+ // a reset to the snapshot's tree would forget it: a conflict whose
2048+ // sides hold marker-like lines is materialized with longer markers,
2049+ // and a forgotten length makes the next snapshot read those markers
2050+ // back at 7 characters, fail to parse them and record the edited
2051+ // conflict as resolved text, markers and all (§7.4).
2052+ if let Some(on_disk) = on_disk {
2053+ let old_tree = locked_ws.locked_wc().old_tree().clone();
2054+ let reset = match scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()).await {
2055+ Ok(scanned) if scanned.tree_ids() == on_disk.tree_ids() => false,
2056+ // the directory changed while the program ran: where the
2057+ // checkout would write the focus over such a change, which
2058+ // no snapshot holds and jj's checkout does not look for,
2059+ // the run crashes before anything is recorded or written
2061+ Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
2063+ Ok(Some(path)) => {
2064+ return Err(Crash::new(format!(
2065+ "cannot check out the focus: `{}` changed in the working directory while the program ran, and the checkout would write over it; nothing was recorded, so run the command again",
2066+ path.as_internal_file_string()
2069+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2071+ // or it can no longer be scanned (an entry appeared,
2072+ // vanished or cannot be read; a failed scan leaves the state
2076+ // Otherwise the state describes `on_disk` instead. A reset
2077+ // writes no file; the paths it changes are re-read by the next
2082+ .reset(&with_tree(commit, on_disk.clone()))
2084+ .map_err(|e| Crash::new(format!("cannot reset the working copy: {}", e)))?;
2087+ // Where the focus adds a file and something the directory does not
2088+ // track stands (an ignored file, a symlink, a directory holding
2089+ // one), jj's checkout would skip the path, and the next run read
2090+ // what stands there into the focus: refuse before anything is
2091+ // recorded or written, as git does (§7.5 step 7). The trees of
2092+ // empty directories found there, and the files holding what the
2093+ // checkout writes, are cleared once `op` is published.
2094+ let markers = marker_style(self.current_repo().settings())
780 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
2095 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
781− locked_ws
2096+ let clear = match on_disk {
782− .finish(op_id)
2097+ None => Vec::new(),
2098+ Some(on_disk) => match look_where_added(&root, markers, on_disk, &commit.tree()).await {
2099+ Ok(Ok(clear)) => clear,
2100+ Ok(Err(in_the_way)) => {
2101+ return Err(Crash::new(format!("cannot check out the focus: {}", in_the_way_of(&in_the_way))));
2103+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2106+ // from here until the state is saved, a signal to stop waits
2107+ let _deferral = Deferral::start();
2108+ let published = op.is_some();
2109+ let op_id = match op {
2110+ None => self.current_repo().operation().id().clone(),
784− .map_err(|e| Crash::new(format!("cannot finish the checkout: {}", e)))?;
2115+ .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
2116+ let op_id = repo.operation().id().clone();
2117+ *self.inner.repo.lock().unwrap() = repo;
2121+ // what a crash from here on says: the state is left stale, which the
2122+ // next run carries on from (§7.5 step 7)
2123+ let carry_on = |what: &str| match back {
2124+ None => String::new(),
2125+ Some(back) if published => {
2126+ format!("; the operation is recorded, but {}: the next command carries on from there{}", what, back)
2128+ Some(_) => format!("; {}: the next command carries on from there", what),
2130+ // A checkout cut short leaves the directory holding `on_disk` but
2131+ // where it wrote, and the next run takes what it holds for changes
2132+ // to the state (`snapshot_tree`). So the state records `on_disk`
2133+ // first, at the operation it was saved at, which leaves it stale
2134+ // until the checkout completes: were it left recording the tree
2135+ // before the run's snapshot, the snapshot's changes, now in the
2136+ // operation, would be taken for new ones and replayed onto the
2137+ // focus too. It is saved only once `op` is published: a state
2138+ // recording the snapshot, beside a head that does not, would lose
2140+ if on_disk.is_some_and(|t| t.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids()) {
2141+ let old_op = locked_ws.locked_wc().old_operation_id().clone();
2142+ let saved = match locked_ws.finish(old_op).await {
2143+ Ok(()) => ws_guard.start_working_copy_mutation().await.map_err(|e| e.to_string()),
2144+ Err(e) => Err(e.to_string()),
2146+ locked_ws = match saved {
2147+ Ok(locked) => locked,
2149+ let what = "the working directory was not updated";
2150+ return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2154+ // what is moved aside is put back where the checkout does not write,
2155+ // or cuts the write short, once it returns, however it returns
2156+ let mut aside = clear_the_way(&root, markers, &commit.tree(), &clear).await;
2157+ let from = locked_ws.locked_wc().old_tree().clone();
2158+ let what = "the working directory was only partly updated";
2159+ let stats = match locked_ws.locked_wc().check_out(commit).await {
2160+ Ok(stats) => stats,
2161+ Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}{}", e, carry_on(what)))),
2163+ // it cut no write short, so a start of a file moved aside, standing
2164+ // at its path, was put there meanwhile
2165+ aside.returned = true;
2166+ // Something the directory does not track, put where the focus adds a
2167+ // file since it was looked at, or a file holding what the checkout
2168+ // writes there that could not be moved aside, made jj skip the
2169+ // path, which it records as written. The state is left stale, as by
2170+ // a checkout that fails, so that no run reads what stands there
2172+ if stats.skipped_files > 0 {
2173+ let skipped = first_skipped(&root, markers, &from, &commit.tree()).await.ok().flatten();
2174+ let why = match (skipped, &aside.stuck) {
2175+ (Some(path), _) => format!(
2176+ "`{}` was not written, as something untracked in the working directory was in its way (move it aside)",
2177+ path.as_internal_file_string()
2179+ (None, Some((path, e))) => format!(
2180+ "`{}` was not written, as the untracked file there, holding what it writes, could not be moved out of its way: {}",
2181+ path.as_internal_file_string(),
2184+ (None, None) => format!(
2185+ "{} of its files were not written, as something untracked in the working directory was in their way",
2186+ stats.skipped_files
2189+ return Err(Crash::new(format!("cannot check out the focus: {}{}", why, carry_on(what))));
2191+ // the state names the operation it was checked out at, so it is
2192+ // saved only once that operation is published
2193+ if let Err(e) = locked_ws.finish(op_id).await {
2194+ let what = "the working copy's record of what the directory holds was not updated";
2195+ return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
788 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
2200 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
789− 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))?;
790 Ok(Signature {
2203 Ok(Signature {
@@ -879,6 +2292,7 @@ impl JjBackend {
881 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2294 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295+ let url = &origin_url(url)?;
882 self.take_lock();
2296 self.take_lock();
883 let base = self.current_repo();
2297 let base = self.current_repo();
884 let origin = RemoteName::new("origin");
2298 let origin = RemoteName::new("origin");
@@ -896,7 +2310,17 @@ impl JjBackend {
896 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
2310 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
2313+ /// Run from the top of the workspace, as git runs from the top of its
2314+ /// working tree: git resolves a relative local URL (one stored by git,
2315+ /// or by `j` before §7.8 resolved them) against the directory it runs
2316+ /// in, and jj-lib's git and ours inherit this process's
2317+ fn enter_workspace_root(&self) -> Result<(), OpenError> {
2318+ std::env::set_current_dir(&self.inner.workspace_root)
2319+ .map_err(|e| (1, format!("cannot enter {}: {}", self.inner.workspace_root.display(), e)))
899 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2322 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323+ self.enter_workspace_root()?;
900 let origin = RemoteName::new("origin");
2324 let origin = RemoteName::new("origin");
901 let base = self.head_repo()?;
2325 let base = self.head_repo()?;
902 let mut tx = base.start_transaction();
2326 let mut tx = base.start_transaction();
@@ -909,27 +2333,52 @@ impl JjBackend {
909 let _ = block_on(self.publish_tx(tx, "fetch"))?;
2333 let _ = block_on(self.publish_tx(tx, "fetch"))?;
912− 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()?;
913 let origin = RemoteName::new("origin");
2338 let origin = RemoteName::new("origin");
914 let base = self.head_repo()?;
2339 let base = self.head_repo()?;
915− let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
2340+ let (value, vis, immutable) = self.eval_push_expr(cfg, expr_text, &base)?;
916 let records = parse_push_records(&value, &vis, &base)?;
2341 let records = parse_push_records(&value, &vis, &base)?;
2342+ if records.is_empty() {
2343+ return Ok(PushOutcome::default());
917− check_push_records(&records, &vis, &base)?;
2345+ let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
918− let _ = self.push_to_origin(&base, origin, &records, expr_text)?;
2346+ let mut outcome = PushOutcome::default();
919− Ok(())
2347+ for (name, target) in &records {
2348+ let before = base.view()
2349+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2350+ .target.as_resolved().cloned().unwrap_or(None);
2351+ if &before == target {
2352+ outcome.unchanged += 1;
2353+ } else if target.is_none() {
2354+ outcome.deleted += 1;
2356+ outcome.published += 1;
2359+ let (_, clean) = self.inspect_working_copy(&base, false)?;
2361+ crate::terminal::report(format_args!(
2362+ "j: warning: working copy has unrecorded changes; push sends recorded content only, so these edits will not be sent. Run `j id` to record them, then push again to include them."
2365+ let description = format!("push {}", expr_text);
2366+ let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
922 /// evaluate `EXPR` against the recorded repository, without snapshotting
2370 /// evaluate `EXPR` against the recorded repository, without snapshotting
923− /// (§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)
924 fn eval_push_expr(
2373 fn eval_push_expr(
926− cfg: &Config,
2375+ cfg: &mut Config,
927 expr_text: &str,
2376 expr_text: &str,
928 base: &Arc<ReadonlyRepo>,
2377 base: &Arc<ReadonlyRepo>,
929− ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
2378+ ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
930 let outer = Rc::new(cfg.global_names.clone());
2379 let outer = Rc::new(cfg.global_names.clone());
931 let expr = crate::parse::parse_expr(expr_text, outer)
2380 let expr = crate::parse::parse_expr(expr_text, outer)
932− .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)))?;
933 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
2382 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
934 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
2383 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
935 let (prefix, candidates) = &failures[0];
2384 let (prefix, candidates) = &failures[0];
@@ -940,15 +2389,30 @@ impl JjBackend {
942 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
2391 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
2392+ // the set a push may not move a bookmark out of (§7.6); build_interp
2393+ // evaluated it for this value to place the focus, so it is cached
2394+ let immutable = crate::repo::compute_immutable(&mut interp, ¤t)
2395+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2396+ let expression_error = |crash: Crash| {
2397+ let mut message = format!("crash: {}", crash.msg);
2399+ Some(name) => message.push_str(&format!("\n in {}, from push {}", name, expr_text)),
2400+ None => message.push_str(&format!("\n from push {}", expr_text)),
2402+ if let Some(hint) = crate::diagnostics::push_expression_hint(expr_text, cfg, &crash) {
2403+ message.push_str(&format!("\n {}", hint));
943 let env = interp.global_env();
2407 let env = interp.global_env();
944 let expr_rc = Rc::new(expr);
2408 let expr_rc = Rc::new(expr);
945 let mut v = interp
2409 let mut v = interp
946 .eval(&expr_rc, &env)
2410 .eval(&expr_rc, &env)
947− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2411+ .map_err(&expression_error)?;
948 if matches!(v, Value::Fun(_)) {
2412 if matches!(v, Value::Fun(_)) {
949 v = interp
2413 v = interp
950 .apply(v, current)
2414 .apply(v, current)
951− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2415+ .map_err(expression_error)?;
953 let _ = base;
2417 let _ = base;
954 let vis = self
2418 let vis = self
@@ -958,19 +2422,28 @@ impl JjBackend {
958 .unwrap()
2422 .unwrap()
959 .clone()
2423 .clone()
960 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
2424 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
961− Ok((v, vis))
2425+ Ok((v, vis, immutable))
964 /// push the updates to `origin` and record the new remote-bookmark
2428 /// push the updates to `origin` and record the new remote-bookmark
965− /// 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
966 fn push_to_origin(
2432 fn push_to_origin(
968 base: &Arc<ReadonlyRepo>,
2434 base: &Arc<ReadonlyRepo>,
969 origin: &RemoteName,
2435 origin: &RemoteName,
970 records: &[(String, Option<CommitId>)],
2436 records: &[(String, Option<CommitId>)],
2437+ deletes_wait: bool,
971 description: &str,
2438 description: &str,
972 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
2439 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
2440+ // Never hand an empty refspec list to git, whose push.default could
2441+ // select branches the expression did not request.
2442+ if records.is_empty() {
2443+ return Ok(base.clone());
973 let mut targets = jj_lib::git::GitPushRefTargets::default();
2445 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446+ let mut deletes = jj_lib::git::GitPushRefTargets::default();
974 for (name, after) in records {
2447 for (name, after) in records {
975 let before = base
2448 let before = base
@@ -979,48 +2452,116 @@ impl JjBackend {
979 .as_resolved()
2452 .as_resolved()
980 .cloned()
2453 .cloned()
981 .unwrap_or(None);
2454 .unwrap_or(None);
982− 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 };
983 .bookmarks
2459 .bookmarks
984 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
2460 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
986 let mut tx = base.start_transaction();
2462 let mut tx = base.start_transaction();
987 let subprocess_options = self.git_subprocess_options()?;
2463 let subprocess_options = self.git_subprocess_options()?;
988 let mut callback = QuietGitCallback;
2464 let mut callback = QuietGitCallback;
2465+ let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
989− let stats = jj_lib::git::push_refs(
2466+ jj_lib::git::push_refs(
990− tx.repo_mut(),
2467+ tx.repo_mut(),
991− subprocess_options,
2468+ subprocess_options.clone(),
992− origin,
2469+ origin,
993− &targets,
2470+ targets,
994− &mut callback,
2471+ &mut callback,
995− &jj_lib::git::GitPushOptions::default(),
2472+ &jj_lib::git::GitPushOptions::default(),
997− .map_err(|e| match e {
2474+ .map_err(|e| match e {
998− jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2475+ jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
999− other => (1, format!("push failed: {}", other)),
2476+ other => (1, format!("push failed: {}", other)),
2479+ let mut stats = push(&mut tx, &targets)?;
2480+ // git push is not atomic: the remote may accept some updates and
2481+ // reject others. When only a bookmark this push sets keeps a deleted
2482+ // bookmark's immutable commit reachable, the deletes go in a second
2483+ // push once the remote has accepted every update, and are not sent
2484+ // otherwise (§7.6)
2485+ let mut not_sent: Vec<String> = Vec::new();
2486+ let mut failed = None;
2487+ if !deletes.bookmarks.is_empty() {
2488+ if stats.all_ok() {
2489+ match push(&mut tx, &deletes) {
2491+ stats.pushed.extend(more.pushed);
2492+ stats.rejected.extend(more.rejected);
2493+ stats.remote_rejected.extend(more.remote_rejected);
2494+ stats.unexported_bookmarks.extend(more.unexported_bookmarks);
2496+ Err(e) => failed = Some(e),
2499+ for (name, _) in &deletes.bookmarks {
2500+ not_sent.push(format!("refs/heads/{}", name.as_str()));
2504+ // push_refs has set the accepted updates in `tx`, and they are on the
2505+ // remote, so they are recorded before the rest is reported; a push
2506+ // the remote refused entirely records nothing
2507+ let mut repo = base.clone();
2508+ if stats.all_ok() || stats.some_exported() {
2509+ let unpublished = block_on(tx.write(truncate_chars(description, 200)))
2510+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2511+ repo = block_on(unpublished.publish())
2512+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2513+ *self.inner.repo.lock().unwrap() = repo.clone();
2515+ if let Some((code, msg)) = failed {
2516+ return Err((code, format!("{}; the other bookmarks were pushed and recorded", msg)));
1001 if !stats.all_ok() {
2518 if !stats.all_ok() {
1002 let mut names: Vec<String> = Vec::new();
2519 let mut names: Vec<String> = Vec::new();
1003 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
2520 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
1004 names.push(name.as_str().to_string());
2521 names.push(name.as_str().to_string());
2523+ // pushed, but not exported to the colocated git repo, so jj-lib
2524+ // left them out of `tx`
2525+ for (symbol, _) in &stats.unexported_bookmarks {
2526+ names.push(format!("{} (pushed but not recorded)", symbol.name.as_str()));
2528+ let mut msg = format!("push rejected: {}", names.join(", "));
2529+ if !not_sent.is_empty() {
2530+ msg.push_str(&format!("; deletes not sent: {}", not_sent.join(", ")));
2532+ if stats.some_exported() {
2533+ msg.push_str("; the other bookmarks were pushed and recorded");
1006− return Err((1, format!("push rejected: {}", names.join(", "))));
2535+ return Err((1, msg));
1008− let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1009− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1010− let repo = block_on(unpublished.publish())
1011− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1012− *self.inner.repo.lock().unwrap() = repo.clone();
1013 Ok(repo)
2537 Ok(repo)
1016− pub fn cmd_undo(&self, redo: bool) -> Result<(), OpenError> {
2540+ pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
1017 let base = self.head_repo()?;
2541 let base = self.head_repo()?;
1018− self.check_wc_clean(&base)?;
2542+ let on_disk = self.check_wc_clean(&base)?;
1019 let loader = self.repo_loader();
2543 let loader = self.repo_loader();
1020 let mut cur = base.operation().clone();
2544 let mut cur = base.operation().clone();
1021 let target_op_id: OperationId;
2545 let target_op_id: OperationId;
1023− match (op_marker(cur.metadata()), redo) {
2547+ let marker = op_marker(cur.metadata());
2548+ if let Some(m) = marker.as_ref().filter(|m| m.restores_view) {
2549+ // jj 0.45 names the operation whose view was restored.
2550+ // Repeating the same direction follows that target; reversing
2551+ // direction restores the marker's parent. The target may
2552+ // itself be an undo or redo, not just a normal operation.
2553+ if (m.kind == "redo") == redo {
2554+ cur = block_on(loader.load_operation(&m.id))
2555+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2558+ let Some(parent) = cur.parent_ids().first().cloned() else {
2559+ return Err((1, if redo { "nothing to redo" } else { "nothing to undo" }.to_string()));
2561+ target_op_id = parent;
2564+ match (marker, redo) {
1024 (None, false) => {
2565 (None, false) => {
1025 let Some(parent) = cur.parent_ids().first().cloned() else {
2566 let Some(parent) = cur.parent_ids().first().cloned() else {
1026 return Err((1, "nothing to undo".to_string()));
2567 return Err((1, "nothing to undo".to_string()));
@@ -1046,7 +2587,16 @@ impl JjBackend {
1046 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2587 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1048 (Some(m), true) if m.kind == "undo" => {
2589 (Some(m), true) if m.kind == "undo" => {
2590+ // An old j marker names the discarded normal operation,
2591+ // but its parent has the same view and retains any earlier
2592+ // undos. Modern redo markers must keep that navigation
2593+ // point so another redo can continue across the upgrade.
1049− target_op_id = m.id;
2594+ target_op_id = if m.authored_by_j {
2595+ cur.parent_ids().first().cloned()
2596+ .ok_or_else(|| (1, "nothing to redo".to_string()))?
1052 (Some(m), false) => {
2602 (Some(m), false) => {
@@ -1091,8 +2641,29 @@ impl JjBackend {
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()));
1094− let target_op = block_on(loader.load_operation(&target_op_id))
2651+ let mut target_op = block_on(loader.load_operation(&target_op_id))
1095 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2652 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2653+ // jj also follows markers in the selected direction at the target:
2654+ // undoing a redo whose parent is an undo restores that undo's target,
2655+ // and conversely for redo. Keep the same canonical navigation point.
2656+ while let Some(m) = op_marker(target_op.metadata())
2657+ .filter(|m| m.restores_view && (m.kind == "redo") == redo)
2659+ target_op = block_on(loader.load_operation(&m.id))
2660+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2662+ let target_op_id = target_op.id().clone();
2663+ // Report the expression whose state is being reversed or restored,
2664+ // following restore markers left by repeated undo/redo commands.
2665+ let affected = if redo { &target_op } else { &cur };
2666+ let affected_description = restored_operation_description(&loader, affected);
1096 let target_view = block_on(
2667 let target_view = block_on(
1098 .op_store()
2669 .op_store()
@@ -1116,65 +2687,94 @@ impl JjBackend {
2690+ // Use jj's current marker so either CLI can continue this history.
2691+ // The older j and jj marker formats remain readable above.
1119− let marker = if redo {
2692+ let marker = format!(
1120− format!("redo: {}", cur.id().hex())
2693+ "{}: restore to operation {}",
1121− } else {
2694+ if redo { "redo" } else { "undo" },
1122− format!("undo: {}", cur.id().hex())
2695+ target_op_id.hex()
1124 let mut tx = base.start_transaction();
2697 let mut tx = base.start_transaction();
1125 tx.repo_mut().set_view(target_view.clone());
2698 tx.repo_mut().set_view(target_view.clone());
1126 let unpublished = block_on(tx.write(&marker))
2699 let unpublished = block_on(tx.write(&marker))
1127 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2700 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1128− let op_id = unpublished.operation().id().clone();
2701+ // the operation is published, then the restored focus checked out
1129− let repo = block_on(unpublished.publish())
2702+ // from what the directory holds (§7.7)
2703+ let wc_id = &target_view.wc_commit_ids[WorkspaceName::DEFAULT];
2704+ let wc_commit = block_on(base.store().get_commit_async(wc_id))
1130− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2705+ .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
1131− *self.inner.repo.lock().unwrap() = repo.clone();
2706+ let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
1132− let wc_commit = block_on(self.wc_commit(&repo))?;
1133− block_on(self.checkout(&wc_commit, op_id)).map_err(|c| (1, c.msg))?;
2707+ block_on(self.checkout(&wc_commit, Some(&on_disk), Some(unpublished), Some(back)))
2708+ .map_err(|c| (1, c.msg))?;
2709+ let focus_message = wc_commit.description().lines().next().unwrap_or("").trim();
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 }),
1137− /// refuse if the working directory differs from the focused commit's
2719+ /// Refuse, as `undo` and `redo` do before anything else (§7.7), when the
1138− /// files (§7.7): reserved commands never snapshot, so compare the locked
2720+ /// working directory holds changes the working-copy commit does not:
1139− /// working copy's recorded tree with the focus commit's tree
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.
1140− fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<(), OpenError> {
2729+ fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<MergedTree, OpenError> {
2730+ let (scanned, clean) = self.inspect_working_copy(base, true)?;
1141− let dirty = (1, "working copy has changes not in @; run `j id` to record them or discard them".to_string());
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()));
1142− let wc_commit = block_on(self.wc_commit(base))?;
2733+ }
1143− // snapshot the working directory (without persisting anything) and
2734+ Ok(scanned)
1144− // compare against the focused commit's files (§7.7)
2735+ }
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(
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();
1145 let mut ws_guard = self.inner.workspace.lock().unwrap();
2746 let mut ws_guard = self.inner.workspace.lock().unwrap();
2747+ let root = ws_guard.workspace_root().to_owned();
2748+ let saved = SavedState::of(&ws_guard);
1146 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
2749 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
1147 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
2750 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1148− let options = SnapshotOptions {
2751+ let old_tree = locked_ws.locked_wc().old_tree().clone();
1149− base_ignores: GitIgnoreFile::empty(),
1151− start_tracking_matcher: &EverythingMatcher,
1152− force_tracking_matcher: &NothingMatcher,
1153− max_new_file_size: u64::MAX,
1155− let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
2752+ let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
1156 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2753 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1157− let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
2754+ let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
1158− if same {
2755+ let recorded = match marker_style(base.settings()) {
1159− // the scanned tree is the one already recorded: saving the state
2756+ Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
1160− // only refreshes the mtime cache
2757+ Err(e) => Err(e),
1161− block_on(locked_ws.finish(base.operation().id().clone()))
2758+ }
1162− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
2759+ .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
1163− } else {
2760+ if recorded.tree_ids() != wc_tree.tree_ids() {
1164− // the working copy differs from @ and this command records
1165− // nothing: drop the lock without saving, or the state would claim
2761+ // drop the lock without saving, or the state would claim the
1166− // the directory was snapshotted and hide the changes from the next
2762+ // directory was snapshotted and hide the changes from the next
1167 // run (§7.7)
2763 // run (§7.7)
1168− drop(locked_ws);
2764+ return Ok((scanned, false));
1170− drop(ws_guard);
2766+ // saving the state only refreshes what it knows of each file, or
1171− if !same {
2767+ // takes in what the commit already has
1172− return Err(dirty);
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)))?;
1174− Ok(())
2772+ Ok((scanned, true))
2775+ /// The operation log for `ops` (§7.7), one line per operation, newest
2776+ /// first; the caller writes it to stdout.
1177− pub fn cmd_ops(&self) -> Result<(), OpenError> {
2777+ pub fn cmd_ops(&self) -> Result<String, OpenError> {
1178 let base = self.head_repo()?;
2778 let base = self.head_repo()?;
1179 let loader = self.repo_loader();
2779 let loader = self.repo_loader();
1180 let head = base.operation().clone();
2780 let head = base.operation().clone();
@@ -1187,8 +2787,8 @@ impl JjBackend {
1187 let mut tag = "";
2787 let mut tag = "";
1188 match op_kind(op.metadata()) {
2788 match op_kind(op.metadata()) {
1189 OpKind::Normal => {}
2789 OpKind::Normal => {}
1190− OpKind::Undo { .. } => tag = "undo",
2790+ OpKind::Undo => tag = "undo",
1191− OpKind::Redo { .. } => tag = "redo",
2791+ OpKind::Redo => tag = "redo",
1193 if desc.is_empty() {
2793 if desc.is_empty() {
1194 desc = "(no description)".to_string();
2794 desc = "(no description)".to_string();
@@ -1211,8 +2811,7 @@ impl JjBackend {
1211 None => None,
2811 None => None,
1215− Ok(())
2814+ Ok(out)
@@ -1249,6 +2848,7 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
1250 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
2849 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1251 let backend = JjBackend {
2850 let backend = JjBackend {
2851+ replay_cache: Rc::new(ReplayCache::default()),
1252 inner: Arc::new(JjInner {
2852 inner: Arc::new(JjInner {
1253 workspace_root: cwd.clone(),
2853 workspace_root: cwd.clone(),
1254 workspace: Mutex::new(workspace),
2854 workspace: Mutex::new(workspace),
@@ -1257,23 +2857,43 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
1257 visible: Mutex::new(None),
2857 visible: Mutex::new(None),
1258 lock_guard: Mutex::new(None),
2858 lock_guard: Mutex::new(None),
1259 pending: Mutex::new(None),
2859 pending: Mutex::new(None),
2860+ folds_case: OnceLock::new(),
2861+ long_names: Mutex::new(HashMap::new()),
2864+ // the import and the working-copy commit are one operation, the one
2865+ // undo never undoes (§7.7)
2866+ let mut tx = backend.current_repo().start_transaction();
1262 if had_git {
2867 if had_git {
1263 let import_options = backend.git_import_options()?;
2868 let import_options = backend.git_import_options()?;
1264− let mut tx = backend.current_repo().start_transaction();
1265 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
2869 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1266 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2870 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2871+ // HEAD too: it is the head the working-copy commit starts on, and a
2872+ // detached HEAD's commits are reachable from no ref
2873+ block_on(jj_lib::git::import_head(
2875+ &backend.inner.workspace_name,
2878+ .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
1267 block_on(tx.repo_mut().rebase_descendants())
2879 block_on(tx.repo_mut().rebase_descendants())
1268 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
2880 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1269− let _ = block_on(backend.publish_tx(tx, "import git refs"))?;
2882+ // §7.8: the current head is git's HEAD (none on an unborn branch)
2886+ .git_head(&backend.inner.workspace_name)
1271− create_initial_wc_commit(&backend, cfg, "init")?;
2890+ create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
1275pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
2894pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
1276 let settings = user_settings_strict(cfg)?;
2895 let settings = user_settings_strict(cfg)?;
2896+ let url = &origin_url(url)?;
1277 let dir_path = PathBuf::from(dir);
2897 let dir_path = PathBuf::from(dir);
1278 if dir_path.join(".jj").is_dir() {
2898 if dir_path.join(".jj").is_dir() {
1279 return Err((2, format!("{} already contains a jj repository", dir)));
2899 return Err((2, format!("{} already contains a jj repository", dir)));
@@ -1287,82 +2907,296 @@ pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
1287 } else if dir_path.exists() {
2907 } else if dir_path.exists() {
1288 return Err((2, format!("{} already exists and is not empty", dir)));
2908 return Err((2, format!("{} already exists and is not empty", dir)));
1290− std::fs::create_dir_all(&dir_path)
2910+ let existed = dir_path.is_dir();
2911+ // the directory an existing DIR leads to, through a symlink DIR may be:
2912+ // a clone that fails empties it only while DIR still leads there (§7.8)
2913+ let found = if existed { dir_identity(&dir_path, true) } else { None };
2914+ // the directories this run creates, in order, each with its device and
2915+ // inode: a clone that fails removes DIR and those of its parents left
2916+ // empty, or an existing DIR's entries, and nothing else (§7.8).
2917+ // Each is made by `create_dir`, whose success proves it new. The checks
2918+ // above read DIR as written, before its missing parents exist; once
2919+ // they do, `new/..` or `new/../keep` names a directory that was there,
2920+ // so DIR's own `create_dir` failing, AlreadyExists included, refuses it
2921+ // as `git clone` does
2922+ let mut created: Vec<(&std::path::Path, Option<(u64, u64)>)> = Vec::new();
2923+ // those of them a failed clone leaves, as their paths no longer lead
2925+ let mut left: Vec<&std::path::Path> = Vec::new();
2926+ // from here until the clone has removed what it made or completed, a
2927+ // signal to stop waits, and fails a clone that has not yet made its
2928+ // working-copy commit (`unless_interrupted`, §1.3)
2929+ let _held = Deferral::start();
2931+ // DIR and its missing parents, DIR first ("" is the current
2932+ // directory, which exists)
2933+ let missing: Vec<&std::path::Path> = dir_path
2936+ .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
2939+ for p in missing.into_iter().rev() {
2940+ match std::fs::create_dir(p) {
2941+ Ok(()) => created.push((p, dir_identity(p, false))),
2942+ // a parent reached again through `..`
2944+ if e.kind() == std::io::ErrorKind::AlreadyExists
2946+ && p.is_dir() => {}
2948+ for &(p, id) in created.iter().rev() {
2949+ if !remove_created_dir(p, id, false) {
1291− .map_err(|e| (2, format!("cannot create {}: {}", dir, e)))?;
2953+ let msg = format!("cannot create {}: {}", dir, e);
2954+ return Err((2, msg + &left_note(&left)));
2959+ let cloned = clone_into(cfg, &settings, url, &dir_path);
2960+ let Err((code, msg)) = cloned else {
2963+ // like `git clone`, a clone that fails leaves nothing behind: its
2964+ // working-copy commit would claim files the checkout never wrote, and a
2965+ // later clone would refuse the directory (§7.8)
2967+ // DIR, the last one created, goes whole; a parent holds nothing of
2968+ // the clone's but DIR, so it goes only if left empty: what else was
2969+ // put there meanwhile, a sibling clone say, stays
2970+ for (i, &(p, id)) in created.iter().enumerate().rev() {
2971+ if !remove_created_dir(p, id, i + 1 == created.len()) {
2975+ Err((code, msg + &left_note(&left)))
2976+ } else if found.is_some() && dir_identity(&dir_path, true) == found {
2977+ if let Ok(entries) = std::fs::read_dir(&dir_path) {
2978+ for entry in entries.flatten() {
2979+ let _ = match entry.file_type() {
2980+ Ok(t) if t.is_dir() => std::fs::remove_dir_all(entry.path()),
2981+ _ => std::fs::remove_file(entry.path()),
2986+ } else if dir_path.symlink_metadata().is_ok() {
2987+ // a symlink put in place of DIR, or of a directory above it, would
2988+ // have `read_dir` list what that names
2992+ "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
3001+/// The device and inode of the directory at `path`, following a symlink
3002+/// there only when `follow` says so; `None` when `path` holds no directory
3003+fn dir_identity(path: &std::path::Path, follow: bool) -> Option<(u64, u64)> {
3004+ use std::os::unix::fs::MetadataExt;
3005+ let meta = if follow {
3006+ std::fs::metadata(path)
3008+ std::fs::symlink_metadata(path)
3010+ meta.ok().filter(|m| m.is_dir()).map(|m| (m.dev(), m.ino()))
3013+/// Remove the directory a failed clone created at `path`, `whole` or only
3014+/// if empty, while `path` still leads to it: the directory whose device and
3015+/// inode `id` were read on creating it (§7.8). A symlink put in its place,
3016+/// or in place of a directory above it, would have the removal follow it
3017+/// and delete what that names, so a path leading anywhere else is left, and
3018+/// the result is false. One leading nowhere holds nothing to remove.
3019+fn remove_created_dir(path: &std::path::Path, id: Option<(u64, u64)>, whole: bool) -> bool {
3020+ if id.is_some() && dir_identity(path, false) == id {
3022+ std::fs::remove_dir_all(path)
3024+ std::fs::remove_dir(path)
3028+ path.symlink_metadata().is_err()
3032+/// What a failed clone's error adds for the directories it created and
3033+/// `left` (§7.8): nothing when it left none
3034+fn left_note(left: &[&std::path::Path]) -> String {
3035+ let named = |ps: &[&std::path::Path]| {
3036+ ps.iter().map(|p| format!("`{}`", p.display())).collect::<Vec<_>>().join(", ")
3039+ [] => String::new(),
3041+ "; left {}, as it is no longer the directory this clone created",
3044+ [init @ .., last] => format!(
3045+ "; left {} and {}, as they are no longer the directories this clone created",
3052+/// `url` as `origin` holds it (§7.8): a local path resolved against the
3053+/// current directory, as `jj git clone` stores it, for git resolves a
3054+/// relative one against the directory each later `fetch` or `push` runs
3055+/// in; one under a home directory (`~/…`, `~user/…`) and any other URL, a
3056+/// `file://` one included, as given. One that does not parse, or a path
3057+/// that does not resolve to UTF-8, is a usage error.
3058+fn origin_url(url: &str) -> Result<String, OpenError> {
3059+ let mut parsed = gix::url::parse(url).map_err(|e| (2, format!("invalid URL `{}`: {}", url, e)))?;
3060+ // a bare path is a file location in the alternative form
3061+ if parsed.scheme != gix::url::Scheme::File || !parsed.serialize_alternative_form {
3062+ return Ok(url.to_string());
3064+ // git expands a leading `~` to the home directory on every fetch and
3065+ // push, from wherever it runs; resolved here, it would name a directory
3066+ // called `~` (`./~/…` still does)
3067+ if parsed.path.starts_with(b"~") {
3068+ return Ok(url.to_string());
3070+ let cwd = std::env::current_dir()
3071+ .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
3073+ .canonicalize(&cwd)
3074+ .map_err(|e| (2, format!("cannot resolve `{}`: {}", url, e)))?;
3075+ // `add_remote` takes UTF-8, which only the current directory's name or
3076+ // a symlink's target can break; stored as given, the path would reach
3077+ // the remote only from where it was typed
3078+ String::from_utf8(parsed.to_bstring().into())
3079+ .map_err(|_| (2, format!("cannot resolve `{}`: the path it names is not UTF-8", url)))
3082+/// `clone`'s work once `dir_path` exists and is empty (§7.8)
3085+ settings: &UserSettings,
3087+ dir_path: &std::path::Path,
3088+) -> Result<(), OpenError> {
1292 let (workspace, repo) = block_on(Workspace::init_colocated_git(
3089 let (workspace, repo) = block_on(Workspace::init_colocated_git(
1293− &settings,
3090+ settings,
1294− &dir_path,
3091+ dir_path,
1295 gix::hash::Kind::Sha1,
3092 gix::hash::Kind::Sha1,
1297 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
3094 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
1298 let backend = JjBackend {
3095 let backend = JjBackend {
3096+ replay_cache: Rc::new(ReplayCache::default()),
1299 inner: Arc::new(JjInner {
3097 inner: Arc::new(JjInner {
1300− workspace_root: dir_path.clone(),
3098+ workspace_root: dir_path.to_path_buf(),
1301 workspace: Mutex::new(workspace),
3099 workspace: Mutex::new(workspace),
1302 repo: Mutex::new(repo),
3100 repo: Mutex::new(repo),
1303 workspace_name: WorkspaceName::DEFAULT.to_owned(),
3101 workspace_name: WorkspaceName::DEFAULT.to_owned(),
1304 visible: Mutex::new(None),
3102 visible: Mutex::new(None),
1305 lock_guard: Mutex::new(None),
3103 lock_guard: Mutex::new(None),
1306 pending: Mutex::new(None),
3104 pending: Mutex::new(None),
3105+ folds_case: OnceLock::new(),
3106+ long_names: Mutex::new(HashMap::new()),
1309 let origin = RemoteName::new("origin");
3109 let origin = RemoteName::new("origin");
1310 let mut tx = backend.current_repo().start_transaction();
3110 let mut tx = backend.current_repo().start_transaction();
3111+ // a signal to stop fails each step before the working-copy commit's
3113+ unless_interrupted(
1311− jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3114+ jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
1312− .map_err(|e| (1, format!("cannot set the remote: {}", e)))?;
3115+ .map_err(|e| (1, format!("cannot set the remote: {}", e))),
1313− backend.git_fetch_refs(tx.repo_mut(), origin)?;
3117+ unless_interrupted(backend.git_fetch_refs(tx.repo_mut(), origin))?;
3118+ let default_branch = unless_interrupted(git_default_branch(
3119+ git_backend(tx.repo().store())?.git_repo_path(),
1314 let import_options = backend.git_import_options()?;
3122 let import_options = backend.git_import_options()?;
3123+ unless_interrupted(
1315− block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3124+ block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1316− .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
3125+ .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
3127+ unless_interrupted(
1317− block_on(tx.repo_mut().rebase_descendants())
3128+ block_on(tx.repo_mut().rebase_descendants())
1318− .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3129+ .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
1319− let _ = block_on(backend.publish_tx(tx, &format!("clone {}", url)))?;
3130+ )?;
3131+ // §7.8: the default bookmark's target (none when the remote's HEAD names
3132+ // no branch, or a branch it does not have)
3133+ let head = default_branch.and_then(|name| {
3136+ .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3142+ // the fetch and the working-copy commit are one operation, the one undo
3143+ // never undoes (§7.7)
1320− create_initial_wc_commit(&backend, cfg, &format!("clone {}", url))?;
3144+ create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
1324−/// §7.8: after init/clone, create a working-copy commit as a child of the
3148+/// §7.8: after init/clone, create a working-copy commit as a child of
1325−/// current head (a bookmark target if there is one, otherwise the root
3149+/// `head` (the root commit when there is none), record it in `tx` with
1326−/// commit) and check it out.
3150+/// what the command has imported, and check it out. The commit holds its
3151+/// parent's files less any a checkout cannot create (`checkout_tree`), so
3152+/// it is empty unless the parent has one. `adopt` says the working
3153+/// directory already holds a checkout of the parent (init over an existing
3154+/// git repository): the working copy is then reset to the commit instead of
3155+/// written, so no file is touched and what the directory holds beyond the
3156+/// parent is the next snapshot's change. `back` is what a checkout that
3157+/// fails says (`JjBackend::checkout`): none for a clone, which then removes
1327fn create_initial_wc_commit(
3159fn create_initial_wc_commit(
1328 backend: &JjBackend,
3160 backend: &JjBackend,
1329 cfg: &Config,
3161 cfg: &Config,
3162+ mut tx: jj_lib::transaction::Transaction,
1330 description: &str,
3163 description: &str,
3164+ head: Option<CommitId>,
3166+ back: Option<&str>,
1331) -> Result<(), OpenError> {
3167) -> Result<(), OpenError> {
1332− let base = backend.current_repo();
3168+ let store = tx.base_repo().store().clone();
1333− let store = base.store().clone();
1334− let mut parent = store.root_commit_id().clone();
1335− 'outer: for (_name, target) in base.view().local_bookmarks() {
1336− if let Some(id) = target.as_resolved().and_then(|t| t.clone()) {
1341− if parent == *store.root_commit_id() {
3169+ let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
1342− for (_name, remote_ref) in base.view().remote_bookmarks(RemoteName::new("origin")) {
1343− if let Some(id) = remote_ref.target.as_resolved().and_then(|t| t.clone()) {
1349 // jj's own init already created a working-copy commit; if it is already
3170 // jj's own init already created a working-copy commit; if it is already
1350 // a child of the target parent, keep it and just check it out
3171 // a child of the target parent, keep it and just check it out
1351 let mut to_abandon: Option<CommitId> = None;
3172 let mut to_abandon: Option<CommitId> = None;
1352− 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()) {
1353 if existing.parent_ids().first() == Some(&parent) {
3175 if existing.parent_ids().first() == Some(&parent) {
1354− let op_id = base.operation().id().clone();
3176+ // jj's commit is an empty child of the root; record only what
3177+ // was imported, if anything
3178+ let op = if tx.repo().has_changes() {
3180+ block_on(tx.write(truncate_chars(description, 200)))
3181+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
1355− block_on(backend.checkout(&existing, op_id)).map_err(|c| (1, c.msg))?;
3186+ block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
1356 return Ok(());
3187 return Ok(());
1358 if existing.id() != store.root_commit_id() {
3189 if existing.id() != store.root_commit_id() {
1359 to_abandon = Some(existing.id().clone());
3190 to_abandon = Some(existing.id().clone());
3193+ let parent_tree = block_on(store.get_commit_async(&parent))
3194+ .map_err(|e| (1, format!("cannot read the parent commit: {}", e)))?
3196+ let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
1362 let user_sig = backend
3197 let user_sig = backend
1363 .user_signature(cfg)
3198 .user_signature(cfg)
1364 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3199 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1365− let mut tx = base.start_transaction();
1366 if let Some(old_wc) = &to_abandon {
3200 if let Some(old_wc) = &to_abandon {
1367 tx.repo_mut().record_abandoned_commit_with_parents(
3201 tx.repo_mut().record_abandoned_commit_with_parents(
1368 old_wc.clone(),
3202 old_wc.clone(),
@@ -1371,7 +3205,7 @@ fn create_initial_wc_commit(
1372 let wc = block_on(
3206 let wc = block_on(
1373 tx.repo_mut()
3207 tx.repo_mut()
1374− .new_commit(vec![parent], store.empty_merged_tree())
3208+ .new_commit(vec![parent], wc_tree)
1375 .set_author(user_sig.clone())
3209 .set_author(user_sig.clone())
1376 .set_committer(user_sig)
3210 .set_committer(user_sig)
1377 .write(),
3211 .write(),
@@ -1384,19 +3218,49 @@ fn create_initial_wc_commit(
1384 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
3218 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
1385 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
3219 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1386 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
3220 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1387− let op_id = unpublished.operation().id().clone();
3221+ // an adopted directory is reset to the commit, not checked out over:
1388− let repo = block_on(unpublished.publish())
3222+ // checking out from the empty tree jj's init recorded would recreate
1389− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
3223+ // the files the user deleted there. Anything else is written, once the
1390− *backend.inner.repo.lock().unwrap() = repo;
3224+ // operation is published (§7.5 step 7).
3225+ let on_disk = if adopt { Some(wc.tree()) } else { None };
1391− block_on(backend.checkout(&wc, op_id)).map_err(|c| (1, c.msg))?;
3226+ block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
3230+/// `tree` without the files at a path a checkout cannot create (§7.5 step
3231+/// 1), which a git branch can hold: a `.jj` directory committed by mistake,
3232+/// a name the workspace's filesystem does not hold. jj's checkout stops at
3233+/// the first such path, failing the whole clone, so the working-copy commit
3234+/// init or clone starts on leaves them out, and their removal is its change
3236+async fn checkout_tree(tree: MergedTree, backend: &JjBackend) -> Result<MergedTree, OpenError> {
3237+ let folds_case = backend.folds_case();
3238+ let refused: Vec<RepoPathBuf> = tree
3240+ .map(|(path, _)| path)
3242+ path.components().any(|c| {
3243+ crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3247+ if refused.is_empty() {
3250+ let mut builder = MergedTreeBuilder::new(tree);
3251+ for path in refused {
3252+ builder.set_or_remove(path, Merge::absent());
3257+ .map_err(|e| (1, format!("cannot write a tree: {}", e)))
1395/// user settings for init/clone: a missing or malformed `user` is a
3260/// user settings for init/clone: a missing or malformed `user` is a
1396/// configuration error (exit 3) (§7.9)
3261/// configuration error (exit 3) (§7.9)
1397fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3262fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1398− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3263+ let (name, email) = validated_identity(cfg)?;
1399− .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1400 let mut config = StackedConfig::with_defaults();
3264 let mut config = StackedConfig::with_defaults();
1401 let text = format!(
3265 let text = format!(
1402 "[user]\nname = {}\nemail = {}\n",
3266 "[user]\nname = {}\nemail = {}\n",
@@ -1410,6 +3274,39 @@ fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
1410 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
3274 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
3277+/// The identity shown during repository setup. Validation is deliberately
3278+/// confined to authoring boundaries so inspection and recovery still work
3279+/// with an unchanged reference configuration.
3280+pub fn configured_identity(cfg: &Config) -> Result<String, OpenError> {
3281+ let (name, email) = validated_identity(cfg)?;
3282+ Ok(format!("{} <{}>", terminal_text(&name), terminal_text(&email)))
3285+fn terminal_text(text: &str) -> String {
3286+ text.chars().map(|c| {
3287+ if c.is_control() { c.escape_debug().to_string() } else { c.to_string() }
3291+fn validated_identity(cfg: &Config) -> Result<(String, String), OpenError> {
3292+ let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3293+ .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3294+ check_identity(&name, &email)?;
3298+fn check_identity(name: &str, email: &str) -> Result<(), OpenError> {
3299+ if name.trim().is_empty() || email.trim().is_empty()
3300+ || name.trim() == "Your Name" || email.trim().eq_ignore_ascii_case("you@example.com")
3302+ return Err((3, format!(
3303+ "config.j: user identity is not configured ({} <{}>); set `user.name` and `user.email` in the `user` record before creating or recording commits",
3304+ terminal_text(name), terminal_text(email),
1413// ----------------------------------------------------------------------
3310// ----------------------------------------------------------------------
1414// reserved-command helpers
3311// reserved-command helpers
1415// ----------------------------------------------------------------------
3312// ----------------------------------------------------------------------
@@ -1473,41 +3370,72 @@ fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Resul
1473 if out.status.success() {
3370 if out.status.success() {
1475 } else {
3372 } else {
1479− "fetch failed: {}",
3373+ Err((1, format!("fetch failed: {}", git_failure(&out))))
3377+/// What a git that failed said, or how it ended when it said nothing (a
3378+/// Ctrl-C stops it silently)
3379+fn git_failure(out: &std::process::Output) -> String {
3380+ use std::os::unix::process::ExitStatusExt;
1480− String::from_utf8_lossy(&out.stderr).trim()
3381+ let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
1481− ),
3382+ match (said.is_empty(), out.status.signal()) {
1482− ))
3383+ (false, _) => said,
3384+ (true, Some(sig)) => format!("git was stopped by signal {}", sig),
3385+ (true, None) => format!("git exited with status {}", out.status.code().unwrap_or(-1)),
3389+/// the branch the remote's HEAD names, its default bookmark (§7.8), asked of
3390+/// the remote with `git ls-remote --symref` (the git `git_fetch_subprocess`
3391+/// runs); `None` when HEAD names no branch (a detached HEAD)
3392+fn git_default_branch(
3393+ git_dir: &std::path::Path,
3394+ remote: &RemoteName,
3395+) -> Result<Option<String>, OpenError> {
3396+ let exe = std::env::var("J_GIT").unwrap_or_else(|_| "git".to_string());
3397+ let out = std::process::Command::new(exe)
3400+ .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401+ .stdin(std::process::Stdio::null())
3403+ .map_err(|e| (1, format!("cannot run git ls-remote: {}", e)))?;
3404+ if !out.status.success() {
3405+ return Err((1, format!("cannot read the remote's default branch: {}", git_failure(&out))));
3407+ // `ref: refs/heads/<branch>\tHEAD`
3408+ Ok(String::from_utf8_lossy(&out.stdout).lines().find_map(|line| {
3409+ let branch = line.strip_prefix("ref: refs/heads/")?.strip_suffix("\tHEAD")?;
3410+ Some(branch.to_string())
1486enum OpKind {
3414enum OpKind {
1488− /// the operation this undo discarded the view of
3416+ Undo,
1489− Undo { #[allow(dead_code)] undid: OperationId },
3417+ Redo,
1490− /// the undo this redo reversed
1491− Redo { #[allow(dead_code)] redid: OperationId },
1494/// a marker parsed from an operation description (§7.7 kinds)
3420/// a marker parsed from an operation description (§7.7 kinds)
1495struct OpMarker {
3421struct OpMarker {
1496 kind: &'static str,
3422 kind: &'static str,
1497 id: OperationId,
3423 id: OperationId,
1498− /// whether the description is j's own exact marker (`undo: <id>` /
3424+ /// jj 0.45 names the operation whose view was restored, rather than
1499− /// `redo: <id>`); only those are reliably loadable for the walk
3425+ /// the operation undone or the undo reversed.
3426+ restores_view: bool,
3427+ /// Whether a legacy marker is j's exact `undo: <id>` / `redo: <id>`
3428+ /// format, whose discarded-operation links the walk can follow.
1500 authored_by_j: bool,
3429 authored_by_j: bool,
1503−/// §7.7 kinds: `j` marks undo/redo operations as `undo: <opid>` /
3432+/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
1504−/// `redo: <opid>` in the description (0.45's Transaction cannot write
3433+/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
1505−/// metadata tags); jj's own `undo operation <id>` / `redo operation <id>`
3434+/// `redo operation <id>` descriptions remain readable too.
1506−/// markers are treated the same way.
1507fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
3435fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
1508 match op_marker(meta) {
3436 match op_marker(meta) {
1509− Some(OpMarker { kind: "undo", id, .. }) => OpKind::Undo { undid: id },
3437+ Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
1510− Some(OpMarker { kind: "redo", id, .. }) => OpKind::Redo { redid: id },
3438+ Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
1511 _ => OpKind::Normal,
3439 _ => OpKind::Normal,
@@ -1515,20 +3443,33 @@ fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
1515fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
3443fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
1516 let desc = meta.description.trim();
3444 let desc = meta.description.trim();
1517 for kind in ["undo", "redo"] {
3445 for kind in ["undo", "redo"] {
3446+ if let Some(rest) = desc.strip_prefix(&format!("{}: restore to operation ", kind)) {
3447+ if let Some(id) = parse_op_id(rest.trim()) {
3448+ return Some(OpMarker { kind, id, restores_view: true, authored_by_j: false });
1518 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
3451 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
1519 if let Some(id) = parse_op_id(rest.trim()) {
3452 if let Some(id) = parse_op_id(rest.trim()) {
1520− return Some(OpMarker { kind, id, authored_by_j: true });
3453+ return Some(OpMarker { kind, id, restores_view: false, authored_by_j: true });
1523 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
3456 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
1524 if let Some(id) = parse_op_id(rest.trim()) {
3457 if let Some(id) = parse_op_id(rest.trim()) {
1525− return Some(OpMarker { kind, id, authored_by_j: false });
3458+ return Some(OpMarker { kind, id, restores_view: false, authored_by_j: false });
3465+/// Whether `meta` is the operation `init` or `clone` recorded (§7.7), by its
3466+/// description, `init` or `clone URL`: no expression is described so, both
3467+/// words being reserved (§1.1).
3468+fn op_created_repo(meta: &jj_lib::op_store::OperationMetadata) -> bool {
3469+ let desc = meta.description.trim();
3470+ desc == "init" || desc.starts_with("clone ")
1532/// a full-length (or prefix) hex operation id
3473/// a full-length (or prefix) hex operation id
1533fn parse_op_id(s: &str) -> Option<OperationId> {
3474fn parse_op_id(s: &str) -> Option<OperationId> {
1534 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
3475 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
@@ -1554,6 +3495,42 @@ fn op_parent_id(
1554 Ok(op.parent_ids().first().cloned())
3495 Ok(op.parent_ids().first().cloned())
3498+/// A restore operation represents an earlier view. Follow that view to a
3499+/// normal operation for useful confirmation text without changing undo's
3500+/// navigation or its jj-compatible metadata.
3501+fn restored_operation_description(
3502+ loader: &jj_lib::repo::RepoLoader,
3503+ op: &jj_lib::operation::Operation,
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 {
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()
3516+ // Unknown legacy jj history is best identified by its actual
3517+ // description instead of guessing which operation it applied.
3520+ let Some(target) = target else { break };
3521+ // Missing historical metadata must not prevent an otherwise valid
3522+ // recovery operation merely because its summary cannot be expanded.
3523+ let Ok(target_op) = block_on(loader.load_operation(&target)) else { break };
3524+ represented = target_op;
3526+ let description = represented.metadata().description.lines().next().unwrap_or("").trim();
3527+ if description.is_empty() {
3528+ format!("operation {}", truncate_chars(&represented.id().hex(), 12))
3530+ terminal_text(description)
1557/// parse the push-record list (§7.6): records of shape {id, name} set a
3534/// parse the push-record list (§7.6): records of shape {id, name} set a
1558/// bookmark, records of shape {delete} remove one; duplicates merge, the same
3535/// bookmark, records of shape {delete} remove one; duplicates merge, the same
1559/// name with different effects crashes (exit 1)
3536/// name with different effects crashes (exit 1)
@@ -1620,6 +3597,11 @@ fn parse_push_records(
1620 let rec = vis.commits.get(&change_id).ok_or_else(|| {
3597 let rec = vis.commits.get(&change_id).ok_or_else(|| {
1621 (1, format!("push: `@{}` is not a visible commit", change_id))
3598 (1, format!("push: `@{}` is not a visible commit", change_id))
3600+ // jj's root has no Git object: its all-zero commit id is
3601+ // interpreted by `git push` as a deletion, not a target.
3602+ if rec.commit.id() == repo.store().root_commit_id() {
3603+ return Err((1, "push: the root commit cannot be pushed".to_string()));
1623 out.push((name, Some(rec.commit.id().clone())));
3605 out.push((name, Some(rec.commit.id().clone())));
1625 None => {
3607 None => {
@@ -1639,68 +3621,190 @@ fn parse_push_records(
3624+/// §7.6: refuse to move a bookmark whose current target is immutable to a
3625+/// commit that does not descend from that target, or to delete it while no
3626+/// bookmark left on `origin` after the push reaches that target; and refuse
1642−/// §7.6: refuse to send commits with unresolved files or empty descriptions,
3627+/// to send commits with unresolved files or empty descriptions. Returns
1643−/// and refuse to move/delete a bookmark whose current target is immutable
3628+/// whether such a delete is allowed only because a record's new target
1644−/// away from a descendant of that target
3629+/// reaches it, so the deletes must wait for the updates (push_to_origin),
3630+/// and refuses a push whose deletes would wait for a bookmark git cannot
3631+/// create while one of them is still there
1645fn check_push_records(
3632fn check_push_records(
1646 records: &[(String, Option<CommitId>)],
3633 records: &[(String, Option<CommitId>)],
1648 repo: &Arc<ReadonlyRepo>,
3634 repo: &Arc<ReadonlyRepo>,
3636+ immutable: &BTreeSet<String>,
1649−) -> Result<(), OpenError> {
3637+) -> Result<bool, OpenError> {
1650 let store = repo.store().clone();
3638 let store = repo.store().clone();
3639+ let origin = RemoteName::new("origin");
3640+ // the records' new targets, which the push sends and leaves on `origin`
3641+ let new_targets: Vec<CommitId> = records.iter().filter_map(|(_, t)| t.clone()).collect();
3642+ // the targets of the other bookmarks left on `origin` after the push:
3643+ // those no record names, where they are as of the last fetch or push;
3644+ // computed for the first delete that needs them
3645+ let mut unnamed: Option<Vec<CommitId>> = None;
3646+ // the deletes that only records' new targets keep reached
3647+ let mut waiting: Vec<&str> = Vec::new();
1651 for (name, target) in records {
3648 for (name, target) in records {
1652− let mut seeds: Vec<String> = Vec::new();
1653− if let Some(commit_id) = target {
1654− let commit = block_on(store.get_commit_async(commit_id))
1655− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1656− let change_id = commit.change_id().reverse_hex();
1657− if vis.commits.contains_key(&change_id) {
1658− seeds.push(change_id);
1661− if let Some(current) = repo
3649+ let Some(current) = repo
1663− .get_remote_bookmark(RefName::new(name).to_remote_symbol(RemoteName::new("origin")))
3651+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
1665 .as_resolved()
3653 .as_resolved()
1666 .cloned()
3654 .cloned()
1667 .unwrap_or(None)
3655 .unwrap_or(None)
1669− let commit = block_on(store.get_commit_async(¤t))
3659+ let change_id = block_on(store.get_commit_async(¤t))
1670− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
3660+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
1671− let change_id = commit.change_id().reverse_hex();
3662+ .reverse_hex();
1672− if vis.commits.contains_key(&change_id) {
3663+ // the immutable set holds visible commits: a target rewritten here
1673− seeds.push(change_id);
3664+ // since the last fetch or push is hidden, and not in it although the
1674− }
3665+ // visible rewrite carries its change id (and its label)
1676− let ancestors = ancestors_of_change_ids(vis, &seeds);
1677− for change_id in &ancestors {
1678− let rec = vis.commits.get(change_id).expect("ancestors are stored");
3666+ let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == ¤t);
1679− for (_path, value) in rec.commit.tree().entries() {
3667+ if !visible || !immutable.contains(&change_id) {
1680− let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3668+ continue;
1681− if !value.is_resolved() {
3669+ }
3672+ // a delete loses no history while another bookmark still
3673+ // reaches the commit: a merged feature, a second name. When
3674+ // only a bookmark this push sets reaches it (`rename`), the
3675+ // remote may reject that update and accept the delete, so
3676+ // the delete waits for the update to be accepted
3677+ let unnamed = unnamed.get_or_insert_with(|| {
3678+ let named: BTreeSet<&str> = records.iter().map(|(n, _)| n.as_str()).collect();
3680+ .remote_bookmarks(origin)
3681+ .filter(|(n, _)| !named.contains(n.as_str()))
3682+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3686+ let reached = |heads: &[CommitId]| {
3687+ ResolvedRevsetExpression::commits(vec![current.clone()])
3688+ .intersection(&ResolvedRevsetExpression::commits(heads.to_vec()).ancestors())
3689+ .evaluate(repo.as_ref())
3690+ .and_then(|r| r.is_empty())
3691+ .map(|empty| !empty)
3692+ .map_err(|e| (2, format!("cannot read the index: {}", e)))
3694+ if !reached(unnamed)? {
3695+ if !reached(&new_targets)? {
3699+ "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3704+ waiting.push(name);
3708+ let descends = block_on(repo.index().is_ancestor(¤t, new))
3709+ .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3714+ "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
3722+ // git cannot hold a bookmark and one under its name at once (`master`
3723+ // and `master/legacy`), so while the deletes wait, a first push that
3724+ // creates a name nesting with a deleted one is rejected and the deletes
3725+ // are never sent. With a bookmark no record names on the waiting
3726+ // delete's commit, nothing waits and it is one git push (§7.6)
3727+ let deletes_wait = !waiting.is_empty();
3729+ let nests = |a: &str, b: &str| b.strip_prefix(a).is_some_and(|rest| rest.starts_with('/'));
3730+ for (deleted, _) in records.iter().filter(|(_, t)| t.is_none()) {
3731+ for (set, _) in records.iter().filter(|(_, t)| t.is_some()) {
3732+ if nests(deleted, set) || nests(set, deleted) {
3733+ let keep = if waiting.contains(&deleted.as_str()) { deleted.as_str() } else { waiting[0] };
3734+ let literal = crate::show::text_literal(keep);
3735+ // the temporary label: the first of `tmp`, `tmp-1`, ...
3736+ // that is no bookmark, here or on origin, no record
3737+ // names, and nests with none of those, so pushing and
3738+ // deleting it moves none of the user's bookmarks, and
3739+ // this push, which names nothing nesting with it, no
3740+ // longer waits on `keep`'s commit
3741+ let taken: Vec<&str> = repo
3744+ .map(|(n, _)| n.as_str())
3745+ .chain(records.iter().map(|(n, _)| n.as_str()))
3747+ let tmp = std::iter::once("tmp".to_string())
3748+ .chain((1..).map(|i| format!("tmp-{i}")))
3749+ .find(|t| !taken.iter().any(|n| n == t || nests(n, t) || nests(t, n)))
3750+ .expect("finitely many names are taken");
3751+ let tmp = crate::show::text_literal(&tmp);
1682 return Err((
3752 return Err((
1684 format!(
3754 format!(
1685− "push: commit `@{}` has unresolved files",
3755+ "push: `{deleted}` must stay on origin until `{set}` is accepted, and git cannot create `{set}` while `{deleted}` is there; first push a temporary label on `{keep}`'s commit, `push (label {tmp} (labelled {literal}))`, then this push, then `push (unlabel {tmp})`"
1691− if rec.commit.description().trim().is_empty() && rec.commit.id() != store.root_commit_id() {
3760+ }
1692− return Err((
3761+ }
1693− 1,
3762+ // the commits that would be sent: ancestors of the new targets that no
1694− format!("push: commit `@{}` has an empty description", rec.change_id),
3763+ // bookmark on the remote already reaches; a delete sends none
1695− ));
3764+ if new_targets.is_empty() {
3765+ return Ok(deletes_wait);
3767+ let on_remote: Vec<CommitId> = repo
3769+ .remote_bookmarks(origin)
3770+ .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3773+ let sent = ResolvedRevsetExpression::commits(on_remote)
3774+ .range(&ResolvedRevsetExpression::commits(new_targets))
3775+ .evaluate(repo.as_ref())
3776+ .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3777+ let sent: Vec<Commit> = block_on(sent.stream().commits(&store).try_collect())
3778+ .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3779+ for commit in &sent {
3780+ let change_id = commit.change_id().reverse_hex();
3781+ let mut unresolved_paths = Vec::new();
3782+ for (path, value) in commit.tree().entries() {
3783+ let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3784+ if !value.is_resolved() {
3785+ unresolved_paths.push(path.as_internal_file_string().to_string());
3788+ if !unresolved_paths.is_empty() {
3789+ let mut paths = unresolved_paths.iter().take(5)
3790+ .map(|path| format!("`{}`", terminal_text(path)))
3791+ .collect::<Vec<_>>().join(", ");
3792+ if unresolved_paths.len() > 5 {
3793+ paths.push_str(&format!(" and {} more", unresolved_paths.len() - 5));
3795+ return Err((1, format!(
3796+ "push: commit `@{change_id}` has unresolved files: {paths}; run `j conflicts` to inspect conflicts, then `j by @{change_id}` to edit this commit. Resolve the listed files, run `j id` to record the resolution, and check `j conflicts` before pushing again"
3799+ if commit.description().trim().is_empty() && commit.id() != store.root_commit_id() {
3800+ return Err((1, format!("push: commit `@{}` has an empty description", change_id)));
1699− Ok(())
3803+ Ok(deletes_wait)
1702/// change-id closure of jj-parents from the seeds (all jj parents, not just
3806/// change-id closure of jj-parents from the seeds (all jj parents, not just
1703−/// the first), for the push content checks
3807+/// the first), for the immutable set (§7.5)
1704fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
3808fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
1705 let mut seen = BTreeSet::new();
3809 let mut seen = BTreeSet::new();
1706 let mut stack: Vec<String> = seeds.to_vec();
3810 let mut stack: Vec<String> = seeds.to_vec();
@@ -1721,23 +3825,11 @@ fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<Stri
1722/// git-check-ref-format --branch rules
3826/// git-check-ref-format --branch rules
1723fn is_valid_git_branch_name(name: &str) -> bool {
3827fn is_valid_git_branch_name(name: &str) -> bool {
1725− || name.starts_with('-')
3828+ if name.starts_with('-') {
1726− || name.starts_with('.')
1727− || name.ends_with('.')
1728− || name.ends_with('/')
1729− || name.ends_with(".lock")
1730− || name.contains("..")
1731− || name.contains("//")
1732− || name.contains("@{")
1735 return false;
3829 return false;
1737− name.chars().all(|c| {
3831+ let full_name = format!("refs/heads/{name}");
1738− !(c.is_ascii_control()
3832+ gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
1739− || matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
1743// ----------------------------------------------------------------------
3835// ----------------------------------------------------------------------
@@ -1745,6 +3837,12 @@ fn is_valid_git_branch_name(name: &str) -> bool {
1745// ----------------------------------------------------------------------
3837// ----------------------------------------------------------------------
1747impl Backend for JjBackend {
3839impl Backend for JjBackend {
3840+ fn validate_authoring(&self) -> Result<(), Crash> {
3841+ let repo = self.current_repo();
3842+ check_identity(repo.settings().user_name(), repo.settings().user_email())
3843+ .map_err(|(_, message)| Crash::new(message))
1748 fn visible_ids(&self) -> Vec<String> {
3846 fn visible_ids(&self) -> Vec<String> {
1749 match self.inner.visible.lock().unwrap().as_ref() {
3847 match self.inner.visible.lock().unwrap().as_ref() {
1750 Some(v) => v.commits.keys().cloned().collect(),
3848 Some(v) => v.commits.keys().cloned().collect(),
@@ -1779,38 +3877,93 @@ impl Backend for JjBackend {
1779 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
3877 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
1782− fn is_empty(&self, id: &str) -> Option<bool> {
3880+ fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
1783 let vis = self.inner.visible.lock().unwrap().clone();
3881 let vis = self.inner.visible.lock().unwrap().clone();
1784− // O(1): empty iff the tree id equals the first parent's tree id
3882+ // O(1): empty iff the tree id equals the parent's tree id
1785 vis.and_then(|v| {
3883 vis.and_then(|v| {
1786 let rec = v.commits.get(id)?;
3884 let rec = v.commits.get(id)?;
1787− let parent = v.commits.get(&rec.first_parent)?;
3885+ let parent = v.commits.get(parent)?;
1788 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3886 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
3890+ fn folds_case(&self) -> bool {
3891+ // jj's checkout refuses a name whose file identity is that of
3892+ // `.git` or `.jj` in the same directory (local_working_copy.rs), so
3893+ // where `.JJ` is the workspace's own `.jj` it refuses `.GIT` too.
3894+ // Where each spelling of a name gets its own inode number
3895+ // (exfat-fuse) the identities differ and the checkout writes
3896+ // `.GIT/x` into `.git`, so folding is told by how names resolve:
3897+ // `.JJ` resolves, but the directory does not list it under that
3898+ // spelling. A directory that cannot be listed is taken to fold
3899+ // case, the answer that refuses more.
3900+ *self.inner.folds_case.get_or_init(|| {
3901+ use jj_lib::file_util::FileIdentity;
3902+ let root = &self.inner.workspace_root;
3903+ let Ok(upper) = FileIdentity::from_symlink_path(root.join(".JJ")) else {
3906+ FileIdentity::from_symlink_path(root.join(".jj")).is_ok_and(|lower| lower == upper)
3907+ || std::fs::read_dir(root).map_or(true, |mut entries| {
3908+ !entries.any(|e| e.is_ok_and(|e| e.file_name() == ".JJ"))
3913+ fn name_fits(&self, name: &str) -> bool {
3914+ if name.len() <= crate::repo::NAME_MAX {
3917+ // a component, never a path: one with `/` would be created elsewhere
3918+ if name.contains(['/', '\0']) {
3921+ // no call tells whether the limit counts bytes (ext4) or UTF-16
3922+ // units or characters (exFAT, NTFS, APFS), so the name is tried:
3923+ // created in a directory of its own under `.jj`, which is then
3924+ // removed, once per name and run
3925+ let mut known = self.inner.long_names.lock().unwrap();
3926+ if let Some(fits) = known.get(name) {
3929+ let fits = match tempfile::Builder::new()
3931+ .tempdir_in(self.inner.workspace_root.join(".jj"))
3934+ let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3940+ known.insert(name.to_string(), fits);
1792 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
3944 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
1793 let vis = self.inner.visible.lock().unwrap().clone();
3945 let vis = self.inner.visible.lock().unwrap().clone();
1794 let Some(vis) = vis else {
3946 let Some(vis) = vis else {
1795 return BTreeSet::new();
3947 return BTreeSet::new();
1797− let mut seen = BTreeSet::new();
3949+ // every jj parent, not just the first: rewriting a commit reached
3950+ // only through a merge's second parent rewrites the merge too
1798− let mut stack: Vec<String> = ids.iter().cloned().collect();
3952+ let seeds: Vec<String> = ids.iter().cloned().collect();
1799− while let Some(id) = stack.pop() {
3953+ ancestors_of_change_ids(&vis, &seeds)
1800− if seen.insert(id.clone()) {
1801− if let Some(rec) = vis.commits.get(&id) {
1802− if !rec.first_parent.is_empty() {
1803− stack.push(rec.first_parent.clone());
1811 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
3956 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
3957+ self.replay_snapshot(
3958+ &Value::list(onto.to_vec()),
3959+ &Value::list(from.to_vec()),
3960+ &Value::list(to.to_vec()),
3961+ )?.as_list().map(<[Value]>::to_vec)
3964+ fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
1812 let store = self.current_repo().store().clone();
3965 let store = self.current_repo().store().clone();
1813− block_on(jj_replay(&store, onto, from, to))
3966+ block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
@@ -1818,30 +3971,197 @@ impl Backend for JjBackend {
1818// jj three-way tree merge for `replay` (§7.3)
3971// jj three-way tree merge for `replay` (§7.3)
1819// ----------------------------------------------------------------------
3972// ----------------------------------------------------------------------
3974+/// Replay sees the same lists repeatedly: a parent's old and new files
3975+/// become each child's bases. Retain their validated trees by list identity,
3976+/// including its offset, without keeping discarded snapshots alive.
3978+struct ReplayCache {
3979+ trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980+ empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981+ next_sweep: Cell<usize>,
3983+ entries: EntryCache,
3987+ storage: Weak<Vec<Value>>,
3989+ /// A list read from a resolved tree, already in canonical path order.
3994+ fn remember(&self, list: &ListVal, tree: MergedTree, canonical: bool) {
3995+ let mut trees = self.trees.borrow_mut();
3996+ trees.insert(list.identity(), ReplayTree {
3997+ storage: list.weak_storage(),
4001+ if trees.len() >= self.next_sweep.get().max(256) {
4002+ trees.retain(|_, t| t.storage.strong_count() != 0);
4003+ self.next_sweep.set((trees.len() * 2).max(256));
4007+ async fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<(MergedTree, bool), Crash> {
4008+ let Value::List(list) = files else {
4012+ if let Some(cached) = self.trees.borrow().get(&list.identity()) {
4013+ // The weak reference keeps its allocation reserved, so another
4014+ // list cannot reuse this address while the key is cached.
4015+ if cached.storage.strong_count() != 0 {
4016+ return Ok((cached.tree.clone(), cached.canonical));
4019+ // Validate every new list before caching it, even when from == to:
4020+ // an unchanged change must still reject malformed snapshots.
4021+ let tree = build_tree(store, files).await?;
4022+ self.remember(list, tree.clone(), false);
4026+ async fn files_of(&self, store: &Arc<Store>, tree: &MergedTree) -> Result<Value, Crash> {
4027+ let (files, reusable) = tree_to_files_inner(store, tree, &self.blobs, &self.entries).await?;
4028+ let list = ListVal::new(files);
4029+ if reusable && !self.has_empty_subtrees(tree).await? {
4030+ self.remember(&list, tree.clone(), true);
4032+ Ok(Value::List(list))
4035+ /// Flattening a Git tree omits empty directories. Reusing such a tree
4036+ /// would let a replay conflict with a directory absent from its input
4037+ /// snapshot. Inspect directories once per tree, without rebuilding files.
4038+ async fn has_empty_subtrees(&self, tree: &MergedTree) -> Result<bool, Crash> {
4039+ let Some(root) = tree.tree_ids().as_resolved() else { return Ok(true) };
4040+ if let Some(found) = self.empty_subtrees.borrow().get(root) {
4043+ let store = tree.store();
4044+ let mut pending = vec![(RepoPathBuf::root(), root.clone())];
4045+ let mut visited = BTreeSet::new();
4046+ while let Some((dir, id)) = pending.pop() {
4047+ if !visited.insert(id.clone()) {
4050+ let known = self.empty_subtrees.borrow().get(&id).copied();
4051+ if let Some(found) = known {
4053+ self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4058+ let loaded = store.get_tree(dir.clone(), &id).await
4059+ .map_err(|e| Crash::new(format!("cannot read a tree: {}", e)))?;
4060+ for entry in loaded.entries_non_recursive() {
4061+ if let TreeValue::Tree(child) = entry.value() {
4062+ if child == store.empty_tree_id() {
4063+ self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4066+ pending.push((dir.join(entry.name()), child.clone()));
4070+ // Every subtree visited was nonempty all the way down. Sharing this
4071+ // answer also avoids rescanning unchanged directories of later trees.
4072+ self.empty_subtrees.borrow_mut().extend(visited.into_iter().map(|id| (id, false)));
1821async fn jj_replay(
4077async fn jj_replay(
1822 store: &Arc<Store>,
4078 store: &Arc<Store>,
4079+ cache: &ReplayCache,
1823− onto: &[Value],
4080+ onto: &Value,
1824− from: &[Value],
4081+ from: &Value,
1825− to: &[Value],
4082+ to: &Value,
1826−) -> Result<Vec<Value>, Crash> {
4083+) -> Result<Value, Crash> {
1827− let onto_tree = build_tree(store, &Value::list(onto.to_vec())).await?;
4084+ let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
1828− let from_tree = build_tree(store, &Value::list(from.to_vec())).await?;
4085+ let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
1829− let to_tree = build_tree(store, &Value::list(to.to_vec())).await?;
4086+ let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
1830− let merge = Merge::from_removes_adds(
4087+ // any of the three may itself be conflicted (a snapshot with unresolved
1831− vec![from_tree.tree_ids().as_resolved().cloned().unwrap()],
4088+ // blobs, §7.3), so merge the trees the way jj's rebase does:
1832− vec![
4089+ // MergedTree::merge flattens conflicted inputs into one merge, simplifies
1833− onto_tree.tree_ids().as_resolved().cloned().unwrap(),
1834− to_tree.tree_ids().as_resolved().cloned().unwrap(),
1837− // tree_merge::merge_trees resolves file contents line-level and keeps
4090+ // it, and resolves file contents line-level with tree_merge::merge_trees,
1838− // unresolvable paths as conflicts (tree_merge.rs:78)
4091+ // keeping unresolvable paths as conflicts. Empty labels leave the result
1839− let merged = jj_lib::tree_merge::merge_trees(store, merge)
4092+ // unlabeled; the labels would not survive tree_to_files anyway.
4093+ let merged = MergedTree::merge(Merge::from_removes_adds(
4094+ vec![(from_tree.clone(), String::new())],
4095+ vec![(onto_tree.clone(), String::new()), (to_tree.clone(), String::new())],
1841− .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4098+ .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4099+ refuse_file_directory_clash(&merged, &onto_tree, &from_tree, &to_tree).await?;
4100+ // Reusing a resolved tree's list preserves replay's sorted output. A
4101+ // constructed list may have another order, and conflicts may simplify
4102+ // on reading, so neither can take this shortcut.
4103+ for (files, tree, canonical) in [
4104+ (onto, &onto_tree, onto_canonical),
4105+ (to, &to_tree, to_canonical),
4106+ (from, &from_tree, from_canonical),
4108+ if canonical && merged.tree_ids() == tree.tree_ids() {
4109+ return Ok(files.clone());
1842 // conflicts stay as unresolved blobs (§7.3)
4112 // conflicts stay as unresolved blobs (§7.3)
1843− let merged = MergedTree::new(store.clone(), merged, jj_lib::conflict_labels::ConflictLabels::unlabeled());
4113+ cache.files_of(store, &merged).await
1844− tree_to_files(store, &merged, &BlobCache::default(), &EntryCache::default()).await
4114+}
4116+/// Crash if the merge left a conflict with a file on one side and, on
4117+/// another, a directory `onto` or `to` holds at that path: one entry cannot
4118+/// list that directory's entries (§7.3). A conflict that `onto`, `from` or
4119+/// `to` already holds at the path is not made here: its sides, a directory
4120+/// side jj made earlier included, came in as an unresolved blob, and replay
4121+/// carries them beside the others, as when the child resolving that conflict
4122+/// is squashed into it.
4123+async fn refuse_file_directory_clash(
4124+ merged: &MergedTree,
4128+) -> Result<(), Crash> {
4129+ let value_at = async |tree: &MergedTree, path: &RepoPath| {
4130+ tree.path_value(path).await.map_err(|e| Crash::new(format!("replay: {}", e)))
4132+ // walks only the unresolved part of the tree: nothing when it is resolved
4133+ for (path, value) in merged.conflicts() {
4134+ let value = value.map_err(|e| Crash::new(format!("replay: {}", e)))?;
4135+ // sides that cancel are no sides
4136+ let value = value.simplify();
4137+ let has = |dir: bool| {
4140+ .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4142+ if !has(false) || !has(true) {
4146+ value_at(onto, &path).await?,
4147+ value_at(from, &path).await?,
4148+ value_at(to, &path).await?,
4150+ // an input's conflict at the path, as opposed to one inside the
4151+ // directory there, which leaves the path a directory in the input
4152+ if held.iter().any(|h| !h.is_resolved() && !h.is_tree()) {
4155+ let side = |dir: &MergedTreeValueT| {
4156+ dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
4158+ if side(&held[0]) || side(&held[2]) {
4159+ let comps: Vec<String> =
4160+ path.components().map(|c| c.as_internal_str().to_string()).collect();
4161+ return Err(crate::domain::file_directory_clash(&comps));
1847// ----------------------------------------------------------------------
4167// ----------------------------------------------------------------------
@@ -1862,6 +4182,107 @@ impl ConflictTreeValue {
4185+/// The stored trees a program's commits may still hold the files of, to
4186+/// write each such commit with the tree itself (§7.5 step 4). A list holds
4187+/// each conflict simplified (`merged_value_to_blob`), and the tree it was
4188+/// loaded from need not, so a tree built from the list could differ from
4189+/// the stored one where the files do not: a child `new` made would not be
4190+/// empty, as jj's own `new` leaves one.
4191+struct LoadedTrees<'a> {
4192+ /// the stored commits the program's repo was loaded from
4193+ visible: Option<&'a VisibleRepo>,
4194+ /// the lists the lazy `files` of the program's commits have loaded, by
4195+ /// identity: `c.files` takes the list out, so a commit given it, as
4196+ /// `new` gives its child its parent's, holds the list itself
4197+ lists: HashMap<(*const Vec<Value>, usize), Rc<ThunkVal>>,
4200+impl<'a> LoadedTrees<'a> {
4201+ /// The trees `repos`' commits were loaded with; nothing is read
4202+ fn new(visible: Option<&'a VisibleRepo>, repos: &[&Value]) -> Result<LoadedTrees<'a>, Crash> {
4203+ let mut lists = HashMap::new();
4204+ for repo in repos {
4205+ for commit in crate::repo::all_commits(repo)? {
4206+ if let Value::Thunk(t) = crate::repo::files_of(&commit)? {
4207+ if let (Some(_), Some(Value::List(list))) = (t.tree(), t.peek()) {
4208+ lists.insert(list.identity(), t);
4213+ Ok(LoadedTrees { visible, lists })
4216+ /// The tree to write for a commit on `parents` whose files are `files`:
4217+ /// where they are still a list loaded from a stored tree, that tree as
4218+ /// jj stored it, and otherwise the one `build_tree` makes of them; but
4219+ /// where either lists the parents' files, the parents' tree
4220+ fn tree_to_write(&self, repo: &dyn Repo, files: &Value, parents: &[CommitId]) -> Result<MergedTree, Crash> {
4221+ let tree = self.tree_of(repo.store(), files)?;
4222+ Ok(block_on(parents_tree_if_same(repo, parents, tree)))
4225+ fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4226+ let loaded = match files {
4227+ Value::Thunk(t) => Some(t),
4228+ Value::List(list) => self.lists.get(&list.identity()),
4231+ if let Some(t) = loaded {
4232+ let stored = t.origin().zip(self.visible).and_then(|(id, vis)| vis.commits.get(id));
4233+ if let Some(tree) = stored.map(|rec| rec.commit.tree()) {
4234+ if t.tree() == Some(tree_name(&tree).as_str()) {
4239+ block_on(build_tree(store, &files.forced()?))
4243+/// The tree of `parents`, merged where there are several, as jj's emptiness
4244+/// takes it, where it lists the files `tree` lists, and otherwise `tree`
4245+/// (§7.5 step 4). A list holds each conflict less the sides that cancel
4246+/// (§7.3), and a tree built from it holds no labels, where a tree jj merged
4247+/// keeps both; so a commit left with its parent's files by `squash`,
4248+/// `rebase` or `abandon`, which build the files anew, got another tree for
4249+/// them: jj took it for a change at each conflicted path, the tree's empty
4250+/// mark (`is_empty`) did not show, and the checkout wrote the conflicts
4251+/// again with generic labels. So did a commit moved onto another tree for
4252+/// its files, which kept its own, and a snapshot that reverted the commit's
4253+/// change, which builds on the commit's tree (§7.4). Files with no conflict
4254+/// make one tree, so a resolved parents' tree is not compared, nor one of
4255+/// the same ids, and the comparison stops at the first path where the two
4256+/// differ. It only chooses between two trees for the same files, so where
4257+/// the parents' cannot be read, it is `tree`.
4258+async fn parents_tree_if_same(repo: &dyn Repo, parents: &[CommitId], tree: MergedTree) -> MergedTree {
4259+ let mut commits = Vec::with_capacity(parents.len());
4260+ for id in parents {
4261+ match repo.store().get_commit_async(id).await {
4262+ Ok(commit) => commits.push(commit),
4263+ Err(_) => return tree,
4266+ let Ok(parents_tree) = jj_lib::rewrite::merge_commit_trees(repo, &commits).await else {
4269+ // the same terms: the parents' tree, with jj's labels for them
4270+ if parents_tree.tree_ids() == tree.tree_ids() {
4271+ return parents_tree;
4273+ if parents_tree.tree_ids().is_resolved() {
4276+ let mut diff = parents_tree.diff_stream(&tree, &EverythingMatcher);
4277+ while let Some(entry) = diff.next().await {
4278+ match entry.values {
4279+ Ok(Diff { before, after }) if value_as_read(&before) == value_as_read(&after) => {}
1865async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4286async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
1866 let entries = files
4287 let entries = files
1867 .as_list()
4288 .as_list()
@@ -1921,19 +4342,41 @@ async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<Merge
1921 return Err(Crash::new("persistence: malformed conflict blob"));
4342 return Err(Crash::new("persistence: malformed conflict blob"));
1923 // jj order: [add, (remove, add)*] → Merge with interleaved adds
4344 // jj order: [add, (remove, add)*] → Merge with interleaved adds
1924− // 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
1925 let mut values: Vec<Option<TreeValue>> = Vec::new();
4348 let mut values: Vec<Option<TreeValue>> = Vec::new();
1926 for side in sides {
4349 for side in sides {
4350+ let Some(side) = side else {
4354+ let value = if let Some(hex) = &side.tree {
4356+ TreeId::try_from_hex(hex)
4357+ .ok_or_else(|| Crash::new("persistence: bad tree id"))?,
4359+ } else if side.kind == BlobKind::Symlink {
4360+ let target = std::str::from_utf8(&side.bytes)
4361+ .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4363+ .write_symlink(&RepoPathBuf::root(), target)
4365+ .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366+ TreeValue::Symlink(id)
1927− let mut slice: &[u8] = side.as_ref();
4368+ let mut slice: &[u8] = side.bytes.as_ref();
1928− let id = store
4369+ let id = store
1929− .write_file(&RepoPathBuf::root(), &mut slice)
4370+ .write_file(&RepoPathBuf::root(), &mut slice)
1931− .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4372+ .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
1932− values.push(Some(TreeValue::File {
4373+ TreeValue::File {
1934− executable: b.kind == BlobKind::Executable,
4375+ executable: side.kind == BlobKind::Executable,
1935− copy_id: CopyId::placeholder(),
4376+ copy_id: CopyId::placeholder(),
4379+ values.push(Some(value));
1938 let mut adds = Vec::new();
4381 let mut adds = Vec::new();
1939 let mut removes = Vec::new();
4382 let mut removes = Vec::new();
@@ -1963,34 +4406,49 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
1963// jj trees -> values
4406// jj trees -> values
1964// ----------------------------------------------------------------------
4407// ----------------------------------------------------------------------
1966−/// blob contents keyed by file id, shared across the whole repo load: most
4409+/// Blob contents keyed by file id, shared while snapshots hold their bytes.
1967−/// files are unchanged between commits, so inflating each blob once (instead
4410+/// Weak entries let discarded replay results release their conflict sides.
1968−/// of once per commit that references it) keeps loading linear in the
1969−/// repository's unique data rather than commits × files
1970#[derive(Clone, Default)]
4411#[derive(Clone, Default)]
1971−struct BlobCache(Rc<RefCell<HashMap<FileId, Rc<Vec<u8>>>>>);
4413+ bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
4414+ next_sweep: Rc<Cell<usize>>,
1973impl BlobCache {
4417impl BlobCache {
1974 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
4418 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
1975− self.0.borrow().get(id).cloned()
4419+ self.bytes.borrow().get(id).and_then(Weak::upgrade)
1977 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
4421 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
1978− 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));
1982−/// whole file *entries* ({path, content}) keyed by (path, blob identity),
4431+/// Whole file entries keyed by (path, blob identity), shared by loaded
1983−/// shared across the whole repo load: an unchanged file is the same Value in
4432+/// snapshots and replays. Weak references let transient replay results go
1984−/// every commit, so it is built once and cloned (an Rc bump) thereafter
4433+/// away while unchanged entries still held by a snapshot are reused.
1985#[derive(Clone, Default)]
4434#[derive(Clone, Default)]
1986−struct EntryCache(Rc<RefCell<HashMap<String, Value>>>);
4435+struct EntryCache {
4436+ entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437+ next_sweep: Rc<Cell<usize>>,
1988impl EntryCache {
4440impl EntryCache {
1989 fn get(&self, key: &str) -> Option<Value> {
4441 fn get(&self, key: &str) -> Option<Value> {
1990− self.0.borrow().get(key).cloned()
4442+ self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
1992 fn insert(&self, key: String, entry: Value) {
4444 fn insert(&self, key: String, entry: Value) {
4445+ let Value::Record(record) = entry else { return };
1993− self.0.borrow_mut().insert(key, entry);
4446+ let mut entries = self.entries.borrow_mut();
4447+ entries.insert(key, Rc::downgrade(&record));
4448+ if entries.len() >= self.next_sweep.get().max(256) {
4449+ entries.retain(|_, entry| entry.strong_count() != 0);
4450+ self.next_sweep.set((entries.len() * 2).max(256));
@@ -2018,20 +4476,64 @@ async fn read_file_bytes(
2021−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(
2022 store: &Arc<Store>,
4482 store: &Arc<Store>,
2023 path: &RepoPath,
4483 path: &RepoPath,
2024 v: &Option<TreeValue>,
4484 v: &Option<TreeValue>,
2025 cache: &BlobCache,
4485 cache: &BlobCache,
2026−) -> Result<Rc<Vec<u8>>, Crash> {
4486+) -> Result<Option<ConflictSide>, Crash> {
2027 match v {
4487 match v {
4489+ Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490+ kind: if *executable {
4491+ BlobKind::Executable
2028− Some(TreeValue::File { id, .. }) => read_file_bytes(store, path, id, cache).await,
4495+ bytes: read_file_bytes(store, path, id, cache).await?,
2029− Some(TreeValue::Symlink(id)) => store
4498+ Some(TreeValue::Symlink(id)) => {
2030− .read_symlink(path, id)
4500+ .read_symlink(path, id)
2032− .map(|s| Rc::new(s.into_bytes()))
2033− .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e))),
4502+ .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
4503+ Ok(Some(ConflictSide {
4504+ kind: BlobKind::Symlink,
4505+ bytes: Rc::new(target.into_bytes()),
4509+ // a directory side, in a conflict between a file and a directory,
4510+ // reads as empty content but keeps its tree, so writing the conflict
4511+ // back cannot put an empty file where the directory was
4512+ Some(TreeValue::Tree(id)) => Ok(Some(ConflictSide {
4513+ kind: BlobKind::Regular,
2034− _ => Ok(Rc::new(Vec::new())),
4514+ bytes: Rc::new(Vec::new()),
4515+ tree: Some(id.hex()),
4517+ // a submodule side reads as an empty file, as a resolved one does in
4518+ // tree_value_to_blob (§12)
4519+ Some(TreeValue::GitSubmodule(_)) => Ok(Some(ConflictSide::regular(&[]))),
4523+/// The value a list reads where a tree holds `value` (§7.3): a conflict less
4524+/// each pair of sides that cancel, one added and one removed the same, as jj
4525+/// simplifies a file conflict to write it out. jj keeps every term of a
4526+/// merge it cannot resolve, pairs that cancel included, so a replayed
4527+/// commit's conflict inherited from its parent was not the parent's, and
4528+/// each commit a rebase replayed above took two more sides for it. Where
4529+/// only directory sides would be left, the value is kept whole: jj keeps it
4530+/// as one conflict at the path, and the simplified one would be a conflict
4531+/// between directories, which jj merges entry by entry. Two values read
4532+/// the same are the same entry of a list.
4533+fn value_as_read(value: &MergedTreeValueT) -> std::borrow::Cow<'_, MergedTreeValueT> {
4534+ match (!value.is_resolved()).then(|| value.simplify()).filter(|v| !v.is_tree()) {
4535+ Some(simplified) => std::borrow::Cow::Owned(simplified),
4536+ None => std::borrow::Cow::Borrowed(value),
@@ -2041,6 +4543,9 @@ async fn merged_value_to_blob(
2041 value: &jj_lib::backend::MergedTreeValue,
4543 value: &jj_lib::backend::MergedTreeValue,
2042 cache: &BlobCache,
4544 cache: &BlobCache,
2043) -> Result<Value, Crash> {
4545) -> Result<Value, Crash> {
4546+ // the conflict less the sides that cancel (§7.3)
4547+ let read = value_as_read(value);
4548+ let value = read.as_ref();
2044 if let Some(v) = value.as_resolved() {
4549 if let Some(v) = value.as_resolved() {
2045 return match v {
4550 return match v {
2046 Some(tv) => tree_value_to_blob(store, path, tv).await,
4551 Some(tv) => tree_value_to_blob(store, path, tv).await,
@@ -2051,11 +4556,11 @@ async fn merged_value_to_blob(
2053 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
4558 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
2054− let mut sides: Vec<Rc<Vec<u8>>> = Vec::new();
4559+ let mut sides: Vec<Option<ConflictSide>> = Vec::new();
2055 for (i, add) in value.adds().enumerate() {
4560 for (i, add) in value.adds().enumerate() {
2056− sides.push(side_to_bytes(store, path, add, cache).await?);
4561+ sides.push(conflict_side(store, path, add, cache).await?);
2057 if let Some(remove) = value.get_remove(i) {
4562 if let Some(remove) = value.get_remove(i) {
2058− sides.push(side_to_bytes(store, path, remove, cache).await?);
4563+ sides.push(conflict_side(store, path, remove, cache).await?);
2061 Ok(Value::Blob(Rc::new(BlobVal {
4566 Ok(Value::Blob(Rc::new(BlobVal {
@@ -2107,8 +4612,9 @@ async fn tree_value_to_blob(
2107 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
4612 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
2110− // a conflicted directory is emitted without its children by
4615+ // MergedTree::entries recurses into a resolved directory, and a
4616+ // directory side of a conflict goes through conflict_side, so this is
2111− // MergedTree::entries; §12 puts such repairs out of scope
4617+ // a submodule, which §12 puts out of scope
2112 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
4618 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
2113 kind: BlobKind::Regular,
4619 kind: BlobKind::Regular,
2114 content: BlobContent::Resolved(Rc::new(Vec::new())),
4620 content: BlobContent::Resolved(Rc::new(Vec::new())),
@@ -2122,9 +4628,27 @@ async fn tree_to_files(
2122 cache: &BlobCache,
4628 cache: &BlobCache,
2123 entries: &EntryCache,
4629 entries: &EntryCache,
2124) -> Result<Vec<Value>, Crash> {
4630) -> Result<Vec<Value>, Crash> {
4631+ tree_to_files_inner(store, tree, cache, entries).await.map(|(files, _)| files)
4634+/// Whether these files can reuse the original tree in replay. Reading a
4635+/// conflict simplifies it, and an unsupported git submodule reads as an
4636+/// empty file, so their original trees cannot represent the resulting list.
4637+async fn tree_to_files_inner(
4638+ store: &Arc<Store>,
4641+ entries: &EntryCache,
4642+) -> Result<(Vec<Value>, bool), Crash> {
2125 let mut out = Vec::new();
4643 let mut out = Vec::new();
4644+ let mut reusable = tree.tree_ids().is_resolved();
2126 for (path, value) in tree.entries() {
4645 for (path, value) in tree.entries() {
2127 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4646 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4647+ reusable &= match value.as_resolved() {
4648+ Some(Some(TreeValue::File { copy_id, .. })) => *copy_id == CopyId::placeholder(),
4649+ Some(Some(TreeValue::Symlink(_))) => true,
2128 // cache key: the entry is fully determined by its path and the
4652 // cache key: the entry is fully determined by its path and the
2129 // identity of its (resolved) content; conflicts are not cached
4653 // identity of its (resolved) content; conflicts are not cached
2130 let key = match value.as_resolved() {
4654 let key = match value.as_resolved() {
@@ -2155,13 +4679,22 @@ async fn tree_to_files(
2156 out.push(entry);
4680 out.push(entry);
2158− Ok(out)
4682+ Ok((out, reusable))
2161// ----------------------------------------------------------------------
4685// ----------------------------------------------------------------------
2162// value helpers
4686// value helpers
2163// ----------------------------------------------------------------------
4687// ----------------------------------------------------------------------
4689+/// The name of a stored tree that a commit's lazy `files` carry
4690+/// (`ThunkVal::tree`): its tree ids, one per term of a conflicted tree. The
4691+/// list `tree_to_files` makes depends on nothing else, so two trees of one
4692+/// name list the same entries.
4693+fn tree_name(tree: &MergedTree) -> String {
4694+ let ids: Vec<String> = tree.tree_ids().iter().map(|id| id.hex()).collect();
2165fn commit_id_of(commit: &Value) -> Result<String, Crash> {
4698fn commit_id_of(commit: &Value) -> Result<String, Crash> {
2166 match commit.field("id")? {
4699 match commit.field("id")? {
2167 Value::Id(i) => Ok(i.to_string()),
4700 Value::Id(i) => Ok(i.to_string()),
@@ -2189,15 +4722,13 @@ fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
2189// building the Repo value from the visible map (§7.2)
4722// building the Repo value from the visible map (§7.2)
2190// ----------------------------------------------------------------------
4723// ----------------------------------------------------------------------
2192−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> {
2193 let root_rec = vis
4726 let root_rec = vis
2194 .commits
4727 .commits
2195 .get(ROOT_ID)
4728 .get(ROOT_ID)
2196 .ok_or_else(|| (2, "no root commit".to_string()))?;
4729 .ok_or_else(|| (2, "no root commit".to_string()))?;
2197 let store = root_rec.commit.store().clone();
4730 let store = root_rec.commit.store().clone();
2198− let cache = BlobCache::default();
2199− let entries = EntryCache::default();
2200− let mut subtree = build_subtree(vis, root_rec, &store, &cache, &entries)?;
4731+ let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
2201 sort_subtree(&mut subtree, vis);
4732 sort_subtree(&mut subtree, vis);
2202 let top = Value::record(&[
4733 let top = Value::record(&[
2203 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
4734 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
@@ -2214,19 +4745,20 @@ fn build_subtree(
2214 vis: &VisibleRepo,
4745 vis: &VisibleRepo,
2215 rec: &StoredCommit,
4746 rec: &StoredCommit,
2216 store: &Arc<Store>,
4747 store: &Arc<Store>,
2217− cache: &BlobCache,
4748+ replay_cache: &Rc<ReplayCache>,
2218− entries: &EntryCache,
2219) -> Result<Value, OpenError> {
4749) -> Result<Value, OpenError> {
2220 // the files list is lazy (§7.2): most commits are never inspected, so
4750 // the files list is lazy (§7.2): most commits are never inspected, so
2221− // their file entries are materialized only on first `field("files")`
4751+ // their file entries are materialized only on first `field("files")`.
4752+ // Tagged with the change id, it tells the renderer when the O(1)
4753+ // `has_conflict`/`is_empty` answers still describe the commit's files;
4754+ // named by its tree, it lets persistence compare it unread (§7.5)
2222 let files_thunk = {
4755 let files_thunk = {
2223 let store = store.clone();
4756 let store = store.clone();
2224 let tree = rec.commit.tree();
4757 let tree = rec.commit.tree();
2225− let cache = cache.clone();
4758+ let name = Some(tree_name(&tree));
2226− let entries = entries.clone();
4759+ let replay_cache = replay_cache.clone();
2227− Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
4760+ Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
2228− let files = block_on(tree_to_files(&store, &tree, &cache, &entries))?;
4761+ block_on(replay_cache.files_of(&store, &tree))
2229− Ok(Value::list(files))
2232 let labels: Vec<Value> = vis
4764 let labels: Vec<Value> = vis
@@ -2247,7 +4779,7 @@ fn build_subtree(
2247 if let Some(kids) = vis.children.get(&rec.change_id) {
4779 if let Some(kids) = vis.children.get(&rec.change_id) {
2248 for kid in kids {
4780 for kid in kids {
2249 if let Some(krec) = vis.commits.get(kid) {
4781 if let Some(krec) = vis.commits.get(kid) {
2250− children.push(build_subtree(vis, krec, store, cache, entries)?);
4782+ children.push(build_subtree(vis, krec, store, replay_cache)?);
@@ -2282,3 +4814,165 @@ fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
2282 let root = subtree.field("root").expect("subtree has a root");
4814 let root = subtree.field("root").expect("subtree has a root");
2283 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
4815 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
4821+ use jj_lib::backend::SymlinkId;
4822+ use jj_lib::conflict_labels::ConflictLabels;
4824+ fn replay_store() -> (tempfile::TempDir, Arc<Store>) {
4825+ let dir = tempfile::tempdir().unwrap();
4826+ let (_, repo) = block_on(Workspace::init_colocated_git(
4827+ &user_settings(None), dir.path(), gix::hash::Kind::Sha1,
4829+ (dir, repo.store().clone())
4832+ fn file(path: &str, content: &str) -> Value {
4834+ ("path", Value::list(vec![Value::text(path)])),
4835+ ("content", BlobVal::text_blob(content)),
4839+ fn loaded_files(store: &Arc<Store>, cache: &ReplayCache, files: Vec<Value>) -> Value {
4840+ let tree = block_on(build_tree(store, &Value::list(files))).unwrap();
4841+ block_on(cache.files_of(store, &tree)).unwrap()
4845+ fn replay_reuses_loaded_snapshots_and_unchanged_entries() {
4846+ let (_dir, store) = replay_store();
4847+ let cache = ReplayCache::default();
4848+ let base = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "base b")]);
4849+ let same = block_on(jj_replay(&store, &cache, &base, &base, &base)).unwrap();
4850+ let (Value::List(before), Value::List(after)) = (&base, &same) else { panic!() };
4851+ assert_eq!(before.identity(), after.identity(), "no-op replay must retain the snapshot");
4853+ let onto = loaded_files(&store, &cache, vec![file("a", "onto"), file("b", "base b")]);
4854+ let to = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "to")]);
4855+ let merged = block_on(jj_replay(&store, &cache, &onto, &base, &to)).unwrap();
4856+ let expected = Value::list(vec![file("a", "onto"), file("b", "to")]);
4857+ assert!(crate::value::value_eq(&merged, &expected).unwrap());
4858+ for (result, input) in [
4859+ (&merged.as_list().unwrap()[0], &onto.as_list().unwrap()[0]),
4860+ (&merged.as_list().unwrap()[1], &to.as_list().unwrap()[1]),
4862+ let (Value::Record(result), Value::Record(input)) = (result, input) else { panic!() };
4863+ assert!(Rc::ptr_eq(result, input), "unchanged file entries must be shared");
4868+ fn replay_cache_preserves_validation_ordering_and_list_offsets() {
4869+ let (_dir, store) = replay_store();
4870+ let cache = ReplayCache::default();
4871+ let unsorted = Value::list(vec![file("b", "b"), file("a", "a")]);
4872+ let empty = Value::list(vec![]);
4873+ let sorted = block_on(jj_replay(&store, &cache, &unsorted, &empty, &empty)).unwrap();
4874+ assert!(crate::value::value_eq(&sorted, &Value::list(vec![file("a", "a"), file("b", "b")])).unwrap());
4876+ let Value::List(list) = &sorted else { panic!() };
4877+ let tail = Value::List(list.skip(1));
4878+ let replayed_tail = block_on(jj_replay(&store, &cache, &tail, &empty, &empty)).unwrap();
4879+ assert!(crate::value::value_eq(&tail, &replayed_tail).unwrap());
4880+ assert_eq!(replayed_tail.as_list().unwrap().len(), 1);
4882+ let malformed = Value::list(vec![file("a", "one"), file("a", "two")]);
4883+ let err = block_on(jj_replay(&store, &cache, &sorted, &malformed, &malformed)).unwrap_err();
4884+ assert!(err.msg.contains("duplicate paths"), "{}", err.msg);
4888+ fn replay_ignores_empty_directories_not_represented_by_snapshots() {
4889+ let (_dir, store) = replay_store();
4890+ let cache = ReplayCache::default();
4891+ let entry = |id| jj_lib::backend::Tree::from_sorted_entries(vec![
4892+ (RepoPathComponentBuf::new("d".to_string()).unwrap(), TreeValue::Tree(id)),
4894+ let direct = block_on(store.write_tree(RepoPath::root(), entry(store.empty_tree_id().clone()))).unwrap();
4895+ let nested = block_on(store.write_tree(RepoPath::root(), entry(direct.id().clone()))).unwrap();
4896+ let empty = Value::list(vec![]);
4897+ let to = Value::list(vec![file("d", "file")]);
4898+ for tree in [direct, nested] {
4899+ let tree = MergedTree::resolved(store.clone(), tree.id().clone());
4900+ let onto = block_on(cache.files_of(&store, &tree)).unwrap();
4901+ assert!(onto.as_list().unwrap().is_empty());
4902+ let result = block_on(jj_replay(&store, &cache, &onto, &empty, &to)).unwrap();
4903+ assert!(crate::value::value_eq(&result, &to).unwrap());
4908+ fn replay_cache_does_not_keep_discarded_snapshots_alive() {
4909+ let (_dir, store) = replay_store();
4910+ let cache = ReplayCache::default();
4911+ let files = loaded_files(&store, &cache, vec![file("a", "a")]);
4912+ let Value::List(list) = &files else { panic!() };
4913+ let storage = list.weak_storage();
4914+ let Value::Record(entry) = &files.as_list().unwrap()[0] else { panic!() };
4915+ let entry = Rc::downgrade(entry);
4917+ assert!(storage.upgrade().is_none());
4918+ assert!(entry.upgrade().is_none());
4920+ let bytes = Rc::new(vec![1, 2, 3]);
4921+ let weak_bytes = Rc::downgrade(&bytes);
4922+ let id = FileId::new(vec![1; 20]);
4923+ cache.blobs.insert(id.clone(), bytes.clone());
4924+ assert!(Rc::ptr_eq(&cache.blobs.get(&id).unwrap(), &bytes));
4926+ assert!(weak_bytes.upgrade().is_none());
4927+ assert!(cache.blobs.get(&id).is_none());
4931+ fn user_settings_preserve_every_ascii_control_character() {
4932+ let identity: String = (0u8..=127).map(char::from).collect();
4933+ let text = format!(
4934+ "[user]\nname = {}\nemail = {}\n",
4935+ toml_string(&identity),
4936+ toml_string(&identity)
4938+ let mut config = StackedConfig::with_defaults();
4939+ config.add_layer(ConfigLayer::parse(ConfigSource::User, &text).unwrap());
4940+ let settings = UserSettings::from_config(config).unwrap();
4941+ assert_eq!(settings.user_name(), identity);
4942+ assert_eq!(settings.user_email(), identity);
4946+ fn a_description_names_its_terms_whatever_their_labels_and_the_sides_that_cancel() {
4947+ // what a checkout writes for a symlink/file conflict of five terms,
4948+ // and for the three it simplifies to, which a snapshot may write
4949+ // without writing the file again (§7.4)
4950+ let link = Some(TreeValue::Symlink(SymlinkId::new(vec![1; 20])));
4951+ let file = |n: u8| {
4952+ let id = FileId::new(vec![n; 20]);
4953+ Some(TreeValue::File { id, executable: false, copy_id: CopyId::placeholder() })
4955+ let five = Merge::from_vec(vec![link.clone(), link.clone(), file(2), file(1), link.clone()]);
4956+ let three = five.simplify();
4957+ assert_eq!(three.num_sides(), 2);
4958+ let unlabeled = ConflictLabels::unlabeled();
4959+ let written = five.describe(&unlabeled);
4960+ let simpler = three.describe(&unlabeled);
4961+ assert_ne!(written, simpler);
4962+ let terms = described_terms(simpler.as_bytes());
4963+ assert!(terms.is_some());
4964+ assert_eq!(described_terms(written.as_bytes()), terms);
4965+ let names = ["left (one)", "base", "middle", "base 2", "right"];
4966+ let labels = ConflictLabels::from_vec(names.iter().map(|n| n.to_string()).collect());
4967+ let labelled = five.describe(&labels);
4968+ assert!(labelled.contains(" (left (one))\n"), "{}", labelled);
4969+ assert_eq!(described_terms(labelled.as_bytes()), terms);
4970+ // other sides, and text that is no description, are no such conflict
4971+ let other = Merge::from_vec(vec![link.clone(), file(1), file(3)]).describe(&unlabeled);
4972+ assert!(described_terms(other.as_bytes()).is_some_and(|t| Some(t) != terms));
4973+ assert_eq!(described_terms(b"resolved\n"), None);
4974+ assert_eq!(described_terms(written.replace("Conflict:", "CONFLICT:").as_bytes()), None);
4975+ assert_eq!(described_terms(written.trim_end().as_bytes()), None);
4976+ assert_eq!(described_terms(format!("{} Adding file with id 0101 and more\n", written).as_bytes()), None);