@@ -6,38 +6,33 @@
66use crate::config::Config;
77use crate::domain::{Backend, MetaInfo, ROOT_ID};
88use crate::eval::Interp;
9−use crate::value::{BlobContent, BlobKind, BlobVal, ConflictSide, Crash, LazyBlob, ListVal, ThunkVal, Value};
10−use futures::{StreamExt, TryStreamExt};
9+use crate::value::{BlobContent, BlobKind, BlobVal, Crash, LazyBlob, Value};
1110use jj_lib::backend::{CopyId, Signature, Timestamp, TreeValue};
1211use jj_lib::commit::Commit;
1312use jj_lib::config::{ConfigLayer, ConfigSource, StackedConfig};
14−use jj_lib::conflicts::ConflictMarkerStyle;
1513use jj_lib::default_backend_factories::{default_backend_factories, default_working_copy_factories};
1614use jj_lib::gitignore::GitIgnoreFile;
17−use jj_lib::local_working_copy::{FileState, FileStates, FileType, LocalWorkingCopy, TreeState, TreeStateSettings};
18−use jj_lib::matchers::{DifferenceMatcher, EverythingMatcher, FilesMatcher, NothingMatcher};
15+use jj_lib::matchers::{EverythingMatcher, NothingMatcher};
1916use jj_lib::merge::{Diff, Merge};
2017use jj_lib::merged_tree::MergedTree;
2118use jj_lib::merged_tree_builder::MergedTreeBuilder;
22−use jj_lib::backend::{CommitId, FileId, MergedTreeValueExt, TreeId};
19+use jj_lib::backend::{CommitId, FileId};
2320use jj_lib::object_id::ObjectId;
2421use jj_lib::op_store::OperationId;
2522use jj_lib::ref_name::{RefName, RemoteName, WorkspaceName, WorkspaceNameBuf};
2623use jj_lib::repo::{ReadonlyRepo, Repo};
27−use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponent, RepoPathComponentBuf};
28−use jj_lib::revset::{ResolvedRevsetExpression, RevsetStreamExt};
24+use jj_lib::repo_path::{RepoPath, RepoPathBuf, RepoPathComponentBuf};
2925use jj_lib::settings::UserSettings;
3026use jj_lib::store::Store;
31−use jj_lib::transaction::UnpublishedOperation;
32−use jj_lib::working_copy::{LockedWorkingCopy, SnapshotError, SnapshotOptions};
27+use jj_lib::working_copy::SnapshotOptions;
3328use jj_lib::workspace::Workspace;
34−use std::cell::{Cell, RefCell};
29+use std::cell::RefCell;
3530use std::collections::{BTreeMap, BTreeSet, HashMap};
3631use std::future::Future;
3732use std::os::unix::io::AsRawFd;
3833use std::path::PathBuf;
39−use std::rc::{Rc, Weak};
40−use std::sync::{Arc, Mutex, OnceLock};
34+use std::rc::Rc;
35+use std::sync::{Arc, Mutex};
4136
4237fn block_on<F: Future>(f: F) -> F::Output {
4338 pollster::block_on(f)
@@ -45,15 +40,6 @@ fn block_on<F: Future>(f: F) -> F::Output {
4540
4641type OpenError = (u8, String);
4742
48−/// The successful effects of a push, after duplicate requests are merged.
49−/// All zeroes means no bookmarks were selected and nothing was sent or saved.
50−#[derive(Clone, Debug, Default, PartialEq, Eq)]
51−pub struct PushOutcome {
52− pub published: usize,
53− pub deleted: usize,
54− pub unchanged: usize,
55−}
56−
5743// ----------------------------------------------------------------------
5844// locating and opening the repository (§7.1)
5945// ----------------------------------------------------------------------
@@ -78,7 +64,6 @@ fn toml_string(s: &str) -> String {
7864 '\n' => out.push_str("\\n"),
7965 '\r' => out.push_str("\\r"),
8066 '\t' => out.push_str("\\t"),
81− c if c.is_ascii_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
8267 c => out.push(c),
8368 }
8469 }
@@ -267,1166 +252,31 @@ pub struct JjInner {
267252 repo: Mutex<Arc<ReadonlyRepo>>,
268253 workspace_name: WorkspaceNameBuf,
269254 /// visible map backing the `Backend` lookups during evaluation; filled by
270− /// build_interp with the post-snapshot map, or the loaded one when the
271− /// snapshot goes into a new child of an immutable focus (§7.2)
255+ /// build_interp with the post-snapshot map
272256 visible: Mutex<Option<Arc<VisibleRepo>>>,
273257 lock_guard: Mutex<Option<FileLock>>,
274− /// the snapshot's unpublished rewrite, kept between build_interp and
275− /// persist so the snapshot and the expression's edits form one jj
276− /// operation (§1.2); dropped uncommitted by printing runs. The
277− /// working-copy lock is not held: its saved state is still the tree
278− /// recorded before the snapshot until `persist`'s checkout scans the
279− /// directory again
258+ /// the snapshot transaction and working-copy lock, held between
259+ /// build_interp and persist so the snapshot and the expression's edits
260+ /// form one jj operation (§1.2); dropped uncommitted by printing runs
280261 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>>,
287262}
288263
289264/// holding the file keeps the flock
290265pub struct FileLock(#[allow(dead_code)] std::fs::File);
291266
267+#[allow(dead_code)]
292268struct PendingSnapshot {
293269 /// repo as loaded at head, before the snapshot
294270 pre_repo: Arc<ReadonlyRepo>,
295− /// the snapshot's tree for the wc commit, to be folded into the
271+ /// the snapshot's rewritten wc commit and tree, to be folded into the
296272 /// persisting operation (§1.2 step 8, §7.7)
273+ new_wc_id: CommitId,
297274 tree: MergedTree,
298− /// false when the wc commit is immutable: the snapshot then belongs to
299− /// the focus's new child (§7.2), and the wc commit is kept as stored
300− fold_into_wc: bool,
301− /// the tree the scan found in the working directory, which `persist`'s
302− /// checkout starts from: `tree` itself, unless the working copy was
303− /// stale and `tree` merges the directory's changes onto the commit
304− /// (`snapshot_tree`)
305− scanned: MergedTree,
306−}
307−
308−/// How every scan of the working directory reads it (§7.4): each file
309−/// not ignored by the directory's own `.gitignore`s is tracked, whatever
310−/// its size. `persist`'s checkout rescans with the same options and
311−/// relies on getting the snapshot's tree back.
312−fn snapshot_options() -> SnapshotOptions<'static> {
313− SnapshotOptions {
314− base_ignores: GitIgnoreFile::empty(),
315− progress: None,
316− start_tracking_matcher: &EverythingMatcher,
317− force_tracking_matcher: &NothingMatcher,
318− max_new_file_size: u64::MAX,
319− }
320−}
321−
322−/// Scan the working directory with `options`, as `wc.snapshot` does, where
323−/// `base` is the tree the working copy's state records, as `saved` holds
324−/// it; return the tree scanned. jj's scan loses a regular file found where
325−/// `base` has a conflict it cannot write a file into, one with a side that
326−/// is no file, and keeps the conflict, so the file reaches no tree (§7.4):
327−///
328−/// - at a conflicted path, where a checkout wrote jj's description of the
329−/// sides and a file written since holds something else
330−/// (`FilesOnConflicts::at`). The state is first reset to a resolved file
331−/// there, so that the scan reads the file whole, as it reads any tracked
332−/// file; the path stays tracked where a `.gitignore` has come to match it.
333−/// - above one, where `base` has a directory holding a conflict, whose
334−/// differing sides the scan keeps at the path while it removes the
335−/// entries below (and a debug build fails an assertion). Such a file is
336−/// first left untracked, which scans it as if it had been moved away and
337−/// records the directory's removal; a second scan then records the file
338−/// where nothing is, as a later run would once it was put back.
339−async fn scan(
340− wc: &mut dyn LockedWorkingCopy,
341− root: &std::path::Path,
342− base: &MergedTree,
343− saved: &SavedState,
344− options: &SnapshotOptions<'_>,
345−) -> Result<MergedTree, String> {
346− let files = files_on_conflicts(root, base, Some(saved))?;
347− let mut state = base.clone();
348− if !files.at.is_empty() {
349− // the empty file: a reset records the file at each path it changes
350− // as an empty one, not executable, modified at the epoch, so the
351− // scan reads any other file there, and keeps this one for such a file
352− let store = base.store();
353− let id = store.write_file(RepoPath::root(), &mut &[][..]).await.map_err(|e| e.to_string())?;
354− let copy_id = CopyId::placeholder();
355− let empty = Merge::normal(TreeValue::File { id, executable: false, copy_id });
356− state = reset_paths(wc, &state, files.at.into_iter().map(|path| (path, empty.clone()))).await?;
357− }
358− if !files.over.is_empty() {
359− let untracked = FilesMatcher::new(&files.over);
360− let start = DifferenceMatcher::new(options.start_tracking_matcher, &untracked);
361− let first = SnapshotOptions { start_tracking_matcher: &start, ..options.clone() };
362− state = scan_pass(wc, root, &state, &first).await?;
363− // a state that agrees with its tree tracks every entry below the
364− // directory, so none is left; were a conflict left below, the
365− // second scan would lose the file after all
366− if let Some(path) = files_on_conflicts(root, &state, None)?.over.first() {
367− return Err(format!(
368− "cannot record the file `{}` in place of a directory holding a conflict",
369− path.as_internal_file_string()
370− ));
371− }
372− }
373− scan_pass(wc, root, &state, options).await
374−}
375−
376−/// `wc.snapshot(options)`, where `state` is the tree the working copy's
377−/// state records; return the tree scanned. jj's scan reads a directory a
378−/// `.gitignore` ignores only at the paths the state tracks there, and where
379−/// it cannot look at one of them because a directory above it has been
380−/// replaced by a file, it fails instead of taking the path for deleted, as
381−/// it takes one it does not find. The paths below a file are then dropped
382−/// from the state, which records them as deleted, and the directory is
383−/// scanned again (§7.4). A scan fails so while it walks the directory,
384−/// before it changes anything in the state.
385−async fn scan_pass(
386− wc: &mut dyn LockedWorkingCopy,
387− root: &std::path::Path,
388− state: &MergedTree,
389− options: &SnapshotOptions<'_>,
390−) -> Result<MergedTree, String> {
391− let err = match wc.snapshot(options).await {
392− Ok((scanned, _stats)) => return Ok(scanned),
393− Err(err) => err,
394− };
395− let not_a_dir = |e: &std::io::Error| e.kind() == std::io::ErrorKind::NotADirectory;
396− let SnapshotError::Other { err: source, .. } = &err else {
397− return Err(err.to_string());
398− };
399− if !source.downcast_ref::<std::io::Error>().is_some_and(not_a_dir) {
400− return Err(err.to_string());
401− }
402− // one `lstat` per path the state tracks, only once a scan has failed
403− let mut gone = Vec::new();
404− for (path, value) in state.entries() {
405− value.map_err(|e| e.to_string())?;
406− let Ok(disk) = path.to_fs_path(root) else {
407− continue;
408− };
409− if disk.symlink_metadata().is_err_and(|e| not_a_dir(&e)) {
410− gone.push((path, Merge::absent()));
411− }
412− }
413− if gone.is_empty() {
414− return Err(err.to_string());
415− }
416− reset_paths(wc, state, gone).await?;
417− let (scanned, _stats) = wc.snapshot(options).await.map_err(|e| e.to_string())?;
418− Ok(scanned)
419−}
420−
421−/// Give the working copy's state, which records `state`, the tree `state`
422−/// with `values` at their paths, and return it; nothing in the working
423−/// directory is read or written. A reset takes each path it changes for one
424−/// the next scan is to read again.
425−async fn reset_paths(
426− wc: &mut dyn LockedWorkingCopy,
427− state: &MergedTree,
428− values: impl IntoIterator<Item = (RepoPathBuf, jj_lib::backend::MergedTreeValue)>,
429−) -> Result<MergedTree, String> {
430− let mut builder = MergedTreeBuilder::new(state.clone());
431− for (path, value) in values {
432− builder.set_or_remove(path, value);
433− }
434− let tree = builder.write_tree().await.map_err(|e| e.to_string())?;
435− // a reset reads only the commit's tree
436− let commit = with_tree(&state.store().root_commit(), tree.clone());
437− wc.reset(&commit).await.map_err(|e| e.to_string())?;
438− Ok(tree)
439−}
440−
441−/// Where the working directory holds a regular file that jj's scan reads
442−/// into no tree, as `tree` has a conflict there it cannot write a file into
443−struct FilesOnConflicts {
444− /// the conflicted paths with a side that is no file (a directory, a
445− /// symlink, a submodule), which a checkout writes as jj's description of
446− /// the sides, whose file was written since and describes no such
447− /// conflict
448− at: Vec<RepoPathBuf>,
449− /// the directories holding a conflict, that is those above a conflicted
450− /// path: a directory's sides differ exactly when a conflict is below
451− /// it, as jj writes no tree whose sides differ where every path resolves
452− over: Vec<RepoPathBuf>,
453−}
454−
455−/// Where the working copy saves its state, to read back what the state a
456−/// scan's lock loaded records of a file (`files_on_conflicts`). Nothing is
457−/// read until a scan asks.
458−struct SavedState {
459− store: Arc<Store>,
460− root: PathBuf,
461− path: PathBuf,
462− settings: UserSettings,
463−}
464−
465−impl SavedState {
466− /// Where `ws` saves its working copy's state; taken before the lock,
467− /// which borrows `ws`
468− fn of(ws: &Workspace) -> SavedState {
469− let root = ws.workspace_root().to_owned();
470− let path = match ws.working_copy().downcast_ref::<LocalWorkingCopy>() {
471− Some(wc) => wc.state_path().to_owned(),
472− None => root.join(".jj").join("working_copy"),
473− };
474− SavedState { store: ws.repo_loader().store().clone(), root, path, settings: ws.settings().clone() }
475− }
476−
477− /// The state as saved, where it records `tree`, with the time it was
478− /// saved; None where it cannot be read or records another tree. A state
479− /// file that is not there is not read, as jj would write one to read.
480− fn load(&self, tree: &MergedTree) -> Option<(TreeState, i64)> {
481− let meta = self.path.join("tree_state").symlink_metadata().ok()?;
482− let saved_at = millis(meta.modified().ok()?)?;
483− let settings = TreeStateSettings::try_from_user_settings(&self.settings).ok()?;
484− let state = TreeState::load(self.store.clone(), self.root.clone(), self.path.clone(), &settings).ok()?;
485− (state.current_tree().tree_ids() == tree.tree_ids()).then_some((state, saved_at))
486− }
487−}
488−
489−/// A modification time in milliseconds since the epoch, as jj's working
490−/// copy records one
491−fn millis(time: std::time::SystemTime) -> Option<i64> {
492− match time.duration_since(std::time::UNIX_EPOCH) {
493− Ok(d) => i64::try_from(d.as_millis()).ok(),
494− Err(e) => i64::try_from(e.duration().as_millis()).ok().map(|m| -m),
495− }
496−}
497−
498−/// Whether the working copy's state, saved at `saved_at`, records `state`
499−/// for the regular file `meta` describes as it is now: jj's own test of a
500−/// file left as it was (`FileState::is_clean`, the executable bit aside),
501−/// under which it reads nothing. A file modified in the millisecond the
502−/// state was saved, or later, may have been written after it was.
503−fn left_as_recorded(state: &FileState, meta: &std::fs::Metadata, saved_at: i64) -> bool {
504− matches!(state.file_type, FileType::Normal { .. })
505− && state.size == meta.len()
506− && meta.modified().ok().and_then(millis) == Some(state.mtime.0)
507− && state.mtime.0 < saved_at
508−}
509−
510−/// The terms of a conflict that `text`, jj's description of one, names:
511−/// those removed and those added, each without the label a checkout may
512−/// write after it, with each removed that is also added cancelled against
513−/// it, as simplifying the conflict does, sorted; None for text that is no
514−/// description. Two descriptions naming the same terms describe one
515−/// conflict, whatever its labels or the sides it has lost since.
516−fn described_terms(text: &[u8]) -> Option<(Vec<&str>, Vec<&str>)> {
517− let lines = std::str::from_utf8(text).ok()?.strip_prefix("Conflict:\n")?;
518− if !lines.is_empty() && !lines.ends_with('\n') {
519− return None;
520− }
521− let (mut removes, mut adds) = (Vec::new(), Vec::new());
522− for line in lines.split_terminator('\n') {
523− let (terms, term) = match (line.strip_prefix(" Removing "), line.strip_prefix(" Adding ")) {
524− (Some(term), _) => (&mut removes, term),
525− (_, Some(term)) => (&mut adds, term),
526− _ => return None,
527− };
528− // a term ends with its object's id, which holds no space
529− let id = term.find(" with id ")? + " with id ".len();
530− let end = term[id..].find(' ').map_or(term.len(), |n| id + n);
531− let (term, label) = term.split_at(end);
532− if !label.is_empty() && !(label.starts_with(" (") && label.ends_with(')')) {
533− return None;
534− }
535− terms.push(term);
536− }
537− removes.retain(|term| match adds.iter().position(|t| t == term) {
538− Some(n) => {
539− adds.swap_remove(n);
540− false
541− }
542− None => true,
543− });
544− removes.sort_unstable();
545− adds.sort_unstable();
546− Some((removes, adds))
547−}
548−
549−/// Look on disk at `tree`'s conflicts for `FilesOnConflicts`; with no
550−/// `saved`, for `over` only. Nothing is looked at when `tree` is resolved;
551−/// each directory above a conflict at most once, with one `lstat`; and a
552−/// conflict with a side that is no file below directories, with one more,
553−/// and a read where its file is as long as its description. Only a file
554−/// that holds something else is looked up in the saved state, which is read
555−/// once, and read itself where the state records it otherwise.
556−fn files_on_conflicts(
557− root: &std::path::Path,
558− tree: &MergedTree,
559− saved: Option<&SavedState>,
560−) -> Result<FilesOnConflicts, String> {
561− let mut files = FilesOnConflicts { at: Vec::new(), over: Vec::new() };
562− // whether each directory looked at so far is one on disk
563− let mut dirs: HashMap<RepoPathBuf, bool> = HashMap::new();
564− // the state as saved, read once a file differs from its description,
565− // and what it records of the files, taken once
566− let loaded: std::cell::OnceCell<Option<(TreeState, i64)>> = std::cell::OnceCell::new();
567− let recorded: std::cell::OnceCell<Option<(FileStates<'_>, i64)>> = std::cell::OnceCell::new();
568− for (path, value) in tree.conflicts() {
569− let value = value.map_err(|e| e.to_string())?;
570− let parent = path.parent().expect("a conflicted path has a parent");
571− let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
572− // from the top down, stopping at the first that is not a directory
573− let below_dirs = above.into_iter().rev().all(|dir| match dirs.get(dir) {
574− Some(&is_dir) => is_dir,
575− None => {
576− let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
577− if meta.as_ref().is_some_and(|m| m.is_file()) {
578− files.over.push(dir.to_owned());
579− }
580− let is_dir = meta.is_some_and(|m| m.is_dir());
581− dirs.insert(dir.to_owned(), is_dir);
582− is_dir
583− }
584− });
585− let Some(saved) = saved.filter(|_| below_dirs && value.to_file_merge().is_none()) else {
586− continue;
587− };
588− let Some((disk, meta)) = path.to_fs_path(root).ok().and_then(|disk| {
589− let meta = disk.symlink_metadata().ok()?;
590− meta.is_file().then_some((disk, meta))
591− }) else {
592− continue;
593− };
594− // what jj's checkout writes for it (§7.4)
595− let written = value.describe(tree.labels());
596− let mut bytes = None;
597− if meta.len() == written.len() as u64 {
598− match std::fs::read(&disk) {
599− Ok(read) if read == written.as_bytes() => continue,
600− Ok(read) => bytes = Some(read),
601− Err(_) => continue,
602− }
603− }
604− // the file a checkout wrote, left as it was: it describes the
605− // conflict as the checkout had it, which the state may since
606− // record otherwise with no file written, as a scan that drops a
607− // pair of sides that cancel, or a reset that only relabels, does
608− let states = recorded.get_or_init(|| {
609− let (state, saved_at) = loaded.get_or_init(|| saved.load(tree)).as_ref()?;
610− Some((state.file_states(), *saved_at))
611− });
612− if states.as_ref().is_some_and(|(file_states, saved_at)| {
613− file_states.get(&path).is_some_and(|state| left_as_recorded(&state, &meta, *saved_at))
614− }) {
615− continue;
616− }
617− // written since, or not known to be left: the conflict is kept
618− // while the file describes it, as when an editor saves it unchanged
619− let Some(bytes) = bytes.or_else(|| std::fs::read(&disk).ok()) else {
620− continue;
621− };
622− let unlabelled = value.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
623− let terms = described_terms(unlabelled.as_bytes());
624− if terms.is_none() || described_terms(&bytes) != terms {
625− files.at.push(path);
626− }
627− }
628− Ok(files)
629−}
630−
631−/// The tree a snapshot records in the working-copy commit, whose tree is
632−/// `wc`, where the working copy's state records `state` and a scan of the
633−/// working directory found `scanned` (§7.4): `scanned` itself where the
634−/// state is the commit's, and also where it is `current`, saved at the
635−/// operation the repository is at, which jj takes for the files last
636−/// checked out whatever it records: one lost, which jj makes again
637−/// recording no file, or one put in its place. Such a state may record none
638−/// of the commit's conflicts, so a file holding what a checkout wrote for
639−/// one is read back as it there (`conflicts_written`). Otherwise the state
640−/// is stale (a checkout that failed or was cut short after its operation
641−/// was recorded, a `fetch` that rebased the commit, jj run with
642−/// `--ignore-working-copy` or from another workspace), and what the
643−/// directory holds is a change to `state` rather than to the commit: that
644−/// change is replayed onto the commit, a three-way merge with `state` as
645−/// the base, in which a path both changed differently is a conflict, but
646−/// for one setting a directory against anything else, which takes what the
647−/// working directory holds there, as where the state is current. What a
648−/// checkout cut short wrote is the commit's own content, which the merge
649−/// takes as it is: where the directory holds what the commit does, the base
650−/// is taken to hold it too, so that a conflict the state records there,
651−/// whose sides would not cancel against the commit's, does not make one of
652−/// a change both made. That includes a conflict the checkout wrote, which
653−/// the scan read as its text (`conflicts_written`).
654−async fn snapshot_tree(
655− root: &std::path::Path,
656− markers: ConflictMarkerStyle,
657− wc: &MergedTree,
658− state: &MergedTree,
659− current: bool,
660− scanned: &MergedTree,
661−) -> Result<MergedTree, String> {
662− if state.tree_ids() == wc.tree_ids() {
663− return Ok(scanned.clone());
664− }
665− if current {
666− return conflicts_written(root, markers, wc, state, scanned).await;
667− }
668− if scanned.tree_ids() == state.tree_ids() {
669− return Ok(wc.clone());
670− }
671− let scanned = &conflicts_written(root, markers, wc, state, scanned).await?;
672− let err = |e: jj_lib::backend::BackendError| e.to_string();
673− // the paths where the directory holds something other than the commit
674− let mut apart = std::collections::HashSet::new();
675− let mut diff = wc.diff_stream(scanned, &EverythingMatcher);
676− while let Some(entry) = diff.next().await {
677− entry.values.map_err(err)?;
678− apart.insert(entry.path);
679− }
680− // the base: the state, but for what the directory changed as the
681− // commit did
682− let mut base = MergedTreeBuilder::new(state.clone());
683− let mut same = false;
684− let mut diff = state.diff_stream(scanned, &EverythingMatcher);
685− while let Some(entry) = diff.next().await {
686− let Diff { after, .. } = entry.values.map_err(err)?;
687− if !apart.contains(&entry.path) {
688− base.set_or_remove(entry.path, after);
689− same = true;
690− }
691− }
692− let base = if same { base.write_tree().await.map_err(err)? } else { state.clone() };
693− let merged = MergedTree::merge(Merge::from_removes_adds(
694− vec![(base.clone(), "last checkout".to_string())],
695− vec![
696− (wc.clone(), "working-copy commit".to_string()),
697− (scanned.clone(), "working directory".to_string()),
698− ],
699− ))
700− .await
701− .map_err(err)?;
702− // A conflict setting a directory against a file, a symlink or nothing,
703− // which jj keeps at the path itself, cannot be a blob that lists the
704− // directory's entries (§7.3), and a checkout would write jj's
705− // description of it in place of what the directory holds there,
706− // removing a directory's files: where the directory changed the path,
707− // take what it holds, as a snapshot over a current state does. The
708− // commit's change there is left in the operation before. Where the
709− // directory holds what the base does, the merge took the commit's value
710− // as it is, so such a conflict is one the commit already has, and it
711− // stays. A tree with no conflict is not walked.
712− let mut kept = MergedTreeBuilder::new(merged.clone());
713− let mut mixed = false;
714− for (path, value) in merged.conflicts() {
715− if value.map_err(err)?.iter().any(|term| matches!(term, Some(TreeValue::Tree(_)))) {
716− let held = scanned.path_value(&path).await.map_err(err)?;
717− if held == base.path_value(&path).await.map_err(err)? {
718− continue;
719− }
720− kept.set_or_remove(path, held);
721− mixed = true;
722− }
723− }
724− if !mixed {
725− return Ok(merged);
726− }
727− kept.write_tree().await.map_err(err)
728−}
729−
730−/// `scanned` with the commit's conflict at each path the working-copy state
731−/// records something else at, where the file holds what jj's checkout
732−/// writes for that conflict (`holds_written`): its markers, or its
733−/// description where a side is no file (§7.4). A checkout records the
734−/// conflict in the state as it writes the file, so a scan reads such a file
735−/// back as the conflict; one cut short, or one over a stale state, records
736−/// nothing, nor does a state made again where it was lost, and the scan
737−/// reads the file as the text it holds. Each of the commit's conflicts is
738−/// looked at, and a file read, only where neither the state nor the scan
739−/// has the conflict.
740−async fn conflicts_written(
741− root: &std::path::Path,
742− markers: ConflictMarkerStyle,
743− wc: &MergedTree,
744− state: &MergedTree,
745− scanned: &MergedTree,
746−) -> Result<MergedTree, String> {
747− let err = |e: jj_lib::backend::BackendError| e.to_string();
748− let mut tree = MergedTreeBuilder::new(scanned.clone());
749− let mut found = false;
750− for (path, value) in wc.conflicts() {
751− let value = value.map_err(err)?;
752− if state.path_value(&path).await.map_err(err)? == value
753− || scanned.path_value(&path).await.map_err(err)? == value
754− {
755− continue;
756− }
757− let Ok(disk) = path.to_fs_path(root) else {
758− continue;
759− };
760− if holds_written(wc.store(), &path, value.clone(), wc.labels(), markers, &disk).await? {
761− tree.set_or_remove(path, value);
762− found = true;
763− }
764− }
765− if !found {
766− return Ok(scanned.clone());
767− }
768− tree.write_tree().await.map_err(err)
769−}
770−
771−/// Whether `disk`, the entry at `path`, holds what a checkout writes there
772−/// for `value`: a file's content or a symlink's target, whatever its
773−/// executable bit, or, for a conflict, exactly its markers, compared byte
774−/// for byte (j's settings convert no line endings), or, where a side is no
775−/// file, a description of the same sides, whatever their labels and less
776−/// any it both removes and adds, as a scan reads one back
777−/// (`files_on_conflicts`); nothing is written to the store
778−async fn holds_written(
779− store: &Arc<Store>,
780− path: &RepoPath,
781− value: jj_lib::backend::MergedTreeValue,
782− labels: &jj_lib::conflict_labels::ConflictLabels,
783− markers: ConflictMarkerStyle,
784− disk: &std::path::Path,
785−) -> Result<bool, String> {
786− use jj_lib::conflicts::{self, MaterializedTreeValue};
787− let symlink = matches!(value.as_normal(), Some(TreeValue::Symlink(_)));
788− let Some(meta) = disk.symlink_metadata().ok().filter(|m| if symlink { m.is_symlink() } else { m.is_file() })
789− else {
790− return Ok(false);
791− };
792− let written = conflicts::materialize_tree_value(store, path, value, labels)
793− .await
794− .map_err(|e| e.to_string())?;
795− match written {
796− MaterializedTreeValue::Symlink { target, .. } => {
797− Ok(std::fs::read_link(disk).is_ok_and(|t| t.as_os_str() == target.as_str()))
798− }
799− MaterializedTreeValue::File(mut file) => {
800− let bytes = file.read_all(path).await.map_err(|e| e.to_string())?;
801− Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
802− }
803− MaterializedTreeValue::FileConflict(file) => {
804− let options = conflicts::ConflictMaterializeOptions {
805− marker_style: markers,
806− marker_len: Some(conflicts::choose_materialized_conflict_marker_len(&file.contents)),
807− merge: store.merge_options().clone(),
808− };
809− let bytes: Vec<u8> =
810− conflicts::materialize_merge_result_to_bytes(&file.contents, &file.labels, &options).into();
811− Ok(meta.len() == bytes.len() as u64 && std::fs::read(disk).is_ok_and(|b| b == bytes))
812− }
813− MaterializedTreeValue::OtherConflict { id, .. } => {
814− let unlabelled = id.describe(&jj_lib::conflict_labels::ConflictLabels::unlabeled());
815− let terms = described_terms(unlabelled.as_bytes());
816− Ok(terms.is_some() && std::fs::read(disk).is_ok_and(|b| described_terms(&b) == terms))
817− }
818− _ => Ok(false),
819− }
820−}
821−
822−/// How the working copy writes a conflict, as `settings` say
823−fn marker_style(settings: &UserSettings) -> Result<ConflictMarkerStyle, String> {
824− Ok(TreeStateSettings::try_from_user_settings(settings).map_err(|e| e.to_string())?.conflict_marker_style)
825−}
826−
827−/// The first of the directories between the root and `path`, from the top
828−/// down, that is no directory on disk, with the type of what stands there
829−/// (none where nothing does, or it cannot be looked at), or none if each
830−/// is: a symlink is not one, and is not followed, as no directory below
831−/// one is looked at. Each directory not looked at before costs one
832−/// `lstat`, and what it found is kept in `dirs`.
833−fn first_not_a_dir(
834− root: &std::path::Path,
835− path: &RepoPath,
836− dirs: &mut HashMap<RepoPathBuf, Option<std::fs::FileType>>,
837−) -> Option<(RepoPathBuf, Option<std::fs::FileType>)> {
838− let parent = path.parent()?;
839− let above: Vec<&RepoPath> = parent.ancestors().take_while(|d| !d.is_root()).collect();
840− for dir in above.into_iter().rev() {
841− let stands = match dirs.get(dir) {
842− Some(&stands) => stands,
843− None => {
844− let meta = dir.to_fs_path(root).ok().and_then(|p| p.symlink_metadata().ok());
845− let stands = meta.map(|m| m.file_type());
846− dirs.insert(dir.to_owned(), stands);
847− stands
848− }
849− };
850− if !stands.is_some_and(|t| t.is_dir()) {
851− return Some((dir.to_owned(), stands));
852− }
853− }
854− None
855−}
856−
857−/// The first path where the working directory, scanned again as
858−/// `rescanned`, no longer holds what it held as `on_disk`, the run's
859−/// snapshot of it, and a checkout from `on_disk` to `to` writes or removes
860−/// a file: a change made while the program ran, which no snapshot holds and
861−/// jj's checkout would write over without looking (§7.5 step 7). Paths are
862−/// compared file by file, as a directory is by what it holds; one lookup
863−/// in `to` per path the directory changed at.
864−async fn written_over(
865− on_disk: &MergedTree,
866− rescanned: &MergedTree,
867− to: &MergedTree,
868−) -> Result<Option<RepoPathBuf>, String> {
869− let err = |e: jj_lib::backend::BackendError| e.to_string();
870− let mut diff = on_disk.diff_stream(rescanned, &EverythingMatcher);
871− while let Some(entry) = diff.next().await {
872− let Diff { before, .. } = entry.values.map_err(err)?;
873− // a diff has no tree on either side
874− let focus = to.path_value(&entry.path).await.map_err(err)?;
875− let focus = if focus.is_tree() { Merge::absent() } else { focus };
876− if focus != before {
877− return Ok(Some(entry.path));
878− }
879− }
880− Ok(None)
881−}
882−
883−/// What stands in the way of a path a checkout adds: `holder`, in the
884−/// working directory, which does not track it, at `path`, above it, or in
885−/// a directory at it
886−struct InTheWay {
887− path: RepoPathBuf,
888− holder: RepoPathBuf,
889−}
890−
891−impl InTheWay {
892− /// Whether it is a directory at `path`, holding `holder`
893− fn is_dir(&self) -> bool {
894− self.holder != self.path && self.holder.starts_with(&self.path)
895− }
896−
897− /// What a refused checkout says of it among others: a directory by its
898− /// own path, with what it holds, anything else by its path
899− fn named(&self) -> String {
900− if self.is_dir() {
901− format!(
902− "`{}` (a directory holding untracked files, such as `{}`)",
903− self.path.as_internal_file_string(),
904− self.holder.as_internal_file_string()
905− )
906− } else {
907− format!("`{}`", self.holder.as_internal_file_string())
908− }
909− }
910−}
911−
912−/// Why a checkout is refused over `in_the_way`, which is not empty, as
913−/// git names each untracked file in the way: one with the path the focus
914−/// has there, more by listing the first ten and counting the rest
915−fn in_the_way_of(in_the_way: &[InTheWay]) -> String {
916− const LISTED: usize = 10;
917− match in_the_way {
918− [one] if one.is_dir() => format!(
919− "it has `{0}`, and `{0}` in the working directory is a directory holding untracked files, such as `{1}`; move it aside",
920− one.path.as_internal_file_string(),
921− one.holder.as_internal_file_string()
922− ),
923− [one] => format!(
924− "it has `{}`, and an untracked `{}` in the working directory is in its way; move it aside",
925− one.path.as_internal_file_string(),
926− one.holder.as_internal_file_string()
927− ),
928− _ => {
929− let mut named: Vec<String> = in_the_way.iter().take(LISTED).map(InTheWay::named).collect();
930− let last = match in_the_way.len() - named.len() {
931− 0 => named.pop().unwrap_or_default(),
932− more => format!("{} more", more),
933− };
934− format!(
935− "untracked files in the working directory are in the way of files it has: {} and {}; move them aside",
936− named.join(", "),
937− last
938− )
939− }
940− }
941−}
942−
943−/// What stands in the working directory where a checkout from `from`, the
944−/// tree the directory holds as scanned, to `to` adds a file (or a symlink,
945−/// or a conflict): at each path where `to` has one and `from` has none.
946−/// jj's checkout writes nothing over anything already there, and skips
947−/// the path without failing, recording it as written, so the next scan
948−/// would read what stands there into the focus, an ignored file's content
949−/// in place of the focus's, or take the focus's file for deleted (§7.4).
950−/// What `from` tracks there the checkout removes first; trees of empty
951−/// directories, which no scan sees, and a file holding just what the
952−/// checkout writes there (`holds_written`), as a checkout cut short leaves
953−/// at an ignored path, are cleared for it (`clear_the_way`). The result is
954−/// the paths where nothing else stands, or, walking the whole diff all the
955−/// same, each thing standing where anything else does (`InTheWay`), in
956−/// path order, what stands above several paths once. No symlink is
957−/// followed. Each path costs one `lstat`, and each directory above one not
958−/// looked at before one more; a file standing at a path is read, and a
959−/// directory read until something untracked is found in it, each entry in
960−/// it that is no directory looked up in `from`.
961−async fn look_where_added(
962− root: &std::path::Path,
963− markers: ConflictMarkerStyle,
964− from: &MergedTree,
965− to: &MergedTree,
966−) -> Result<Result<Vec<RepoPathBuf>, Vec<InTheWay>>, String> {
967− let err = |e: jj_lib::backend::BackendError| e.to_string();
968− let mut dirs = HashMap::new();
969− // each thing that is no directory looked up in `from`, above a path
970− let mut looked_up = std::collections::HashSet::new();
971− let mut clear = Vec::new();
972− let mut in_the_way = Vec::new();
973− let mut diff = from.diff_stream(to, &EverythingMatcher);
974− while let Some(entry) = diff.next().await {
975− let Diff { before, after } = entry.values.map_err(err)?;
976− // a submodule's directory is the submodule's own, which jj keeps
977− if before.is_present() || after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
978− continue;
979− }
980− let path = entry.path;
981− if let Some((dir, stands)) = first_not_a_dir(root, &path, &mut dirs) {
982− // where nothing stands above the path, or what `from` tracks
983− // and the checkout removes, nothing stands at it
984− if stands.is_some() && looked_up.insert(dir.clone()) {
985− let is_tracked = from.path_value(&dir).await.map_err(err)?.is_present();
986− if !is_tracked {
987− in_the_way.push(InTheWay { path, holder: dir });
988− }
989− }
990− continue;
991− }
992− let Ok(disk) = path.to_fs_path(root) else {
993− continue;
994− };
995− let Ok(meta) = disk.symlink_metadata() else {
996− continue;
997− };
998− let holder = if meta.is_dir() {
999− untracked_below(&disk, &path, from)
1000− } else if holds_written(to.store(), &path, after, to.labels(), markers, &disk).await? {
1001− None
1002− } else {
1003− Some(path.clone())
1004− };
1005− match holder {
1006− Some(holder) => in_the_way.push(InTheWay { path, holder }),
1007− None => clear.push(path),
1008− }
1009− }
1010− if in_the_way.is_empty() {
1011− Ok(Ok(clear))
1012− } else {
1013− Ok(Err(in_the_way))
1014− }
1015−}
1016−
1017−/// Something that is no directory in the directory `disk`, standing at
1018−/// `path`, or in a directory in it, that `from` does not track, or a
1019−/// directory that cannot be read; none where it holds nothing else. Entries
1020−/// are told apart without following a symlink, and `from` is read below
1021−/// `path` only once there is something to look up.
1022−fn untracked_below(disk: &std::path::Path, path: &RepoPath, from: &MergedTree) -> Option<RepoPathBuf> {
1023− let mut tracked: Option<std::collections::HashSet<RepoPathBuf>> = None;
1024− let mut dirs = vec![(disk.to_owned(), path.to_owned())];
1025− while let Some((dir, at)) = dirs.pop() {
1026− let Ok(entries) = std::fs::read_dir(&dir) else {
1027− return Some(at);
1028− };
1029− for entry in entries {
1030− let Ok(entry) = entry else {
1031− return Some(at);
1032− };
1033− let name = entry.file_name();
1034− // a name no path of a tree holds
1035− let Some(below) = name.to_str().and_then(|n| RepoPathComponent::new(n).ok()).map(|c| at.join(c)) else {
1036− return Some(at);
1037− };
1038− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1039− dirs.push((entry.path(), below));
1040− continue;
1041− }
1042− let tracked = tracked.get_or_insert_with(|| {
1043− let under = jj_lib::matchers::PrefixMatcher::new([path]);
1044− from.entries_matching(&under).filter(|(_, value)| value.is_ok()).map(|(p, _)| p).collect()
1045− });
1046− if !tracked.contains(&below) {
1047− return Some(below);
1048− }
1049− }
1050− }
1051− None
1052−}
1053−
1054−/// Clear each of `paths`, where a checkout from the tree the working
1055−/// directory holds to `to` adds a file (`look_where_added`): a file there
1056−/// is moved aside while it holds what the checkout writes
1057−/// (`holds_written`), and in a directory there every directory holding no
1058−/// file is removed, deepest first, and it too once empty. A path below
1059−/// anything that is no directory on disk, a symlink included, is left
1060−/// alone, and no symlink in one is followed; a directory is removed only
1061−/// while empty, so a file put there meanwhile stays, as does one changed
1062−/// meanwhile, and the checkout skips the path (§7.5 step 7). So does one
1063−/// that cannot be moved, which the result names.
1064−async fn clear_the_way(
1065− root: &std::path::Path,
1066− markers: ConflictMarkerStyle,
1067− to: &MergedTree,
1068− paths: &[RepoPathBuf],
1069−) -> Aside {
1070− fn clear(dir: &std::path::Path) {
1071− if let Ok(entries) = std::fs::read_dir(dir) {
1072− for entry in entries.flatten() {
1073− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1074− clear(&entry.path());
1075− }
1076− }
1077− }
1078− let _ = std::fs::remove_dir(dir);
1079− }
1080− let mut aside = Aside { root: root.to_owned(), dir: None, stuck: None, returned: false };
1081− let mut dirs = HashMap::new();
1082− for path in paths {
1083− if first_not_a_dir(root, path, &mut dirs).is_some() {
1084− continue;
1085− }
1086− let Ok(disk) = path.to_fs_path(root) else {
1087− continue;
1088− };
1089− let Ok(meta) = disk.symlink_metadata() else {
1090− continue;
1091− };
1092− if meta.is_dir() {
1093− clear(&disk);
1094− continue;
1095− }
1096− let Ok(value) = to.path_value(path).await else {
1097− continue;
1098− };
1099− if holds_written(to.store(), path, value, to.labels(), markers, &disk).await.unwrap_or(false) {
1100− if let Err(e) = aside.take(path, &disk) {
1101− aside.stuck.get_or_insert((path.clone(), e));
1102− }
1103− }
1104− }
1105− aside
1106−}
1107−
1108−/// The start of the name of each directory in `.jj` that `Aside` moves
1109−/// files into
1110−const ASIDE: &str = "aside";
1111−
1112−/// The files `clear_the_way` moved out of a checkout's way, each holding
1113−/// what the checkout writes where it stood, in a directory in `.jj` that
1114−/// holds each at its path in the working directory `root`, which no scan
1115−/// reads; the first file it could not move, with why; and whether the
1116−/// checkout returned rather than failing, and so wrote in full each file
1117−/// it wrote. Dropped, once the checkout has written what it could, it puts
1118−/// each back (`put_back`) where nothing stands now, as where a checkout
1119−/// that failed or skipped paths did not get to write, and, unless the
1120−/// checkout returned, where a start of it does, as where it was cut short
1121−/// writing it, and removes the directory with the rest: each holds just
1122−/// what the focus has at its path, which the repository keeps, and what
1123−/// stands there, what the checkout wrote or something put there since,
1124−/// stays (§7.5 step 7). One a run killed meanwhile left, the next puts
1125−/// back (`put_back_left`).
1126−struct Aside {
1127− root: std::path::PathBuf,
1128− dir: Option<tempfile::TempDir>,
1129− stuck: Option<(RepoPathBuf, std::io::Error)>,
1130− returned: bool,
1131−}
1132−
1133−impl Aside {
1134− /// Move `disk`, the file at `path`, into the directory, made on first
1135− /// use; renamed, it is neither copied nor followed if a symlink
1136− fn take(&mut self, path: &RepoPath, disk: &std::path::Path) -> std::io::Result<()> {
1137− let dir = match &mut self.dir {
1138− Some(dir) => dir,
1139− none => none.insert(tempfile::Builder::new().prefix(ASIDE).tempdir_in(self.root.join(".jj"))?),
1140− };
1141− let to = path.to_fs_path(dir.path()).map_err(std::io::Error::other)?;
1142− if let Some(parent) = to.parent() {
1143− std::fs::create_dir_all(parent)?;
1144− }
1145− std::fs::rename(disk, to)
1146− }
1147−}
1148−
1149−impl Drop for Aside {
1150− fn drop(&mut self) {
1151− if let Some(dir) = self.dir.take() {
1152− put_back(&self.root, dir.path(), !self.returned);
1153− }
1154− }
1155−}
1156−
1157−/// Put each file in `aside`, where `Aside` moved it, back at its path in
1158−/// the working directory `root` where nothing stands, making the
1159−/// directories above it that are missing, and, where the checkout may have
1160−/// been `cut` short, where a start of it stands, less than all of it, as a
1161−/// checkout cut short while writing it leaves (`cut_short`), which it
1162−/// replaces; one that cannot be put back, as something else stands there
1163−/// or in place of a directory above it, stays in `aside`. No symlink is
1164−/// followed.
1165−fn put_back(root: &std::path::Path, aside: &std::path::Path, cut: bool) {
1166− let missing = |p: &std::path::Path| {
1167− matches!(p.symlink_metadata(), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
1168− };
1169− // whether each directory above `path` in `root` is one, or is made
1170− let dirs_above = |path: &std::path::Path| {
1171− let mut at = root.to_owned();
1172− for name in path.parent().into_iter().flat_map(|p| p.components()) {
1173− at.push(name);
1174− let made = match at.symlink_metadata() {
1175− Ok(meta) => meta.is_dir(),
1176− Err(e) => e.kind() == std::io::ErrorKind::NotFound && std::fs::create_dir(&at).is_ok(),
1177− };
1178− if !made {
1179− return false;
1180− }
1181− }
1182− true
1183− };
1184− let mut dirs = vec![std::path::PathBuf::new()];
1185− while let Some(dir) = dirs.pop() {
1186− let Ok(entries) = std::fs::read_dir(aside.join(&dir)) else {
1187− continue;
1188− };
1189− for entry in entries.flatten() {
1190− let path = dir.join(entry.file_name());
1191− if entry.file_type().is_ok_and(|t| t.is_dir()) {
1192− dirs.push(path);
1193− continue;
1194− }
1195− let disk = root.join(&path);
1196− // the directories above are looked at first, so that nothing
1197− // is looked at through a symlink
1198− if dirs_above(&path) && (missing(&disk) || (cut && cut_short(&disk, &entry.path()))) {
1199− let _ = std::fs::rename(entry.path(), disk);
1200− }
1201− }
1202− }
1203−}
1204−
1205−/// Whether `disk` is a file holding a start of `whole`, a file moved
1206−/// aside, and less than all of it, or, where `whole` is a symlink, an
1207−/// empty file: what jj's checkout leaves where it was cut short (a full
1208−/// disk, a run killed) while it wrote `whole` there, as it makes a file
1209−/// empty before it writes it, and tries a symlink's path with an empty
1210−/// file first. Reads no more of either than `disk` holds. No symlink is
1211−/// followed.
1212−fn cut_short(disk: &std::path::Path, whole: &std::path::Path) -> bool {
1213− use std::io::Read;
1214− use std::os::unix::fs::OpenOptionsExt;
1215− let (Ok(stands), Ok(copy)) = (disk.symlink_metadata(), whole.symlink_metadata()) else {
1216− return false;
1217− };
1218− if !stands.is_file() {
1219− return false;
1220− }
1221− if copy.is_symlink() {
1222− return stands.len() == 0;
1223− }
1224− if !copy.is_file() || stands.len() >= copy.len() {
1225− return false;
1226− }
1227− let stands_file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW).open(disk);
1228− let (Ok(mut stands_file), Ok(mut copy_file)) = (stands_file, std::fs::File::open(whole)) else {
1229− return false;
1230− };
1231− let (mut a, mut b) = (vec![0; 1 << 16], vec![0; 1 << 16]);
1232− let mut rest = stands.len();
1233− while rest > 0 {
1234− let n = rest.min(a.len() as u64) as usize;
1235− let same = stands_file.read_exact(&mut a[..n]).is_ok()
1236− && copy_file.read_exact(&mut b[..n]).is_ok()
1237− && a[..n] == b[..n];
1238− if !same {
1239− return false;
1240− }
1241− rest -= n as u64;
1242− }
1243− true
1244−}
1245−
1246−/// Put back what a run killed while its checkout ran left moved aside in
1247−/// `.jj` (`Aside`), over a write the kill may have cut short too, and
1248−/// remove the directory it left there. A run does this once it holds the
1249−/// repository's lock, before it looks at the working directory, so no
1250−/// other run is using such a directory.
1251−fn put_back_left(root: &std::path::Path) {
1252− let Ok(entries) = std::fs::read_dir(root.join(".jj")) else {
1253− return;
1254− };
1255− for entry in entries.flatten() {
1256− let named = entry.file_name().to_str().is_some_and(|n| n.starts_with(ASIDE));
1257− if named && entry.file_type().is_ok_and(|t| t.is_dir()) {
1258− put_back(root, &entry.path(), true);
1259− let _ = std::fs::remove_dir_all(entry.path());
1260− }
1261− }
1262−}
1263−
1264−/// The first path a checkout from `from` to `to` that skipped some left
1265−/// holding something other than what it writes there: with no directory
1266−/// above it, or a directory at it, or, where it adds the path, anything but
1267−/// what it writes (`holds_written`); none if no such path is found. Only
1268−/// once a checkout has skipped a path is the directory looked at again,
1269−/// reading the files it added until one differs.
1270−async fn first_skipped(
1271− root: &std::path::Path,
1272− markers: ConflictMarkerStyle,
1273− from: &MergedTree,
1274− to: &MergedTree,
1275−) -> Result<Option<RepoPathBuf>, String> {
1276− let mut dirs = HashMap::new();
1277− let mut diff = from.diff_stream(to, &EverythingMatcher);
1278− while let Some(entry) = diff.next().await {
1279− let Diff { before, after } = entry.values.map_err(|e| e.to_string())?;
1280− if after.is_absent() || matches!(after.as_normal(), Some(TreeValue::GitSubmodule(_))) {
1281− continue;
1282− }
1283− let path = entry.path;
1284− if let Some((_, stands)) = first_not_a_dir(root, &path, &mut dirs) {
1285− if stands.is_some() {
1286− return Ok(Some(path));
1287− }
1288− continue;
1289− }
1290− let Ok(disk) = path.to_fs_path(root) else {
1291− continue;
1292− };
1293− let Ok(meta) = disk.symlink_metadata() else {
1294− continue;
1295− };
1296− if meta.is_dir()
1297− || (before.is_absent() && !holds_written(to.store(), &path, after, to.labels(), markers, &disk).await?)
1298− {
1299− return Ok(Some(path));
1300− }
1301− }
1302− Ok(None)
1303−}
1304−
1305−/// Save the working copy's state as a scan that found `scanned` left it:
1306−/// at `head`, the operation the repository is at, where that is the
1307−/// working-copy commit's tree `wc`; otherwise at the operation it was
1308−/// saved at before, so that it stays stale for jj too, which takes a state
1309−/// saved at the head for a current one and would record the directory
1310−/// into the commit whole (§7.4)
1311−async fn save_scan(
1312− mut locked_ws: jj_lib::workspace::LockedWorkspace<'_>,
1313− scanned: &MergedTree,
1314− wc: &MergedTree,
1315− head: &OperationId,
1316−) -> Result<(), String> {
1317− let op_id = if scanned.tree_ids() == wc.tree_ids() {
1318− head.clone()
1319− } else {
1320− locked_ws.locked_wc().old_operation_id().clone()
1321− };
1322− locked_ws.finish(op_id).await.map_err(|e| e.to_string())
1323−}
1324−
1325−/// The signal that asked the run to stop while its checkout was not to be
1326−/// cut short (`Deferral`), or 0
1327−static DEFERRED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1328−
1329−extern "C" fn defer(sig: libc::c_int) {
1330− DEFERRED.store(sig, std::sync::atomic::Ordering::SeqCst);
1331−}
1332−
1333−/// While it lives, SIGINT, SIGTERM, SIGHUP and SIGQUIT, whichever thread
1334−/// gets them, only note that they came: it lives from the operation's
1335−/// publishing until the working copy's state is saved, so that a checkout
1336−/// the user interrupts completes, leaving the working copy current, and
1337−/// through a clone, which one that comes before the working-copy commit is
1338−/// made fails (`unless_interrupted`), so that it removes what it made
1339−/// (§1.3). A signal the process ignores stays ignored, as it does for the
1340−/// git a clone runs (`nohup j clone`, a background job). Dropping it gives
1341−/// each signal back what it did before, and `raise_deferred` then acts on
1342−/// one that came.
1343−struct Deferral {
1344− previous: Vec<(libc::c_int, libc::sigaction)>,
1345−}
1346−
1347−impl Deferral {
1348− fn start() -> Deferral {
1349− let mut previous = Vec::new();
1350− for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT] {
1351− // SAFETY: the handler only stores to an atomic, which is
1352− // async-signal-safe; both actions are initialised before use
1353− unsafe {
1354− let mut old: libc::sigaction = std::mem::zeroed();
1355− if libc::sigaction(sig, std::ptr::null(), &mut old) != 0
1356− || old.sa_sigaction == libc::SIG_IGN
1357− {
1358− continue;
1359− }
1360− let mut action: libc::sigaction = std::mem::zeroed();
1361− action.sa_sigaction = defer as extern "C" fn(libc::c_int) as libc::sighandler_t;
1362− action.sa_flags = libc::SA_RESTART;
1363− libc::sigemptyset(&mut action.sa_mask);
1364− if libc::sigaction(sig, &action, std::ptr::null_mut()) == 0 {
1365− previous.push((sig, old));
1366− }
1367− }
1368− }
1369− Deferral { previous }
1370− }
1371−}
1372−
1373−impl Drop for Deferral {
1374− fn drop(&mut self) {
1375− for (sig, old) in &self.previous {
1376− // SAFETY: `old` is the action `sigaction` gave back for `sig`
1377− unsafe { libc::sigaction(*sig, old, std::ptr::null_mut()) };
1378− }
1379− }
1380−}
1381−
1382−/// Act on a signal a checkout or a clone deferred (§1.3) as it would have
1383−/// acted then: with its default action, stopping the process (`Deferral`
1384−/// defers no signal the process ignores). Called once the run has reported
1385−/// what it did.
1386−pub fn raise_deferred() {
1387− let sig = DEFERRED.swap(0, std::sync::atomic::Ordering::SeqCst);
1388− if sig != 0 {
1389− // a process a signal stops does not flush what it printed
1390− use std::io::Write;
1391− let _ = std::io::stdout().flush();
1392− // SAFETY: raising a signal whose action `Deferral` gave back
1393− unsafe { libc::raise(sig) };
1394− }
1395−}
1396−
1397−/// `done`, the outcome of a step of a clone, unless a signal to stop came
1398−/// while `cmd_clone`'s `Deferral` held it off: the clone then fails, before
1399−/// it records anything, so that it removes what it made (§1.3, §7.8). A
1400−/// Ctrl-C at the terminal stops the git the step runs too, which fails the
1401−/// step itself: `j` may take the signal only after that, and then reports
1402−/// the step's failure.
1403−fn unless_interrupted<T>(done: Result<T, OpenError>) -> Result<T, OpenError> {
1404− if DEFERRED.load(std::sync::atomic::Ordering::SeqCst) != 0 {
1405− Err((1, "clone interrupted".to_string()))
1406− } else {
1407− done
1408− }
1409−}
1410−
1411−/// `commit` with `tree` in place of its own, for the working copy to read:
1412−/// a checkout and a reset read only a commit's tree. It is never written.
1413−fn with_tree(commit: &Commit, tree: MergedTree) -> Commit {
1414− let (tree_ids, labels) = tree.into_tree_ids_and_labels();
1415− Commit::new(
1416− commit.store().clone(),
1417− commit.id().clone(),
1418− Arc::new(jj_lib::backend::Commit {
1419− root_tree: tree_ids,
1420− conflict_labels: labels.into_merge(),
1421− ..commit.store_commit().as_ref().clone()
1422− }),
1423− )
1424275}
1425276
1426277#[derive(Clone)]
1427278pub struct JjBackend {
1428279 inner: Arc<JjInner>,
1429− replay_cache: Rc<ReplayCache>,
1430280}
1431281
1432282impl JjBackend {
@@ -1434,7 +284,7 @@ impl JjBackend {
1434284 let root = find_repo_root().ok_or_else(|| {
1435285 (
1436286 2,
1437− "no jj repository found in this directory or any parent; run `j init` here, `j clone URL`, or `j help`".to_string(),
287+ "no jj repository found in this directory or any parent".to_string(),
1438288 )
1439289 })?;
1440290 let settings = user_settings(cfg);
@@ -1455,22 +305,10 @@ impl JjBackend {
1455305 ),
1456306 ));
1457307 }
1458− // Snapshot and checkout both apply patterns saved by `jj sparse`,
1459− // independently of our SnapshotOptions. Accepting a partial working
1460− // copy would silently omit disk edits and skip files during checkout.
1461− let sparse = workspace.working_copy().sparse_patterns()
1462− .map_err(|e| (2, format!("cannot read the working copy's sparse patterns: {}", e)))?;
1463− if !sparse.iter().any(|path| path.is_root()) {
1464− return Err((
1465− 2,
1466− "sparse working copies are not supported; run `jj sparse set --clear --add .` to include all files before running j".to_string(),
1467− ));
1468− }
1469308 let workspace_name = workspace.workspace_name().to_owned();
1470309 let repo = block_on(workspace.repo_loader().load_at_head())
1471310 .map_err(|e| (2, format!("cannot load the repository at head: {}", e)))?;
1472311 Ok(JjBackend {
1473− replay_cache: Rc::new(ReplayCache::default()),
1474312 inner: Arc::new(JjInner {
1475313 workspace_root: root,
1476314 workspace: Mutex::new(workspace),
@@ -1479,8 +317,6 @@ impl JjBackend {
1479317 visible: Mutex::new(None),
1480318 lock_guard: Mutex::new(None),
1481319 pending: Mutex::new(None),
1482− folds_case: OnceLock::new(),
1483− long_names: Mutex::new(HashMap::new()),
1484320 }),
1485321 })
1486322 }
@@ -1500,22 +336,21 @@ impl JjBackend {
1500336 {
1501337 Ok(f) => f,
1502338 Err(e) => {
1503− crate::terminal::report_error(format_args!("j: cannot create {}: {}", lock_path.display(), e));
339+ eprintln!("j: cannot create {}: {}", lock_path.display(), e);
1504340 std::process::exit(2);
1505341 }
1506342 };
1507343 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
1508344 if rc != 0 {
1509− crate::terminal::report_error(format_args!("j: another j is running in this repository"));
345+ eprintln!("j: another j is running in this repository");
1510346 std::process::exit(2);
1511347 }
1512348 *self.inner.lock_guard.lock().unwrap() = Some(FileLock(file));
1513− put_back_left(&self.inner.workspace_root);
1514349 }
1515350
1516351 pub fn build_interp(
1517352 &self,
1518− cfg: &mut Config,
353+ cfg: &Config,
1519354 _text: &str,
1520355 snapshot: bool,
1521356 ) -> Result<(Interp, Value, Value), OpenError> {
@@ -1524,8 +359,8 @@ impl JjBackend {
1524359 let head_repo = block_on(self.inner.workspace.lock().unwrap().repo_loader().load_at_head())
1525360 .map_err(|e| (2, format!("cannot reload the repository at head: {}", e)))?;
1526361 *self.inner.repo.lock().unwrap() = head_repo;
1527− let loaded_visible = Arc::new(block_on(read_visible(&self.current_repo()))?);
1528− let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id, &self.replay_cache)?;
362+ let loaded_visible = block_on(read_visible(&self.current_repo()))?;
363+ let loaded = build_repo_value(&loaded_visible, &loaded_visible.wc_change_id)?;
1529364
1530365 let current = if snapshot {
1531366 // §7.4: snapshot the working directory into the wc commit; the
@@ -1536,19 +371,19 @@ impl JjBackend {
1536371 // no working-copy change: the repo is the one already loaded,
1537372 // so reuse its value instead of rebuilding it
1538373 None => {
1539− *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
374+ *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1540375 loaded.clone()
1541376 }
1542377 Some(pending) => {
1543378 *self.inner.pending.lock().unwrap() = Some(pending);
1544379 let vis = block_on(read_visible(&post))?;
1545− let cur = build_repo_value(&vis, &vis.wc_change_id, &self.replay_cache)?;
380+ let cur = build_repo_value(&vis, &vis.wc_change_id)?;
1546381 *self.inner.visible.lock().unwrap() = Some(Arc::new(vis));
1547382 cur
1548383 }
1549384 }
1550385 } else {
1551− *self.inner.visible.lock().unwrap() = Some(loaded_visible.clone());
386+ *self.inner.visible.lock().unwrap() = Some(Arc::new(loaded_visible));
1552387 loaded.clone()
1553388 };
1554389
@@ -1557,59 +392,35 @@ impl JjBackend {
1557392 cfg.shapes.clone(),
1558393 crate::value::Env::empty(),
1559394 );
1560− // §1.2.5: resolve the config's `@prefix` literals now that the visible
1561− // ids are known, and before anything evaluates a definition
1562− if let Some((def, prefix, _)) =
1563− crate::config::resolve_config_ids(cfg, &interp).into_iter().next()
1564− {
1565− return Err((
1566− 3,
1567− format!(
1568− "config.j: `@{}` in `{}` does not resolve to a unique commit",
1569− prefix, def
1570− ),
1571− ));
1572− }
1573395 // §7.2: if the focused commit is in the immutable set, the snapshot
1574396 // cannot go into it; the focus is a new empty child of it holding the
1575397 // snapshot, and persisting records that child
1576− match self.focus_if_immutable(&mut interp, cfg, &loaded, ¤t)? {
1577− Some((loaded, current)) => {
1578− // the snapshot's rewrite of the wc commit (and its rebase of
1579− // the descendants) is not the repository's: lookups answer
1580− // from the stored commits, and persisting leaves the wc
1581− // commit as stored rather than folding the snapshot into it
1582− *self.inner.visible.lock().unwrap() = Some(loaded_visible);
1583− if let Some(p) = self.inner.pending.lock().unwrap().as_mut() {
1584− p.fold_into_wc = false;
1585− }
1586− Ok((interp, loaded, current))
398+ match self.focus_if_immutable(&mut interp, cfg, ¤t)? {
399+ Some(adjusted) => {
400+ // the persisted child is part of the loaded value too, so a
401+ // run that changes nothing is a no-op
402+ Ok((interp, adjusted.clone(), adjusted))
1587403 }
1588404 None => Ok((interp, loaded, current)),
1589405 }
1590406 }
1591407
1592− /// If the focus of `current` is immutable, return the loaded and current
1593− /// repos refocused on a new empty child of it (§7.2). Both are `loaded`
1594− /// with that one child added: the snapshot goes into neither the focus
1595− /// nor, through a rebase, its descendants. The child holds the focus's
1596− /// stored files in the loaded repo and the snapshot in the current one,
1597− /// so a dirty `j id` differs from the loaded value and records the child
1598− /// (§1.2 step 8).
408+ /// If the focus of `current` is immutable, return a repo refocused on a
409+ /// new empty child holding the focus's files (§7.2).
1599410 fn focus_if_immutable(
1600411 &self,
1601412 interp: &mut Interp,
1602413 cfg: &Config,
1603− loaded: &Value,
1604414 current: &Value,
1605− ) -> Result<Option<(Value, Value)>, OpenError> {
1606− // Evaluate the config to compute the immutable set. This uses the
1607− // interpreter the run will go on to use, rather than a throwaway one,
1608− // so that the `immutable` (and the `trunk` inside it) computed here is
1609− // reused when rendering or persisting asks for the same Repo value —
1610− // each is a full interpreted walk of the history.
1611− crate::config::eval_config(interp, cfg).map_err(|c| (3, c.msg))?;
1612− let immutable = crate::repo::compute_immutable(interp, current)
415+ ) -> Result<Option<Value>, OpenError> {
416+ // evaluate the config to compute the immutable set
417+ let mut probe = Interp::new(
418+ Rc::new(self.clone()),
419+ cfg.shapes.clone(),
420+ crate::value::Env::empty(),
421+ );
422+ crate::config::eval_config(&mut probe, cfg).map_err(|c| (3, c.msg))?;
423+ let immutable = crate::repo::compute_immutable(&mut probe, current)
1613424 .map_err(|c| (1, c.msg))?;
1614425 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
1615426 Ok(Value::Id(i)) => i.to_string(),
@@ -1618,101 +429,90 @@ impl JjBackend {
1618429 if !immutable.contains(&focus_id) {
1619430 return Ok(None);
1620431 }
1621− // the focus's location in `loaded`, pre-snapshot; the child is its
1622− // last child, so every existing child sits to the left of it
1623− let field = |v: &Value, name: &str| v.field(name).map_err(|c| (1, c.msg));
1624− let root = field(loaded, "root")?;
1625− let frame = Value::record(&[
1626− ("left", field(loaded, "children")?),
1627− ("parent", root.clone()),
1628− ("right", Value::list(vec![])),
432+ // build: new empty child of the focus with the focus's files, minted id
433+ let _ = interp;
434+ let root = current.field("root").map_err(|c| (1, c.msg))?;
435+ let files = root.field("files").map_err(|c| (1, c.msg))?;
436+ let child = crate::value::Value::record(&[
437+ ("files", files),
438+ ("message", crate::value::Value::text("")),
439+ ("labels", crate::value::Value::list(vec![])),
440+ (
441+ "id",
442+ crate::value::Value::Id(Rc::new(probe.mint_id())),
443+ ),
1629444 ]);
1630− let mut frames = vec![frame];
1631− frames.extend_from_slice(field(loaded, "context")?.as_list().map_err(|c| (1, c.msg))?);
1632− let context = Value::list(frames);
1633− let id = Value::Id(Rc::new(interp.mint_id()));
1634− let at_child = |files: Value| {
1635− Value::record(&[
1636− ("children", Value::list(vec![])),
1637− ("context", context.clone()),
1638− (
1639− "root",
1640− Value::record(&[
1641− ("files", files),
1642− ("id", id.clone()),
1643− ("labels", Value::list(vec![])),
1644− ("message", Value::text("")),
1645− ]),
1646− ),
1647− ])
1648− };
1649− let current_at_child = at_child(field(&field(current, "root")?, "files")?);
1650− // with nothing snapshotted `current` is `loaded` itself: share one
1651− // value, so a run that changes nothing is a no-op without comparing
1652− // the whole repository
1653− let loaded_at_child = match (loaded, current) {
1654− (Value::Record(a), Value::Record(b)) if Rc::ptr_eq(a, b) => current_at_child.clone(),
1655− _ => at_child(field(&root, "files")?),
445+ let child_subtree = crate::value::Value::record(&[
446+ ("root", child.clone()),
447+ ("children", crate::value::Value::list(vec![])),
448+ ]);
449+ let mut new_children: Vec<Value> = current
450+ .field("children")
451+ .map_err(|c| (1, c.msg))?
452+ .as_list()
453+ .map_err(|c| (1, c.msg))?
454+ .to_vec();
455+ new_children.push(child_subtree);
456+ let parent = crate::value::Value::record(&[
457+ ("children", crate::value::Value::list(new_children)),
458+ ("context", current.field("context").map_err(|c| (1, c.msg))?),
459+ ("root", root),
460+ ]);
461+ // refocus onto the child (it is the last child)
462+ let child_id = match child.field("id").map_err(|c| (1, c.msg))? {
463+ Value::Id(i) => i.to_string(),
464+ _ => unreachable!(),
1656465 };
1657− Ok(Some((loaded_at_child, current_at_child)))
466+ crate::repo::by_id(&parent, &child_id)
467+ .map_err(|c| (1, c.msg))?
468+ .ok_or_else(|| (1, "internal: cannot refocus".to_string()))
469+ .map(Some)
1658470 }
1659471
1660− /// Snapshot the working directory into the working-copy commit (§7.4);
1661− /// return the repo at the snapshot's operation, written but not
1662− /// published, or the repo as loaded when there is nothing to record.
472+ /// Snapshot the working copy into the wc commit; returns the repo at the
473+ /// snapshot operation (unpublished but written), or the pre-snapshot repo
474+ /// if nothing changed.
1663475 async fn snapshot_repo(
1664476 &self,
1665477 ) -> Result<(Arc<ReadonlyRepo>, Option<PendingSnapshot>), OpenError> {
1666478 let mut ws_guard = self.inner.workspace.lock().unwrap();
1667− let root = ws_guard.workspace_root().to_owned();
1668− let saved = SavedState::of(&ws_guard);
479+ let loader = ws_guard.repo_loader().clone();
1669480 let mut locked_ws = ws_guard.start_working_copy_mutation()
1670481 .await
1671482 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
1672− let old_tree = locked_ws.locked_wc().old_tree().clone();
1673− let scanned = scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options())
483+ let options = SnapshotOptions {
484+ base_ignores: GitIgnoreFile::empty(),
485+ progress: None,
486+ start_tracking_matcher: &EverythingMatcher,
487+ force_tracking_matcher: &NothingMatcher,
488+ max_new_file_size: u64::MAX,
489+ };
490+ let (new_tree, _stats) = locked_ws
491+ .locked_wc()
492+ .snapshot(&options)
1674493 .await
1675494 .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1676− // the repo build_interp loaded at head from the workspace's own
1677− // loader, whose store the scanned tree lives in
1678− let head = self.current_repo();
1679− let wc_commit = self.wc_commit(&head).await?;
1680− let current = locked_ws.locked_wc().old_operation_id() == head.operation().id();
1681− let tree = match marker_style(head.settings()) {
1682− Ok(markers) => snapshot_tree(&root, markers, &wc_commit.tree(), &old_tree, current, &scanned).await,
1683− Err(e) => Err(e),
1684− }
1685− .map_err(|e| (2, format!("cannot snapshot the working copy: {}", e)))?;
1686− if tree.tree_ids() == wc_commit.tree().tree_ids() {
1687− // nothing to record: the directory holds the commit's files, or,
1688− // where the state is stale, the state's, or changes to them the
1689− // commit already has, such as what a checkout cut short wrote.
1690− // What was scanned is saved, at the operation the state is
1691− // stale since where it still is.
1692− save_scan(locked_ws, &scanned, &wc_commit.tree(), head.operation().id())
495+ let changed = new_tree.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids();
496+ if !changed {
497+ locked_ws
498+ .finish(self.current_repo().operation().id().clone())
1693499 .await
1694500 .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
1695− return Ok((head, None));
501+ return Ok((self.current_repo(), None));
1696502 }
1697− // a directory come to hold the parent's files, as where it reverts
1698− // the commit's change, is recorded with the parent's tree (§7.4), as
1699− // persistence writes a commit with its parent's files (§7.5 step 4):
1700− // the snapshot builds on the commit's tree, and so could make
1701− // another tree for the parent's files, one jj takes for a change.
1702− // Not before the check above, or a commit already holding such a
1703− // tree would be rewritten by a directory that has not changed. A
1704− // merge is immutable, so never folded into (§7.2), and its parents'
1705− // trees are not merged for it
1706− let tree = match wc_commit.parent_ids() {
1707− [_] => parents_tree_if_same(head.as_ref(), wc_commit.parent_ids(), tree).await,
1708− _ => tree,
1709− };
1710− let mut tx = head.start_transaction();
503+ // the snapshotted tree lives in the workspace's store; drive the
504+ // transaction from the workspace's repo loader so stores match
505+ let ws_repo = loader
506+ .load_at_head()
507+ .await
508+ .map_err(|e| (2, format!("cannot reload the repository: {}", e)))?;
509+ let mut tx = ws_repo.start_transaction();
1711510 tx.set_is_snapshot(true);
511+ let wc_commit = self.wc_commit(&ws_repo).await?;
1712512 let new_wc = tx
1713513 .repo_mut()
1714514 .rewrite_commit(&wc_commit)
1715− .set_tree(tree)
515+ .set_tree(new_tree)
1716516 .write()
1717517 .await
1718518 .map_err(|e| (2, format!("cannot write the snapshot commit: {}", e)))?;
@@ -1729,26 +529,25 @@ impl JjBackend {
1729529 .write("snapshot working copy")
1730530 .await
1731531 .map_err(|e| (2, format!("cannot write the snapshot operation: {}", e)))?;
532+ let op_id = unpublished.operation().id().clone();
1732533 let new_repo = unpublished.leave_unpublished();
1733− // Release the lock *without* finishing: `finish` saves the scanned
1734− // tree state to disk, which would record the working directory as
1735− // already snapshotted while the published head still has the old wc
1736− // tree. The next run's incremental snapshot would then find nothing
1737− // to do and build the Repo from the stale tree — and a persisting run
1738− // would check that stale tree back out, destroying the edits. The
1739− // state is advanced only by `persist`'s checkout, which first scans
1740− // the directory again and finishes against the operation that
1741− // actually recorded the tree (§7.4/§7.7).
1742− drop(locked_ws);
534+ // the working-copy state tracks the snapshot tree, against the
535+ // current (published) head; persist will finish against its own op
536+ locked_ws
537+ .finish(self.current_repo().operation().id().clone())
538+ .await
539+ .map_err(|e| (2, format!("cannot finish the snapshot: {}", e)))?;
540+ let _ = op_id;
1743541 let pending = PendingSnapshot {
1744− pre_repo: head,
542+ pre_repo: self.current_repo(),
543+ new_wc_id: new_wc.id().clone(),
1745544 tree: new_wc.tree().clone(),
1746− fold_into_wc: true,
1747− scanned,
1748545 };
1749546 Ok((new_repo, Some(pending)))
1750547 }
1751548
549+
550+
1752551 async fn wc_commit(&self, repo: &Arc<ReadonlyRepo>) -> Result<Commit, OpenError> {
1753552 let id = repo
1754553 .view()
@@ -1766,13 +565,12 @@ impl JjBackend {
1766565 interp: &mut Interp,
1767566 cfg: &Config,
1768567 loaded: &Value,
1769− current: &Value,
568+ _current: &Value,
1770569 new: &Value,
1771570 text: &str,
1772− ) -> Result<bool, Crash> {
1773− // §1.2 step 8: unchanged, up to the order of snapshot entries (§7.3)
1774− if crate::repo::same_repo(loaded, new)? {
1775− return Ok(false);
571+ ) -> Result<(), Crash> {
572+ if crate::value::value_eq(loaded, new)? {
573+ return Ok(());
1776574 }
1777575 *interp.old_repo.borrow_mut() = Some(loaded.clone());
1778576 crate::repo::validate_repo(interp, new)?;
@@ -1792,7 +590,7 @@ impl JjBackend {
1792590 // walk below compares against its output, so the snapshot change and
1793591 // the expression's edits become one rewrite
1794592 let mut snapshot_wc: Option<Commit> = None;
1795− if let Some(p) = pending.as_ref().filter(|p| p.fold_into_wc) {
593+ if let Some(p) = &pending {
1796594 let wc_commit = block_on(self.wc_commit(&base)).map_err(|e| Crash::new(e.1))?;
1797595 let c = block_on(
1798596 tx.repo_mut()
@@ -1816,9 +614,6 @@ impl JjBackend {
1816614 old_stored = Arc::new(vis);
1817615 }
1818616 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])?;
1822617 // shared across the whole persist walk so unchanged blobs are inflated once
1823618 let cache = BlobCache::default();
1824619 let entries = EntryCache::default();
@@ -1842,35 +637,33 @@ impl JjBackend {
1842637 }
1843638 let stored_files = block_on(tree_to_files(&store, &root_jj.tree(), &cache, &entries))
1844639 .map_err(|e| Crash::new(e.msg))?;
1845− if !crate::repo::snapshot_eq(&Value::list(stored_files), &root_v.field("files")?)? {
640+ if !crate::value::value_eq(
641+ &Value::list(stored_files),
642+ &Value::list(root_v.field("files")?.as_list()?.to_vec()),
643+ )? {
1846644 return Err(Crash::new("persistence: the root commit cannot be changed"));
1847645 }
1848646 }
1849647 written.insert(ROOT_ID.to_string(), root_jj.id().clone());
1850648
1851649 // walk `new` top-down (§7.5 step 4), parents before children
1852− // a deque, not a Vec: `remove(0)` shifts everything after it, which
1853− // is quadratic in the number of commits for a wide history
1854− let mut queue: std::collections::VecDeque<(Value, String)> =
1855− std::collections::VecDeque::new(); // (subtree value, parent change id)
650+ let mut queue: Vec<(Value, String)> = Vec::new(); // (subtree value, parent change id)
1856651 for kid in top.field("children")?.as_list()?.iter() {
1857− queue.push_back((kid.clone(), ROOT_ID.to_string()));
652+ queue.push((kid.clone(), ROOT_ID.to_string()));
1858653 }
1859− while let Some((subtree, parent_change)) = queue.pop_front() {
654+ while !queue.is_empty() {
655+ let (subtree, parent_change) = queue.remove(0);
1860656 let commit_v = subtree.field("root")?;
1861657 let id = commit_id_of(&commit_v)?;
1862658 let parent_jj = written
1863659 .get(&parent_change)
1864660 .cloned()
1865661 .ok_or_else(|| Crash::new("persistence: internal: parent not yet written"))?;
1866− // not read unless compared or written: most commits keep the
1867− // stored tree they were loaded with
1868− let files_v = crate::repo::files_of(&commit_v)?;
662+ let files_v = commit_v.field("files")?;
1869663 let message = commit_v.field("message")?.as_text()?.to_string();
1870664 let new_id = match old_stored.commits.get(&id) {
1871665 None => {
1872− let tree =
1873− loaded_trees.tree_to_write(tx.repo(), &files_v, std::slice::from_ref(&parent_jj))?;
666+ let tree = block_on(build_tree(&store, &files_v))?;
1874667 let change_id = jj_lib::backend::ChangeId::try_from_reverse_hex(&id).ok_or_else(|| {
1875668 Crash::new(format!("persistence: `{}` is not a valid change id", id))
1876669 })?;
@@ -1889,31 +682,17 @@ impl JjBackend {
1889682 Some(stored) => {
1890683 let stored_commit = &stored.commit;
1891684 let parent_changed = hex_of(stored_commit.parent_ids().first()) != parent_jj.hex();
1892− let stored_tree = stored_commit.tree();
1893− // loaded from the stored tree, the files are unchanged:
1894− // reading every commit's to compare them made each
1895− // persist read the files of the whole history
1896− let files_changed = crate::repo::stored_tree(&files_v)
1897− != Some(tree_name(&stored_tree).as_str())
1898− && {
1899− let stored_files =
1900− block_on(tree_to_files(&store, &stored_tree, &cache, &entries))?;
1901− // a tree has no order: entries listed differently
1902− // are not a change to rewrite (§7.3)
1903− !crate::repo::snapshot_eq(&Value::list(stored_files), &files_v)?
1904− };
685+ let files_changed = {
686+ let stored_files =
687+ block_on(tree_to_files(&store, &stored_commit.tree(), &cache, &entries))?;
688+ !crate::value::value_eq(
689+ &Value::list(stored_files),
690+ &Value::list(files_v.as_list()?.to_vec()),
691+ )?
692+ };
1905693 let msg_changed = stored_commit.description() != message;
1906694 if parent_changed || files_changed || msg_changed {
1907− // files the stored tree lists are written as it is,
1908− // but where they are the parent's, as the files of a
1909− // commit moved onto another tree for them come to be,
1910− // with the parent's tree (§7.5 step 4)
1911− let parents = std::slice::from_ref(&parent_jj);
1912− let tree = if files_changed {
1913− loaded_trees.tree_to_write(tx.repo(), &files_v, parents)?
1914− } else {
1915− block_on(parents_tree_if_same(tx.repo(), parents, stored_tree))
1916− };
695+ let tree = block_on(build_tree(&store, &files_v))?;
1917696 let c = block_on(
1918697 tx.repo_mut()
1919698 .rewrite_commit(stored_commit)
@@ -1932,7 +711,7 @@ impl JjBackend {
1932711 };
1933712 written.insert(id.clone(), new_id);
1934713 for kid in subtree.field("children")?.as_list()?.iter() {
1935− queue.push_back((kid.clone(), id.clone()));
714+ queue.push((kid.clone(), id.clone()));
1936715 }
1937716 }
1938717
@@ -1976,230 +755,37 @@ impl JjBackend {
1976755 let desc = truncate_chars(text, 200);
1977756 let unpublished = block_on(tx.write(desc))
1978757 .map_err(|e| Crash::new(format!("cannot write the operation: {}", e)))?;
758+ let op_id = unpublished.operation().id().clone();
759+ let _new_repo = block_on(unpublished.publish())
760+ .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
1979761
1980− // §7.4/§7.5 step 7: publish the operation, then check the focus out
762+ // §7.4/§7.5 step 7: check out the focus to the working directory
1981763 let focus_commit = block_on(store.get_commit_async(&focus_jj))
1982764 .map_err(|e| Crash::new(format!("cannot read the focus commit: {}", e)))?;
1983− // the checkout starts from what the working directory holds: the
1984− // tree the run's snapshot scanned, or, where it had nothing to
1985− // record, the tree the working-copy state records, as
1986− // `snapshot_repo` left it. That is the focus as loaded unless the
1987− // state is stale (§7.4), when the checkout writes the focus over it;
1988− // taking the directory to hold the focus instead, it would find it
1989− // did not, describe the focus without writing it (a reset), and the
1990− // next run would record the stale files into the focus.
1991− let on_disk = match pending {
1992− Some(p) => p.scanned,
1993− None => {
1994− let state = self.inner.workspace.lock().unwrap().working_copy().tree().cloned();
1995− // a state that cannot be read fails the checkout, which
1996− // reads it again when it locks
1997− state.or_else(|_| block_on(self.wc_commit(&base)).map(|c| c.tree()).map_err(|e| Crash::new(e.1)))?
1998− }
1999− };
2000− block_on(self.checkout(&focus_commit, Some(&on_disk), Some(unpublished), Some(", and `j undo` goes back")))?;
2001− Ok(true)
765+ block_on(self.checkout(&focus_commit, op_id))?;
766+
767+ Ok(())
2002768 }
2003769
2004− /// Check `commit` out: write its files to the working directory and
2005− /// save the working copy's state as holding them (§7.4). `op`, the
2006− /// operation that makes it the working-copy commit, is published first,
2007− /// as jj does (§7.5 step 7); with none, `commit` is the head's own.
2008− /// `on_disk` is the tree the directory holds, which the checkout starts
2009− /// from: what a persisting run's snapshot scanned, or the tree the
2010− /// working-copy state records where it scanned nothing to record, or
2011− /// what `undo`'s look found; with none, the state as it is. A change
2012− /// made to the directory since `on_disk` was scanned, where the
2013− /// checkout writes, refuses the checkout before `op` is published
2014− /// (`written_over`); one made once it has begun is written over, as by
2015− /// jj. Where the commit adds a file, something `on_disk` does not track
2016− /// standing there, but for empty directories and what the checkout
2017− /// writes, refuses the checkout before `op` is published
2018− /// (`look_where_added`), and one put there since fails it after, as it
2019− /// makes jj skip the path. A checkout that fails, or is cut short, after
2020− /// `op` is published leaves `op` recorded and the state stale, recording
2021− /// `on_disk`: the next run takes what the checkout wrote for the
2022− /// commit's own and carries on (`snapshot_tree`), as the crash says,
2023− /// with `back` naming the way back; with no `back` it says nothing of
2024− /// the operation (a clone that fails removes it).
2025− async fn checkout(
2026− &self,
2027− commit: &Commit,
2028− on_disk: Option<&MergedTree>,
2029− op: Option<UnpublishedOperation>,
2030− back: Option<&str>,
2031− ) -> Result<(), Crash> {
770+ async fn checkout(&self, commit: &Commit, op_id: OperationId) -> Result<(), Crash> {
2032771 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);
2035772 let mut locked_ws = ws_guard.start_working_copy_mutation()
2036773 .await
2037774 .map_err(|e| Crash::new(format!("cannot lock the working copy: {}", e)))?;
2038− // A persisting run's snapshot released its lock without saving what
2039− // it scanned, so the state just loaded may still describe the tree
2040− // recorded before the edits, and jj checks out by diffing that tree
2041− // against the new one: a path whose recorded content equals the
2042− // focus's would keep the user's edit, a file created since would
2043− // stay, and a file replaced by a directory (or back) would leave
2044− // state contradicting the tree. Scan the directory again first, as
2045− // the snapshot did, so the checkout diffs from what is actually on
2046− // disk (§7.4). A scan keeps what the state knows of each file where
2047− // a reset to the snapshot's tree would forget it: a conflict whose
2048− // sides hold marker-like lines is materialized with longer markers,
2049− // and a forgotten length makes the next snapshot read those markers
2050− // back at 7 characters, fail to parse them and record the edited
2051− // conflict as resolved text, markers and all (§7.4).
2052− if let Some(on_disk) = on_disk {
2053− let old_tree = locked_ws.locked_wc().old_tree().clone();
2054− let reset = match scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()).await {
2055− Ok(scanned) if scanned.tree_ids() == on_disk.tree_ids() => false,
2056− // the directory changed while the program ran: where the
2057− // checkout would write the focus over such a change, which
2058− // no snapshot holds and jj's checkout does not look for,
2059− // the run crashes before anything is recorded or written
2060− // (§7.5 step 7)
2061− Ok(scanned) => match written_over(on_disk, &scanned, &commit.tree()).await {
2062− Ok(None) => true,
2063− Ok(Some(path)) => {
2064− return Err(Crash::new(format!(
2065− "cannot check out the focus: `{}` changed in the working directory while the program ran, and the checkout would write over it; nothing was recorded, so run the command again",
2066− path.as_internal_file_string()
2067− )));
2068− }
2069− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2070− },
2071− // or it can no longer be scanned (an entry appeared,
2072− // vanished or cannot be read; a failed scan leaves the state
2073− // as it was)
2074− Err(_) => true,
2075− };
2076− // Otherwise the state describes `on_disk` instead. A reset
2077− // writes no file; the paths it changes are re-read by the next
2078− // run.
2079− if reset {
2080− locked_ws
2081− .locked_wc()
2082− .reset(&with_tree(commit, on_disk.clone()))
2083− .await
2084− .map_err(|e| Crash::new(format!("cannot reset the working copy: {}", e)))?;
2085− }
2086− }
2087− // Where the focus adds a file and something the directory does not
2088− // track stands (an ignored file, a symlink, a directory holding
2089− // one), jj's checkout would skip the path, and the next run read
2090− // what stands there into the focus: refuse before anything is
2091− // recorded or written, as git does (§7.5 step 7). The trees of
2092− // empty directories found there, and the files holding what the
2093− // checkout writes, are cleared once `op` is published.
2094− let markers = marker_style(self.current_repo().settings())
775+ locked_ws
776+ .locked_wc()
777+ .check_out(commit)
778+ .await
2095779 .map_err(|e| Crash::new(format!("cannot check out the focus: {}", e)))?;
2096− let clear = match on_disk {
2097− None => Vec::new(),
2098− Some(on_disk) => match look_where_added(&root, markers, on_disk, &commit.tree()).await {
2099− Ok(Ok(clear)) => clear,
2100− Ok(Err(in_the_way)) => {
2101− return Err(Crash::new(format!("cannot check out the focus: {}", in_the_way_of(&in_the_way))));
2102− }
2103− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}", e))),
2104− },
2105− };
2106− // from here until the state is saved, a signal to stop waits
2107− let _deferral = Deferral::start();
2108− let published = op.is_some();
2109− let op_id = match op {
2110− None => self.current_repo().operation().id().clone(),
2111− Some(op) => {
2112− let repo = op
2113− .publish()
2114− .await
2115− .map_err(|e| Crash::new(format!("cannot publish the operation: {}", e)))?;
2116− let op_id = repo.operation().id().clone();
2117− *self.inner.repo.lock().unwrap() = repo;
2118− op_id
2119− }
2120− };
2121− // what a crash from here on says: the state is left stale, which the
2122− // next run carries on from (§7.5 step 7)
2123− let carry_on = |what: &str| match back {
2124− None => String::new(),
2125− Some(back) if published => {
2126− format!("; the operation is recorded, but {}: the next command carries on from there{}", what, back)
2127− }
2128− Some(_) => format!("; {}: the next command carries on from there", what),
2129− };
2130− // A checkout cut short leaves the directory holding `on_disk` but
2131− // where it wrote, and the next run takes what it holds for changes
2132− // to the state (`snapshot_tree`). So the state records `on_disk`
2133− // first, at the operation it was saved at, which leaves it stale
2134− // until the checkout completes: were it left recording the tree
2135− // before the run's snapshot, the snapshot's changes, now in the
2136− // operation, would be taken for new ones and replayed onto the
2137− // focus too. It is saved only once `op` is published: a state
2138− // recording the snapshot, beside a head that does not, would lose
2139− // it (§7.4).
2140− if on_disk.is_some_and(|t| t.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids()) {
2141− let old_op = locked_ws.locked_wc().old_operation_id().clone();
2142− let saved = match locked_ws.finish(old_op).await {
2143− Ok(()) => ws_guard.start_working_copy_mutation().await.map_err(|e| e.to_string()),
2144− Err(e) => Err(e.to_string()),
2145− };
2146− locked_ws = match saved {
2147− Ok(locked) => locked,
2148− Err(e) => {
2149− let what = "the working directory was not updated";
2150− return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2151− }
2152− };
2153− }
2154− // what is moved aside is put back where the checkout does not write,
2155− // or cuts the write short, once it returns, however it returns
2156− let mut aside = clear_the_way(&root, markers, &commit.tree(), &clear).await;
2157− let from = locked_ws.locked_wc().old_tree().clone();
2158− let what = "the working directory was only partly updated";
2159− let stats = match locked_ws.locked_wc().check_out(commit).await {
2160− Ok(stats) => stats,
2161− Err(e) => return Err(Crash::new(format!("cannot check out the focus: {}{}", e, carry_on(what)))),
2162− };
2163− // it cut no write short, so a start of a file moved aside, standing
2164− // at its path, was put there meanwhile
2165− aside.returned = true;
2166− // Something the directory does not track, put where the focus adds a
2167− // file since it was looked at, or a file holding what the checkout
2168− // writes there that could not be moved aside, made jj skip the
2169− // path, which it records as written. The state is left stale, as by
2170− // a checkout that fails, so that no run reads what stands there
2171− // (§7.5 step 7).
2172− if stats.skipped_files > 0 {
2173− let skipped = first_skipped(&root, markers, &from, &commit.tree()).await.ok().flatten();
2174− let why = match (skipped, &aside.stuck) {
2175− (Some(path), _) => format!(
2176− "`{}` was not written, as something untracked in the working directory was in its way (move it aside)",
2177− path.as_internal_file_string()
2178− ),
2179− (None, Some((path, e))) => format!(
2180− "`{}` was not written, as the untracked file there, holding what it writes, could not be moved out of its way: {}",
2181− path.as_internal_file_string(),
2182− e
2183− ),
2184− (None, None) => format!(
2185− "{} of its files were not written, as something untracked in the working directory was in their way",
2186− stats.skipped_files
2187− ),
2188− };
2189− return Err(Crash::new(format!("cannot check out the focus: {}{}", why, carry_on(what))));
2190− }
2191− // the state names the operation it was checked out at, so it is
2192− // saved only once that operation is published
2193− if let Err(e) = locked_ws.finish(op_id).await {
2194− let what = "the working copy's record of what the directory holds was not updated";
2195− return Err(Crash::new(format!("cannot save the working copy's state: {}{}", e, carry_on(what))));
2196− }
780+ locked_ws
781+ .finish(op_id)
782+ .await
783+ .map_err(|e| Crash::new(format!("cannot finish the checkout: {}", e)))?;
2197784 Ok(())
2198785 }
2199786
2200787 fn user_signature(&self, cfg: &Config) -> Result<Signature, Crash> {
2201− let (name, email) = validated_identity(cfg)
2202− .map_err(|(_, message)| Crash::new(message))?;
788+ let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)?;
2203789 Ok(Signature {
2204790 name,
2205791 email,
@@ -2292,7 +878,6 @@ impl JjBackend {
2292878 }
2293879
2294880 pub fn cmd_remote(&self, url: &str) -> Result<(), OpenError> {
2295− let url = &origin_url(url)?;
2296881 self.take_lock();
2297882 let base = self.current_repo();
2298883 let origin = RemoteName::new("origin");
@@ -2310,17 +895,7 @@ impl JjBackend {
2310895 .map_err(|e| (1, format!("cannot set the remote: {}", e)))
2311896 }
2312897
2313− /// Run from the top of the workspace, as git runs from the top of its
2314− /// working tree: git resolves a relative local URL (one stored by git,
2315− /// or by `j` before §7.8 resolved them) against the directory it runs
2316− /// in, and jj-lib's git and ours inherit this process's
2317− fn enter_workspace_root(&self) -> Result<(), OpenError> {
2318− std::env::set_current_dir(&self.inner.workspace_root)
2319− .map_err(|e| (1, format!("cannot enter {}: {}", self.inner.workspace_root.display(), e)))
2320− }
2321−
2322898 pub fn cmd_fetch(&self) -> Result<(), OpenError> {
2323− self.enter_workspace_root()?;
2324899 let origin = RemoteName::new("origin");
2325900 let base = self.head_repo()?;
2326901 let mut tx = base.start_transaction();
@@ -2333,52 +908,27 @@ impl JjBackend {
2333908 let _ = block_on(self.publish_tx(tx, "fetch"))?;
2334909 Ok(())
2335910 }
2336− pub fn cmd_push(&self, cfg: &mut Config, expr_text: &str) -> Result<PushOutcome, OpenError> {
2337− self.enter_workspace_root()?;
911+ pub fn cmd_push(&self, cfg: &Config, expr_text: &str) -> Result<(), OpenError> {
2338912 let origin = RemoteName::new("origin");
2339913 let base = self.head_repo()?;
2340− let (value, vis, immutable) = self.eval_push_expr(cfg, expr_text, &base)?;
914+ let (value, vis) = self.eval_push_expr(cfg, expr_text, &base)?;
2341915 let records = parse_push_records(&value, &vis, &base)?;
2342− if records.is_empty() {
2343− return Ok(PushOutcome::default());
2344− }
2345− let deletes_wait = check_push_records(&records, &base, &vis, &immutable)?;
2346− let mut outcome = PushOutcome::default();
2347− for (name, target) in &records {
2348− let before = base.view()
2349− .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
2350− .target.as_resolved().cloned().unwrap_or(None);
2351− if &before == target {
2352− outcome.unchanged += 1;
2353− } else if target.is_none() {
2354− outcome.deleted += 1;
2355− } else {
2356− outcome.published += 1;
2357− }
2358− }
2359− let (_, clean) = self.inspect_working_copy(&base, false)?;
2360− if !clean {
2361− crate::terminal::report(format_args!(
2362− "j: warning: working copy has unrecorded changes; push sends recorded content only, so these edits will not be sent. Run `j id` to record them, then push again to include them."
2363− ));
2364− }
2365− let description = format!("push {}", expr_text);
2366− let _ = self.push_to_origin(&base, origin, &records, deletes_wait, &description)?;
2367− Ok(outcome)
916+ check_push_records(&records, &vis, &base)?;
917+ let _ = self.push_to_origin(&base, origin, &records, expr_text)?;
918+ Ok(())
2368919 }
2369920
2370921 /// evaluate `EXPR` against the recorded repository, without snapshotting
2371− /// (§1.1), apply a function result to the repo value once (§7.6); also
2372− /// return the immutable set of that repository (§7.5 step 3)
922+ /// (§1.1), apply a function result to the repo value once (§7.6)
2373923 fn eval_push_expr(
2374924 &self,
2375− cfg: &mut Config,
925+ cfg: &Config,
2376926 expr_text: &str,
2377927 base: &Arc<ReadonlyRepo>,
2378− ) -> Result<(Value, Arc<VisibleRepo>, BTreeSet<String>), OpenError> {
928+ ) -> Result<(Value, Arc<VisibleRepo>), OpenError> {
2379929 let outer = Rc::new(cfg.global_names.clone());
2380930 let expr = crate::parse::parse_expr(expr_text, outer)
2381− .map_err(|p| (3, crate::diagnostics::format_source_error("expression", expr_text, p.line, &p.msg)))?;
931+ .map_err(|p| (3, format!("line {}: {}", p.line, p.msg)))?;
2382932 let (mut interp, _loaded, current) = self.build_interp(cfg, expr_text, false)?;
2383933 let expr = crate::config::resolve_ids(&expr, &interp).map_err(|failures| {
2384934 let (prefix, candidates) = &failures[0];
@@ -2389,30 +939,15 @@ impl JjBackend {
2389939 }
2390940 })?;
2391941 crate::config::eval_config(&mut interp, cfg).map_err(|c| (3, format!("config.j: {}", c.msg)))?;
2392− // the set a push may not move a bookmark out of (§7.6); build_interp
2393− // evaluated it for this value to place the focus, so it is cached
2394− let immutable = crate::repo::compute_immutable(&mut interp, ¤t)
2395− .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2396− let expression_error = |crash: Crash| {
2397− let mut message = format!("crash: {}", crash.msg);
2398− match &crash.def {
2399− Some(name) => message.push_str(&format!("\n in {}, from push {}", name, expr_text)),
2400− None => message.push_str(&format!("\n from push {}", expr_text)),
2401− }
2402− if let Some(hint) = crate::diagnostics::push_expression_hint(expr_text, cfg, &crash) {
2403− message.push_str(&format!("\n {}", hint));
2404− }
2405− (1, message)
2406− };
2407942 let env = interp.global_env();
2408943 let expr_rc = Rc::new(expr);
2409944 let mut v = interp
2410945 .eval(&expr_rc, &env)
2411− .map_err(&expression_error)?;
946+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2412947 if matches!(v, Value::Fun(_)) {
2413948 v = interp
2414949 .apply(v, current)
2415− .map_err(expression_error)?;
950+ .map_err(|c| (1, format!("crash: {}", c.msg)))?;
2416951 }
2417952 let _ = base;
2418953 let vis = self
@@ -2422,28 +957,19 @@ impl JjBackend {
2422957 .unwrap()
2423958 .clone()
2424959 .ok_or_else(|| (2, "internal: repository value not loaded".to_string()))?;
2425− Ok((v, vis, immutable))
960+ Ok((v, vis))
2426961 }
2427962
2428963 /// push the updates to `origin` and record the new remote-bookmark
2429− /// positions as one operation (§7.6); when `deletes_wait`, the deletes
2430− /// go in a second git push, sent only once the remote has accepted every
2431− /// other update
964+ /// positions as one operation (§7.6)
2432965 fn push_to_origin(
2433966 &self,
2434967 base: &Arc<ReadonlyRepo>,
2435968 origin: &RemoteName,
2436969 records: &[(String, Option<CommitId>)],
2437− deletes_wait: bool,
2438970 description: &str,
2439971 ) -> Result<Arc<ReadonlyRepo>, OpenError> {
2440− // Never hand an empty refspec list to git, whose push.default could
2441− // select branches the expression did not request.
2442− if records.is_empty() {
2443− return Ok(base.clone());
2444− }
2445972 let mut targets = jj_lib::git::GitPushRefTargets::default();
2446− let mut deletes = jj_lib::git::GitPushRefTargets::default();
2447973 for (name, after) in records {
2448974 let before = base
2449975 .view()
@@ -2452,116 +978,48 @@ impl JjBackend {
2452978 .as_resolved()
2453979 .cloned()
2454980 .unwrap_or(None);
2455− // check_push_records sets `deletes_wait` only when some record
2456− // sets a bookmark, so the first push is never empty
2457− let batch = if deletes_wait && after.is_none() { &mut deletes } else { &mut targets };
2458− batch
981+ targets
2459982 .bookmarks
2460983 .push((jj_lib::ref_name::RefNameBuf::from(name.clone()), Diff::new(before, after.clone())));
2461984 }
2462985 let mut tx = base.start_transaction();
2463986 let subprocess_options = self.git_subprocess_options()?;
2464987 let mut callback = QuietGitCallback;
2465− let mut push = |tx: &mut jj_lib::transaction::Transaction, targets| {
2466− jj_lib::git::push_refs(
2467− tx.repo_mut(),
2468− subprocess_options.clone(),
2469− origin,
2470− targets,
2471− &mut callback,
2472− &jj_lib::git::GitPushOptions::default(),
2473− )
2474− .map_err(|e| match e {
2475− jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
2476− other => (1, format!("push failed: {}", other)),
2477− })
2478− };
2479− let mut stats = push(&mut tx, &targets)?;
2480− // git push is not atomic: the remote may accept some updates and
2481− // reject others. When only a bookmark this push sets keeps a deleted
2482− // bookmark's immutable commit reachable, the deletes go in a second
2483− // push once the remote has accepted every update, and are not sent
2484− // otherwise (§7.6)
2485− let mut not_sent: Vec<String> = Vec::new();
2486− let mut failed = None;
2487− if !deletes.bookmarks.is_empty() {
2488− if stats.all_ok() {
2489− match push(&mut tx, &deletes) {
2490− Ok(more) => {
2491− stats.pushed.extend(more.pushed);
2492− stats.rejected.extend(more.rejected);
2493− stats.remote_rejected.extend(more.remote_rejected);
2494− stats.unexported_bookmarks.extend(more.unexported_bookmarks);
2495− }
2496− Err(e) => failed = Some(e),
2497− }
2498− } else {
2499− for (name, _) in &deletes.bookmarks {
2500− not_sent.push(format!("refs/heads/{}", name.as_str()));
2501− }
2502− }
2503− }
2504− // push_refs has set the accepted updates in `tx`, and they are on the
2505− // remote, so they are recorded before the rest is reported; a push
2506− // the remote refused entirely records nothing
2507− let mut repo = base.clone();
2508− if stats.all_ok() || stats.some_exported() {
2509− let unpublished = block_on(tx.write(truncate_chars(description, 200)))
2510− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2511− repo = block_on(unpublished.publish())
2512− .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
2513− *self.inner.repo.lock().unwrap() = repo.clone();
2514− }
2515− if let Some((code, msg)) = failed {
2516− return Err((code, format!("{}; the other bookmarks were pushed and recorded", msg)));
2517− }
988+ let stats = jj_lib::git::push_refs(
989+ tx.repo_mut(),
990+ subprocess_options,
991+ origin,
992+ &targets,
993+ &mut callback,
994+ &jj_lib::git::GitPushOptions::default(),
995+ )
996+ .map_err(|e| match e {
997+ jj_lib::git::GitPushError::NoSuchRemote(_) => (1, "no remote origin".to_string()),
998+ other => (1, format!("push failed: {}", other)),
999+ })?;
25181000 if !stats.all_ok() {
25191001 let mut names: Vec<String> = Vec::new();
25201002 for (name, _) in stats.rejected.iter().chain(stats.remote_rejected.iter()) {
25211003 names.push(name.as_str().to_string());
25221004 }
2523− // pushed, but not exported to the colocated git repo, so jj-lib
2524− // left them out of `tx`
2525− for (symbol, _) in &stats.unexported_bookmarks {
2526− names.push(format!("{} (pushed but not recorded)", symbol.name.as_str()));
2527− }
2528− let mut msg = format!("push rejected: {}", names.join(", "));
2529− if !not_sent.is_empty() {
2530− msg.push_str(&format!("; deletes not sent: {}", not_sent.join(", ")));
2531− }
2532− if stats.some_exported() {
2533− msg.push_str("; the other bookmarks were pushed and recorded");
2534− }
2535− return Err((1, msg));
1005+ return Err((1, format!("push rejected: {}", names.join(", "))));
25361006 }
1007+ let unpublished = block_on(tx.write(truncate_chars(description, 200)))
1008+ .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
1009+ let repo = block_on(unpublished.publish())
1010+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1011+ *self.inner.repo.lock().unwrap() = repo.clone();
25371012 Ok(repo)
25381013 }
25391014
2540− pub fn cmd_undo(&self, redo: bool) -> Result<String, OpenError> {
1015+ pub fn cmd_undo(&self, redo: bool) -> Result<(), OpenError> {
25411016 let base = self.head_repo()?;
2542− let on_disk = self.check_wc_clean(&base)?;
1017+ self.check_wc_clean(&base)?;
25431018 let loader = self.repo_loader();
25441019 let mut cur = base.operation().clone();
25451020 let target_op_id: OperationId;
25461021 loop {
2547− let marker = op_marker(cur.metadata());
2548− if let Some(m) = marker.as_ref().filter(|m| m.restores_view) {
2549− // jj 0.45 names the operation whose view was restored.
2550− // Repeating the same direction follows that target; reversing
2551− // direction restores the marker's parent. The target may
2552− // itself be an undo or redo, not just a normal operation.
2553− if (m.kind == "redo") == redo {
2554− cur = block_on(loader.load_operation(&m.id))
2555− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2556− continue;
2557− }
2558− let Some(parent) = cur.parent_ids().first().cloned() else {
2559− return Err((1, if redo { "nothing to redo" } else { "nothing to undo" }.to_string()));
2560− };
2561− target_op_id = parent;
2562− break;
2563− }
2564− match (marker, redo) {
1022+ match (op_marker(cur.metadata()), redo) {
25651023 (None, false) => {
25661024 let Some(parent) = cur.parent_ids().first().cloned() else {
25671025 return Err((1, "nothing to undo".to_string()));
@@ -2587,25 +1045,11 @@ impl JjBackend {
25871045 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
25881046 }
25891047 (Some(m), true) if m.kind == "undo" => {
2590− // An old j marker names the discarded normal operation,
2591− // but its parent has the same view and retains any earlier
2592− // undos. Modern redo markers must keep that navigation
2593− // point so another redo can continue across the upgrade.
2594− target_op_id = if m.authored_by_j {
2595− cur.parent_ids().first().cloned()
2596− .ok_or_else(|| (1, "nothing to redo".to_string()))?
2597− } else {
2598− m.id
2599− };
1048+ target_op_id = m.id;
26001049 break;
26011050 }
26021051 (Some(m), false) => {
2603− // a redo: "move past the operation it reapplied" (§7.7).
2604− // The redo marker names the *undo* it reversed; the
2605− // operation that undo discarded is the one this redo put
2606− // back, so that is where the walk continues. Stopping at
2607− // the undo instead would make the loop treat it as an undo
2608− // to follow and step back one operation too far.
1052+ // a redo: move past the undo it reversed
26091053 if !m.authored_by_j {
26101054 let Some(parent) = cur.parent_ids().first().cloned() else {
26111055 return Err((1, "nothing to undo".to_string()));
@@ -2613,57 +1057,23 @@ impl JjBackend {
26131057 target_op_id = parent;
26141058 break;
26151059 }
2616− let undo_op = block_on(loader.load_operation(&m.id))
1060+ cur = block_on(loader.load_operation(&m.id))
26171061 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2618− cur = match op_marker(undo_op.metadata()) {
2619− Some(u) if u.kind == "undo" && u.authored_by_j => {
2620− block_on(loader.load_operation(&u.id))
2621− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?
2622− }
2623− // cannot follow the marker: degrade to the undo itself
2624− _ => undo_op,
2625− };
26261062 }
26271063 (Some(m), true) => {
2628− // a redo: the undo it reversed is already accounted for,
2629− // so keep looking outward from that undo's parent for an
2630− // undo that has not been reversed yet (§7.7: the head must
2631− // be an undo, "possibly reached through redos").
2632− if !m.authored_by_j {
2633− target_op_id = m.id;
2634− break;
2635− }
2636− let Some(prev) = op_parent_id(&loader, &m.id)? else {
2637− return Err((1, "nothing to redo".to_string()));
1064+ // a redo: reverse the undo it reversed, following further
1065+ // redos to the operation ultimately reapplied
1066+ target_op_id = if m.authored_by_j {
1067+ block_on(chain_through_redos(&loader, &m.id))?
1068+ } else {
1069+ m.id
26381070 };
2639− cur = block_on(loader.load_operation(&prev))
2640− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1071+ break;
26411072 }
26421073 }
26431074 }
2644− // before the operation init or clone recorded there was no
2645− // repository (§7.7); the view jj recorded then has an empty child of
2646− // the root as the working-copy commit, and checking it out would
2647− // delete every file of the head
2648− if !redo && op_created_repo(cur.metadata()) {
2649− return Err((1, "nothing to undo".to_string()));
2650− }
2651− let mut target_op = block_on(loader.load_operation(&target_op_id))
1075+ let target_op = block_on(loader.load_operation(&target_op_id))
26521076 .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2653− // jj also follows markers in the selected direction at the target:
2654− // undoing a redo whose parent is an undo restores that undo's target,
2655− // and conversely for redo. Keep the same canonical navigation point.
2656− while let Some(m) = op_marker(target_op.metadata())
2657− .filter(|m| m.restores_view && (m.kind == "redo") == redo)
2658− {
2659− target_op = block_on(loader.load_operation(&m.id))
2660− .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
2661− }
2662− let target_op_id = target_op.id().clone();
2663− // Report the expression whose state is being reversed or restored,
2664− // following restore markers left by repeated undo/redo commands.
2665− let affected = if redo { &target_op } else { &cur };
2666− let affected_description = restored_operation_description(&loader, affected);
26671077 let target_view = block_on(
26681078 loader
26691079 .op_store()
@@ -2687,94 +1097,56 @@ impl JjBackend {
26871097 },
26881098 ));
26891099 }
2690− // Use jj's current marker so either CLI can continue this history.
2691− // The older j and jj marker formats remain readable above.
2692− let marker = format!(
2693− "{}: restore to operation {}",
2694− if redo { "redo" } else { "undo" },
2695− target_op_id.hex()
2696− );
1100+ let marker = if redo {
1101+ format!("redo: {}", cur.id().hex())
1102+ } else {
1103+ format!("undo: {}", cur.id().hex())
1104+ };
26971105 let mut tx = base.start_transaction();
26981106 tx.repo_mut().set_view(target_view.clone());
26991107 let unpublished = block_on(tx.write(&marker))
27001108 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
2701− // the operation is published, then the restored focus checked out
2702− // from what the directory holds (§7.7)
2703− let wc_id = &target_view.wc_commit_ids[WorkspaceName::DEFAULT];
2704− let wc_commit = block_on(base.store().get_commit_async(wc_id))
2705− .map_err(|e| (2, format!("cannot read the working-copy commit: {}", e)))?;
2706− let back = if redo { ", and `j undo` goes back" } else { ", and `j redo` goes back" };
2707− block_on(self.checkout(&wc_commit, Some(&on_disk), Some(unpublished), Some(back)))
2708− .map_err(|c| (1, c.msg))?;
2709− let focus_message = wc_commit.description().lines().next().unwrap_or("").trim();
2710− Ok(format!(
2711− "{} `{}`; restored focus @{} {}",
2712− if redo { "Redid" } else { "Undid" },
2713− affected_description,
2714− wc_commit.change_id().reverse_hex(),
2715− terminal_text(if focus_message.is_empty() { "(no description)" } else { focus_message }),
2716− ))
2717− }
2718−
2719− /// Refuse, as `undo` and `redo` do before anything else (§7.7), when the
2720− /// working directory holds changes the working-copy commit does not:
2721− /// what a snapshot would record (§7.4), which their checkout would write
2722− /// over. Otherwise return the tree the directory holds, as scanned,
2723− /// which that checkout starts from: the commit's, but for the text
2724− /// written for a conflict the state does not record, as one lost and
2725− /// made again records none, or, where the working-copy state is stale,
2726− /// the state's with any changes to it the commit already has, such as
2727− /// what a checkout cut short wrote. Reserved commands never snapshot,
2728− /// so nothing is recorded either way.
2729− fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<MergedTree, OpenError> {
2730− let (scanned, clean) = self.inspect_working_copy(base, true)?;
2731− if !clean {
2732− return Err((1, "working copy has changes not in @; run `j id` to record them or discard them. Recording creates a new operation: the next `j undo` undoes that recording, not the operation you originally wanted to undo; a new recording also clears pending redos. Use `j ops` to inspect the history".to_string()));
2733− }
2734− Ok(scanned)
1109+ let op_id = unpublished.operation().id().clone();
1110+ let repo = block_on(unpublished.publish())
1111+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1112+ *self.inner.repo.lock().unwrap() = repo.clone();
1113+ let wc_commit = block_on(self.wc_commit(&repo))?;
1114+ block_on(self.checkout(&wc_commit, op_id)).map_err(|c| (1, c.msg))?;
1115+ Ok(())
27351116 }
27361117
2737− /// Compare a scan with the recorded focus without publishing a snapshot.
2738− /// Push only inspects: it must leave the saved working-copy state alone
2739− /// so a later `j id` still sees every unrecorded edit.
2740− fn inspect_working_copy(
2741− &self,
2742− base: &Arc<ReadonlyRepo>,
2743− refresh_clean_state: bool,
2744− ) -> Result<(MergedTree, bool), OpenError> {
2745− let wc_tree = block_on(self.wc_commit(base))?.tree();
1118+ /// refuse if the working directory differs from the focused commit's
1119+ /// files (§7.7): reserved commands never snapshot, so compare the locked
1120+ /// working copy's recorded tree with the focus commit's tree
1121+ fn check_wc_clean(&self, base: &Arc<ReadonlyRepo>) -> Result<(), OpenError> {
1122+ let dirty = (1, "working copy has changes not in @; run `j id` to record them or discard them".to_string());
1123+ let wc_commit = block_on(self.wc_commit(base))?;
1124+ // snapshot the working directory (without persisting anything) and
1125+ // compare against the focused commit's files (§7.7)
27461126 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);
27491127 let mut locked_ws = block_on(ws_guard.start_working_copy_mutation())
27501128 .map_err(|e| (2, format!("cannot lock the working copy: {}", e)))?;
2751− let old_tree = locked_ws.locked_wc().old_tree().clone();
2752− let scanned = block_on(scan(locked_ws.locked_wc(), &root, &old_tree, &saved, &snapshot_options()))
1129+ let options = SnapshotOptions {
1130+ base_ignores: GitIgnoreFile::empty(),
1131+ progress: None,
1132+ start_tracking_matcher: &EverythingMatcher,
1133+ force_tracking_matcher: &NothingMatcher,
1134+ max_new_file_size: u64::MAX,
1135+ };
1136+ let (new_tree, _stats) = block_on(locked_ws.locked_wc().snapshot(&options))
27531137 .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2754− let current = locked_ws.locked_wc().old_operation_id() == base.operation().id();
2755− let recorded = match marker_style(base.settings()) {
2756− Ok(markers) => block_on(snapshot_tree(&root, markers, &wc_tree, &old_tree, current, &scanned)),
2757− Err(e) => Err(e),
2758− }
2759− .map_err(|e| (2, format!("cannot read the working copy: {}", e)))?;
2760− if recorded.tree_ids() != wc_tree.tree_ids() {
2761− // drop the lock without saving, or the state would claim the
2762− // directory was snapshotted and hide the changes from the next
2763− // run (§7.7)
2764− return Ok((scanned, false));
2765− }
2766− // saving the state only refreshes what it knows of each file, or
2767− // takes in what the commit already has
2768− if refresh_clean_state {
2769− block_on(save_scan(locked_ws, &scanned, &wc_tree, base.operation().id()))
2770− .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1138+ let same = new_tree.tree_ids() == wc_commit.tree().tree_ids();
1139+ // discard: finish against the current op so no state changes
1140+ block_on(locked_ws.finish(base.operation().id().clone()))
1141+ .map_err(|e| (2, format!("cannot finish: {}", e)))?;
1142+ drop(ws_guard);
1143+ if !same {
1144+ return Err(dirty);
27711145 }
2772− Ok((scanned, true))
1146+ Ok(())
27731147 }
27741148
2775− /// The operation log for `ops` (§7.7), one line per operation, newest
2776− /// first; the caller writes it to stdout.
2777− pub fn cmd_ops(&self) -> Result<String, OpenError> {
1149+ pub fn cmd_ops(&self) -> Result<(), OpenError> {
27781150 let base = self.head_repo()?;
27791151 let loader = self.repo_loader();
27801152 let head = base.operation().clone();
@@ -2787,8 +1159,8 @@ impl JjBackend {
27871159 let mut tag = "";
27881160 match op_kind(op.metadata()) {
27891161 OpKind::Normal => {}
2790− OpKind::Undo => tag = "undo",
2791− OpKind::Redo => tag = "redo",
1162+ OpKind::Undo { .. } => tag = "undo",
1163+ OpKind::Redo { .. } => tag = "redo",
27921164 }
27931165 if desc.is_empty() {
27941166 desc = "(no description)".to_string();
@@ -2811,7 +1183,8 @@ impl JjBackend {
28111183 None => None,
28121184 };
28131185 }
2814− Ok(out)
1186+ print!("{}", out);
1187+ Ok(())
28151188 }
28161189}
28171190
@@ -2848,7 +1221,6 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
28481221 }
28491222 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
28501223 let backend = JjBackend {
2851− replay_cache: Rc::new(ReplayCache::default()),
28521224 inner: Arc::new(JjInner {
28531225 workspace_root: cwd.clone(),
28541226 workspace: Mutex::new(workspace),
@@ -2857,43 +1229,23 @@ pub fn cmd_init(cfg: &Config) -> Result<(), OpenError> {
28571229 visible: Mutex::new(None),
28581230 lock_guard: Mutex::new(None),
28591231 pending: Mutex::new(None),
2860− folds_case: OnceLock::new(),
2861− long_names: Mutex::new(HashMap::new()),
28621232 }),
28631233 };
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();
28671234 if had_git {
28681235 let import_options = backend.git_import_options()?;
1236+ let mut tx = backend.current_repo().start_transaction();
28691237 block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
28701238 .map_err(|e| (2, format!("cannot import the git history: {}", e)))?;
2871− // HEAD too: it is the head the working-copy commit starts on, and a
2872− // detached HEAD's commits are reachable from no ref
2873− block_on(jj_lib::git::import_head(
2874− tx.repo_mut(),
2875− &backend.inner.workspace_name,
2876− &cwd,
2877− ))
2878− .map_err(|e| (2, format!("cannot import the git HEAD: {}", e)))?;
28791239 block_on(tx.repo_mut().rebase_descendants())
28801240 .map_err(|e| (2, format!("cannot rebase descendants: {}", e)))?;
1241+ let _ = block_on(backend.publish_tx(tx, "import git refs"))?;
28811242 }
2882− // §7.8: the current head is git's HEAD (none on an unborn branch)
2883− let head = tx
2884− .repo()
2885− .view()
2886− .git_head(&backend.inner.workspace_name)
2887− .as_resolved()
2888− .cloned()
2889− .flatten();
2890− create_initial_wc_commit(&backend, cfg, tx, "init", head, had_git, Some(""))?;
1243+ create_initial_wc_commit(&backend, cfg, "init")?;
28911244 Ok(())
28921245}
28931246
28941247pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
28951248 let settings = user_settings_strict(cfg)?;
2896− let url = &origin_url(url)?;
28971249 let dir_path = PathBuf::from(dir);
28981250 if dir_path.join(".jj").is_dir() {
28991251 return Err((2, format!("{} already contains a jj repository", dir)));
@@ -2907,296 +1259,82 @@ pub fn cmd_clone(cfg: &Config, url: &str, dir: &str) -> Result<(), OpenError> {
29071259 } else if dir_path.exists() {
29081260 return Err((2, format!("{} already exists and is not empty", dir)));
29091261 }
2910− let existed = dir_path.is_dir();
2911− // the directory an existing DIR leads to, through a symlink DIR may be:
2912− // a clone that fails empties it only while DIR still leads there (§7.8)
2913− let found = if existed { dir_identity(&dir_path, true) } else { None };
2914− // the directories this run creates, in order, each with its device and
2915− // inode: a clone that fails removes DIR and those of its parents left
2916− // empty, or an existing DIR's entries, and nothing else (§7.8).
2917− // Each is made by `create_dir`, whose success proves it new. The checks
2918− // above read DIR as written, before its missing parents exist; once
2919− // they do, `new/..` or `new/../keep` names a directory that was there,
2920− // so DIR's own `create_dir` failing, AlreadyExists included, refuses it
2921− // as `git clone` does
2922− let mut created: Vec<(&std::path::Path, Option<(u64, u64)>)> = Vec::new();
2923− // those of them a failed clone leaves, as their paths no longer lead
2924− // to them
2925− let mut left: Vec<&std::path::Path> = Vec::new();
2926− // from here until the clone has removed what it made or completed, a
2927− // signal to stop waits, and fails a clone that has not yet made its
2928− // working-copy commit (`unless_interrupted`, §1.3)
2929− let _held = Deferral::start();
2930− if !existed {
2931− // DIR and its missing parents, DIR first ("" is the current
2932− // directory, which exists)
2933− let missing: Vec<&std::path::Path> = dir_path
2934− .ancestors()
2935− .enumerate()
2936− .take_while(|(i, p)| *i == 0 || !(p.as_os_str().is_empty() || p.exists()))
2937− .map(|(_, p)| p)
2938− .collect();
2939− for p in missing.into_iter().rev() {
2940− match std::fs::create_dir(p) {
2941− Ok(()) => created.push((p, dir_identity(p, false))),
2942− // a parent reached again through `..`
2943− Err(e)
2944− if e.kind() == std::io::ErrorKind::AlreadyExists
2945− && p != dir_path
2946− && p.is_dir() => {}
2947− Err(e) => {
2948− for &(p, id) in created.iter().rev() {
2949− if !remove_created_dir(p, id, false) {
2950− left.push(p);
2951− }
2952− }
2953− let msg = format!("cannot create {}: {}", dir, e);
2954− return Err((2, msg + &left_note(&left)));
2955− }
2956− }
2957− }
2958− }
2959− let cloned = clone_into(cfg, &settings, url, &dir_path);
2960− let Err((code, msg)) = cloned else {
2961− return Ok(());
2962− };
2963− // like `git clone`, a clone that fails leaves nothing behind: its
2964− // working-copy commit would claim files the checkout never wrote, and a
2965− // later clone would refuse the directory (§7.8)
2966− if !existed {
2967− // DIR, the last one created, goes whole; a parent holds nothing of
2968− // the clone's but DIR, so it goes only if left empty: what else was
2969− // put there meanwhile, a sibling clone say, stays
2970− for (i, &(p, id)) in created.iter().enumerate().rev() {
2971− if !remove_created_dir(p, id, i + 1 == created.len()) {
2972− left.push(p);
2973− }
2974− }
2975− Err((code, msg + &left_note(&left)))
2976− } else if found.is_some() && dir_identity(&dir_path, true) == found {
2977− if let Ok(entries) = std::fs::read_dir(&dir_path) {
2978− for entry in entries.flatten() {
2979− let _ = match entry.file_type() {
2980− Ok(t) if t.is_dir() => std::fs::remove_dir_all(entry.path()),
2981− _ => std::fs::remove_file(entry.path()),
2982− };
2983− }
2984− }
2985− Err((code, msg))
2986− } else if dir_path.symlink_metadata().is_ok() {
2987− // a symlink put in place of DIR, or of a directory above it, would
2988− // have `read_dir` list what that names
2989− Err((
2990− code,
2991− format!(
2992− "{}; left what `{}` holds, as it is no longer the directory this clone found empty",
2993− msg, dir
2994− ),
2995− ))
2996− } else {
2997− Err((code, msg))
2998− }
2999−}
3000−
3001−/// The device and inode of the directory at `path`, following a symlink
3002−/// there only when `follow` says so; `None` when `path` holds no directory
3003−fn dir_identity(path: &std::path::Path, follow: bool) -> Option<(u64, u64)> {
3004− use std::os::unix::fs::MetadataExt;
3005− let meta = if follow {
3006− std::fs::metadata(path)
3007− } else {
3008− std::fs::symlink_metadata(path)
3009− };
3010− meta.ok().filter(|m| m.is_dir()).map(|m| (m.dev(), m.ino()))
3011−}
3012−
3013−/// Remove the directory a failed clone created at `path`, `whole` or only
3014−/// if empty, while `path` still leads to it: the directory whose device and
3015−/// inode `id` were read on creating it (§7.8). A symlink put in its place,
3016−/// or in place of a directory above it, would have the removal follow it
3017−/// and delete what that names, so a path leading anywhere else is left, and
3018−/// the result is false. One leading nowhere holds nothing to remove.
3019−fn remove_created_dir(path: &std::path::Path, id: Option<(u64, u64)>, whole: bool) -> bool {
3020− if id.is_some() && dir_identity(path, false) == id {
3021− let _ = if whole {
3022− std::fs::remove_dir_all(path)
3023− } else {
3024− std::fs::remove_dir(path)
3025− };
3026− true
3027− } else {
3028− path.symlink_metadata().is_err()
3029− }
3030−}
3031−
3032−/// What a failed clone's error adds for the directories it created and
3033−/// `left` (§7.8): nothing when it left none
3034−fn left_note(left: &[&std::path::Path]) -> String {
3035− let named = |ps: &[&std::path::Path]| {
3036− ps.iter().map(|p| format!("`{}`", p.display())).collect::<Vec<_>>().join(", ")
3037− };
3038− match left {
3039− [] => String::new(),
3040− [one] => format!(
3041− "; left {}, as it is no longer the directory this clone created",
3042− named(&[one])
3043− ),
3044− [init @ .., last] => format!(
3045− "; left {} and {}, as they are no longer the directories this clone created",
3046− named(init),
3047− named(&[last])
3048− ),
3049− }
3050−}
3051−
3052−/// `url` as `origin` holds it (§7.8): a local path resolved against the
3053−/// current directory, as `jj git clone` stores it, for git resolves a
3054−/// relative one against the directory each later `fetch` or `push` runs
3055−/// in; one under a home directory (`~/…`, `~user/…`) and any other URL, a
3056−/// `file://` one included, as given. One that does not parse, or a path
3057−/// that does not resolve to UTF-8, is a usage error.
3058−fn origin_url(url: &str) -> Result<String, OpenError> {
3059− let mut parsed = gix::url::parse(url).map_err(|e| (2, format!("invalid URL `{}`: {}", url, e)))?;
3060− // a bare path is a file location in the alternative form
3061− if parsed.scheme != gix::url::Scheme::File || !parsed.serialize_alternative_form {
3062− return Ok(url.to_string());
3063− }
3064− // git expands a leading `~` to the home directory on every fetch and
3065− // push, from wherever it runs; resolved here, it would name a directory
3066− // called `~` (`./~/…` still does)
3067− if parsed.path.starts_with(b"~") {
3068− return Ok(url.to_string());
3069− }
3070− let cwd = std::env::current_dir()
3071− .map_err(|e| (2, format!("cannot read the current directory: {}", e)))?;
3072− parsed
3073− .canonicalize(&cwd)
3074− .map_err(|e| (2, format!("cannot resolve `{}`: {}", url, e)))?;
3075− // `add_remote` takes UTF-8, which only the current directory's name or
3076− // a symlink's target can break; stored as given, the path would reach
3077− // the remote only from where it was typed
3078− String::from_utf8(parsed.to_bstring().into())
3079− .map_err(|_| (2, format!("cannot resolve `{}`: the path it names is not UTF-8", url)))
3080−}
3081−
3082−/// `clone`'s work once `dir_path` exists and is empty (§7.8)
3083−fn clone_into(
3084− cfg: &Config,
3085− settings: &UserSettings,
3086− url: &str,
3087− dir_path: &std::path::Path,
3088−) -> Result<(), OpenError> {
1262+ std::fs::create_dir_all(&dir_path)
1263+ .map_err(|e| (2, format!("cannot create {}: {}", dir, e)))?;
30891264 let (workspace, repo) = block_on(Workspace::init_colocated_git(
3090− settings,
3091− dir_path,
1265+ &settings,
1266+ &dir_path,
30921267 gix::hash::Kind::Sha1,
30931268 ))
30941269 .map_err(|e| (2, format!("cannot create the repository: {}", e)))?;
30951270 let backend = JjBackend {
3096− replay_cache: Rc::new(ReplayCache::default()),
30971271 inner: Arc::new(JjInner {
3098− workspace_root: dir_path.to_path_buf(),
1272+ workspace_root: dir_path.clone(),
30991273 workspace: Mutex::new(workspace),
31001274 repo: Mutex::new(repo),
31011275 workspace_name: WorkspaceName::DEFAULT.to_owned(),
31021276 visible: Mutex::new(None),
31031277 lock_guard: Mutex::new(None),
31041278 pending: Mutex::new(None),
3105− folds_case: OnceLock::new(),
3106− long_names: Mutex::new(HashMap::new()),
31071279 }),
31081280 };
31091281 let origin = RemoteName::new("origin");
31101282 let mut tx = backend.current_repo().start_transaction();
3111− // a signal to stop fails each step before the working-copy commit's
3112− // (§1.3)
3113− unless_interrupted(
3114− jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
3115− .map_err(|e| (1, format!("cannot set the remote: {}", e))),
3116− )?;
3117− unless_interrupted(backend.git_fetch_refs(tx.repo_mut(), origin))?;
3118− let default_branch = unless_interrupted(git_default_branch(
3119− git_backend(tx.repo().store())?.git_repo_path(),
3120− origin,
3121− ))?;
1283+ jj_lib::git::add_remote(tx.repo_mut(), origin, url, None)
1284+ .map_err(|e| (1, format!("cannot set the remote: {}", e)))?;
1285+ backend.git_fetch_refs(tx.repo_mut(), origin)?;
31221286 let import_options = backend.git_import_options()?;
3123− unless_interrupted(
3124− block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
3125− .map_err(|e| (1, format!("cannot import the fetched refs: {}", e))),
3126− )?;
3127− unless_interrupted(
3128− block_on(tx.repo_mut().rebase_descendants())
3129− .map_err(|e| (1, format!("cannot rebase descendants: {}", e))),
3130− )?;
3131− // §7.8: the default bookmark's target (none when the remote's HEAD names
3132− // no branch, or a branch it does not have)
3133− let head = default_branch.and_then(|name| {
3134− tx.repo()
3135− .view()
3136− .get_remote_bookmark(RefName::new(&name).to_remote_symbol(origin))
3137− .target
3138− .as_resolved()
3139− .cloned()
3140− .flatten()
3141− });
3142− // the fetch and the working-copy commit are one operation, the one undo
3143− // never undoes (§7.7)
3144− create_initial_wc_commit(&backend, cfg, tx, &format!("clone {}", url), head, false, None)?;
1287+ block_on(jj_lib::git::import_refs(tx.repo_mut(), &import_options))
1288+ .map_err(|e| (1, format!("cannot import the fetched refs: {}", e)))?;
1289+ block_on(tx.repo_mut().rebase_descendants())
1290+ .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
1291+ let _ = block_on(backend.publish_tx(tx, &format!("clone {}", url)))?;
1292+ create_initial_wc_commit(&backend, cfg, &format!("clone {}", url))?;
31451293 Ok(())
31461294}
31471295
3148−/// §7.8: after init/clone, create a working-copy commit as a child of
3149−/// `head` (the root commit when there is none), record it in `tx` with
3150−/// what the command has imported, and check it out. The commit holds its
3151−/// parent's files less any a checkout cannot create (`checkout_tree`), so
3152−/// it is empty unless the parent has one. `adopt` says the working
3153−/// directory already holds a checkout of the parent (init over an existing
3154−/// git repository): the working copy is then reset to the commit instead of
3155−/// written, so no file is touched and what the directory holds beyond the
3156−/// parent is the next snapshot's change. `back` is what a checkout that
3157−/// fails says (`JjBackend::checkout`): none for a clone, which then removes
3158−/// what it made.
1296+/// §7.8: after init/clone, create a working-copy commit as a child of the
1297+/// current head (a bookmark target if there is one, otherwise the root
1298+/// commit) and check it out.
31591299fn create_initial_wc_commit(
31601300 backend: &JjBackend,
31611301 cfg: &Config,
3162− mut tx: jj_lib::transaction::Transaction,
31631302 description: &str,
3164− head: Option<CommitId>,
3165− adopt: bool,
3166− back: Option<&str>,
31671303) -> Result<(), OpenError> {
3168− let store = tx.base_repo().store().clone();
3169− let parent = head.unwrap_or_else(|| store.root_commit_id().clone());
1304+ let base = backend.current_repo();
1305+ let store = base.store().clone();
1306+ let mut parent = store.root_commit_id().clone();
1307+ 'outer: for (_name, target) in base.view().local_bookmarks() {
1308+ if let Some(id) = target.as_resolved().and_then(|t| t.clone()) {
1309+ parent = id;
1310+ break 'outer;
1311+ }
1312+ }
1313+ if parent == *store.root_commit_id() {
1314+ for (_name, remote_ref) in base.view().remote_bookmarks(RemoteName::new("origin")) {
1315+ if let Some(id) = remote_ref.target.as_resolved().and_then(|t| t.clone()) {
1316+ parent = id;
1317+ break;
1318+ }
1319+ }
1320+ }
31701321 // jj's own init already created a working-copy commit; if it is already
31711322 // a child of the target parent, keep it and just check it out
31721323 let mut to_abandon: Option<CommitId> = None;
3173− let existing = tx.repo().view().get_wc_commit_id(&backend.inner.workspace_name).cloned();
3174− if let Some(existing) = existing.and_then(|id| block_on(store.get_commit_async(&id)).ok()) {
3175− if existing.parent_ids().first() == Some(&parent) {
3176− // jj's commit is an empty child of the root; record only what
3177− // was imported, if anything
3178− let op = if tx.repo().has_changes() {
3179− Some(
3180− block_on(tx.write(truncate_chars(description, 200)))
3181− .map_err(|e| (1, format!("cannot write the operation: {}", e)))?,
3182− )
3183− } else {
3184− None
3185− };
3186− block_on(backend.checkout(&existing, None, op, back)).map_err(|c| (1, c.msg))?;
1324+ if let Ok(existing) = block_on(backend.wc_commit(&base)) {
1325+ if existing.parent_ids().first() == Some(&parent) {
1326+ let op_id = base.operation().id().clone();
1327+ block_on(backend.checkout(&existing, op_id)).map_err(|c| (1, c.msg))?;
31871328 return Ok(());
31881329 }
31891330 if existing.id() != store.root_commit_id() {
31901331 to_abandon = Some(existing.id().clone());
31911332 }
31921333 }
3193− let parent_tree = block_on(store.get_commit_async(&parent))
3194− .map_err(|e| (1, format!("cannot read the parent commit: {}", e)))?
3195− .tree();
3196− let wc_tree = block_on(checkout_tree(parent_tree, backend))?;
31971334 let user_sig = backend
31981335 .user_signature(cfg)
31991336 .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
1337+ let mut tx = base.start_transaction();
32001338 if let Some(old_wc) = &to_abandon {
32011339 tx.repo_mut().record_abandoned_commit_with_parents(
32021340 old_wc.clone(),
@@ -3205,7 +1343,7 @@ fn create_initial_wc_commit(
32051343 }
32061344 let wc = block_on(
32071345 tx.repo_mut()
3208− .new_commit(vec![parent], wc_tree)
1346+ .new_commit(vec![parent], store.empty_merged_tree())
32091347 .set_author(user_sig.clone())
32101348 .set_committer(user_sig)
32111349 .write(),
@@ -3218,49 +1356,19 @@ fn create_initial_wc_commit(
32181356 .map_err(|e| (1, format!("cannot rebase descendants: {}", e)))?;
32191357 let unpublished = block_on(tx.write(truncate_chars(description, 200)))
32201358 .map_err(|e| (1, format!("cannot write the operation: {}", e)))?;
3221− // an adopted directory is reset to the commit, not checked out over:
3222− // checking out from the empty tree jj's init recorded would recreate
3223− // the files the user deleted there. Anything else is written, once the
3224− // operation is published (§7.5 step 7).
3225− let on_disk = if adopt { Some(wc.tree()) } else { None };
3226− block_on(backend.checkout(&wc, on_disk.as_ref(), Some(unpublished), back)).map_err(|c| (1, c.msg))?;
1359+ let op_id = unpublished.operation().id().clone();
1360+ let repo = block_on(unpublished.publish())
1361+ .map_err(|e| (1, format!("cannot publish the operation: {}", e)))?;
1362+ *backend.inner.repo.lock().unwrap() = repo;
1363+ block_on(backend.checkout(&wc, op_id)).map_err(|c| (1, c.msg))?;
32271364 Ok(())
32281365}
32291366
3230−/// `tree` without the files at a path a checkout cannot create (§7.5 step
3231−/// 1), which a git branch can hold: a `.jj` directory committed by mistake,
3232−/// a name the workspace's filesystem does not hold. jj's checkout stops at
3233−/// the first such path, failing the whole clone, so the working-copy commit
3234−/// init or clone starts on leaves them out, and their removal is its change
3235−/// (§7.8).
3236−async fn checkout_tree(tree: MergedTree, backend: &JjBackend) -> Result<MergedTree, OpenError> {
3237− let folds_case = backend.folds_case();
3238− let refused: Vec<RepoPathBuf> = tree
3239− .entries()
3240− .map(|(path, _)| path)
3241− .filter(|path| {
3242− path.components().any(|c| {
3243− crate::repo::checkout_refuses(c.as_internal_str(), folds_case, |name| backend.name_fits(name))
3244− })
3245− })
3246− .collect();
3247− if refused.is_empty() {
3248− return Ok(tree);
3249− }
3250− let mut builder = MergedTreeBuilder::new(tree);
3251− for path in refused {
3252− builder.set_or_remove(path, Merge::absent());
3253− }
3254− builder
3255− .write_tree()
3256− .await
3257− .map_err(|e| (1, format!("cannot write a tree: {}", e)))
3258−}
3259−
32601367/// user settings for init/clone: a missing or malformed `user` is a
32611368/// configuration error (exit 3) (§7.9)
32621369fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
3263− let (name, email) = validated_identity(cfg)?;
1370+ let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
1371+ .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
32641372 let mut config = StackedConfig::with_defaults();
32651373 let text = format!(
32661374 "[user]\nname = {}\nemail = {}\n",
@@ -3274,39 +1382,6 @@ fn user_settings_strict(cfg: &Config) -> Result<UserSettings, OpenError> {
32741382 .map_err(|e| (3, format!("config.j: invalid user settings: {}", e)))
32751383}
32761384
3277−/// The identity shown during repository setup. Validation is deliberately
3278−/// confined to authoring boundaries so inspection and recovery still work
3279−/// with an unchanged reference configuration.
3280−pub fn configured_identity(cfg: &Config) -> Result<String, OpenError> {
3281− let (name, email) = validated_identity(cfg)?;
3282− Ok(format!("{} <{}>", terminal_text(&name), terminal_text(&email)))
3283−}
3284−
3285−fn terminal_text(text: &str) -> String {
3286− text.chars().map(|c| {
3287− if c.is_control() { c.escape_debug().to_string() } else { c.to_string() }
3288− }).collect()
3289−}
3290−
3291−fn validated_identity(cfg: &Config) -> Result<(String, String), OpenError> {
3292− let (name, email) = crate::config::eval_user_only(&mut Interp::dummy(), cfg)
3293− .map_err(|c| (3, format!("config.j: {}", c.msg)))?;
3294− check_identity(&name, &email)?;
3295− Ok((name, email))
3296−}
3297−
3298−fn check_identity(name: &str, email: &str) -> Result<(), OpenError> {
3299− if name.trim().is_empty() || email.trim().is_empty()
3300− || name.trim() == "Your Name" || email.trim().eq_ignore_ascii_case("you@example.com")
3301− {
3302− return Err((3, format!(
3303− "config.j: user identity is not configured ({} <{}>); set `user.name` and `user.email` in the `user` record before creating or recording commits",
3304− terminal_text(name), terminal_text(email),
3305− )));
3306− }
3307− Ok(())
3308−}
3309−
33101385// ----------------------------------------------------------------------
33111386// reserved-command helpers
33121387// ----------------------------------------------------------------------
@@ -3370,72 +1445,41 @@ fn git_fetch_subprocess(git_dir: &std::path::Path, remote: &RemoteName) -> Resul
33701445 if out.status.success() {
33711446 Ok(())
33721447 } else {
3373− Err((1, format!("fetch failed: {}", git_failure(&out))))
3374− }
3375−}
3376−
3377−/// What a git that failed said, or how it ended when it said nothing (a
3378−/// Ctrl-C stops it silently)
3379−fn git_failure(out: &std::process::Output) -> String {
3380− use std::os::unix::process::ExitStatusExt;
3381− let said = String::from_utf8_lossy(&out.stderr).trim().to_string();
3382− match (said.is_empty(), out.status.signal()) {
3383− (false, _) => said,
3384− (true, Some(sig)) => format!("git was stopped by signal {}", sig),
3385− (true, None) => format!("git exited with status {}", out.status.code().unwrap_or(-1)),
3386− }
3387−}
3388−
3389−/// the branch the remote's HEAD names, its default bookmark (§7.8), asked of
3390−/// the remote with `git ls-remote --symref` (the git `git_fetch_subprocess`
3391−/// runs); `None` when HEAD names no branch (a detached HEAD)
3392−fn git_default_branch(
3393− git_dir: &std::path::Path,
3394− remote: &RemoteName,
3395−) -> Result<Option<String>, OpenError> {
3396− let exe = std::env::var("J_GIT").unwrap_or_else(|_| "git".to_string());
3397− let out = std::process::Command::new(exe)
3398− .arg("--git-dir")
3399− .arg(git_dir)
3400− .args(["ls-remote", "--symref", remote.as_str(), "HEAD"])
3401− .stdin(std::process::Stdio::null())
3402− .output()
3403− .map_err(|e| (1, format!("cannot run git ls-remote: {}", e)))?;
3404− if !out.status.success() {
3405− return Err((1, format!("cannot read the remote's default branch: {}", git_failure(&out))));
1448+ Err((
1449+ 1,
1450+ format!(
1451+ "fetch failed: {}",
1452+ String::from_utf8_lossy(&out.stderr).trim()
1453+ ),
1454+ ))
34061455 }
3407− // `ref: refs/heads/<branch>\tHEAD`
3408− Ok(String::from_utf8_lossy(&out.stdout).lines().find_map(|line| {
3409− let branch = line.strip_prefix("ref: refs/heads/")?.strip_suffix("\tHEAD")?;
3410− Some(branch.to_string())
3411− }))
34121456}
34131457
34141458enum OpKind {
34151459 Normal,
3416− Undo,
3417− Redo,
1460+ /// the operation this undo discarded the view of
1461+ Undo { #[allow(dead_code)] undid: OperationId },
1462+ /// the undo this redo reversed
1463+ Redo { redid: OperationId },
34181464}
34191465
34201466/// a marker parsed from an operation description (§7.7 kinds)
34211467struct OpMarker {
34221468 kind: &'static str,
34231469 id: OperationId,
3424− /// jj 0.45 names the operation whose view was restored, rather than
3425− /// the operation undone or the undo reversed.
3426− restores_view: bool,
3427− /// Whether a legacy marker is j's exact `undo: <id>` / `redo: <id>`
3428− /// format, whose discarded-operation links the walk can follow.
1470+ /// whether the description is j's own exact marker (`undo: <id>` /
1471+ /// `redo: <id>`); only those are reliably loadable for the walk
34291472 authored_by_j: bool,
34301473}
34311474
3432−/// §7.7 kinds: new records use jj 0.45's restored-view descriptions. Older
3433−/// j `undo: <id>` / `redo: <id>` and jj `undo operation <id>` /
3434−/// `redo operation <id>` descriptions remain readable too.
1475+/// §7.7 kinds: `j` marks undo/redo operations as `undo: <opid>` /
1476+/// `redo: <opid>` in the description (0.45's Transaction cannot write
1477+/// metadata tags); jj's own `undo operation <id>` / `redo operation <id>`
1478+/// markers are treated the same way.
34351479fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
34361480 match op_marker(meta) {
3437− Some(OpMarker { kind: "undo", .. }) => OpKind::Undo,
3438− Some(OpMarker { kind: "redo", .. }) => OpKind::Redo,
1481+ Some(OpMarker { kind: "undo", id, .. }) => OpKind::Undo { undid: id },
1482+ Some(OpMarker { kind: "redo", id, .. }) => OpKind::Redo { redid: id },
34391483 _ => OpKind::Normal,
34401484 }
34411485}
@@ -3443,33 +1487,20 @@ fn op_kind(meta: &jj_lib::op_store::OperationMetadata) -> OpKind {
34431487fn op_marker(meta: &jj_lib::op_store::OperationMetadata) -> Option<OpMarker> {
34441488 let desc = meta.description.trim();
34451489 for kind in ["undo", "redo"] {
3446− if let Some(rest) = desc.strip_prefix(&format!("{}: restore to operation ", kind)) {
3447− if let Some(id) = parse_op_id(rest.trim()) {
3448− return Some(OpMarker { kind, id, restores_view: true, authored_by_j: false });
3449− }
3450− }
34511490 if let Some(rest) = desc.strip_prefix(&format!("{}:", kind)) {
34521491 if let Some(id) = parse_op_id(rest.trim()) {
3453− return Some(OpMarker { kind, id, restores_view: false, authored_by_j: true });
1492+ return Some(OpMarker { kind, id, authored_by_j: true });
34541493 }
34551494 }
34561495 if let Some(rest) = desc.strip_prefix(&format!("{} operation", kind)) {
34571496 if let Some(id) = parse_op_id(rest.trim()) {
3458− return Some(OpMarker { kind, id, restores_view: false, authored_by_j: false });
1497+ return Some(OpMarker { kind, id, authored_by_j: false });
34591498 }
34601499 }
34611500 }
34621501 None
34631502}
34641503
3465−/// Whether `meta` is the operation `init` or `clone` recorded (§7.7), by its
3466−/// description, `init` or `clone URL`: no expression is described so, both
3467−/// words being reserved (§1.1).
3468−fn op_created_repo(meta: &jj_lib::op_store::OperationMetadata) -> bool {
3469− let desc = meta.description.trim();
3470− desc == "init" || desc.starts_with("clone ")
3471−}
3472−
34731504/// a full-length (or prefix) hex operation id
34741505fn parse_op_id(s: &str) -> Option<OperationId> {
34751506 if s.is_empty() || !s.chars().all(|c| c.is_ascii_hexdigit()) {
@@ -3495,39 +1526,21 @@ fn op_parent_id(
34951526 Ok(op.parent_ids().first().cloned())
34961527}
34971528
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(
1529+/// follow a chain of redos to the operation that was ultimately reapplied
1530+async fn chain_through_redos(
35021531 loader: &jj_lib::repo::RepoLoader,
3503− op: &jj_lib::operation::Operation,
3504−) -> String {
3505− let mut represented = op.clone();
3506− let mut seen = BTreeSet::new();
3507− while seen.insert(represented.id().clone()) {
3508− let Some(marker) = op_marker(represented.metadata()) else { break };
3509− let target = if marker.restores_view {
3510− Some(marker.id)
3511− } else if marker.authored_by_j {
3512− // A legacy undo names what it discarded; a legacy redo names
3513− // the undo it reversed. Either restored its target's parent.
3514− op_parent_id(loader, &marker.id).ok().flatten()
3515− } else {
3516− // Unknown legacy jj history is best identified by its actual
3517− // description instead of guessing which operation it applied.
3518− None
3519− };
3520− let Some(target) = target else { break };
3521− // Missing historical metadata must not prevent an otherwise valid
3522− // recovery operation merely because its summary cannot be expanded.
3523− let Ok(target_op) = block_on(loader.load_operation(&target)) else { break };
3524− represented = target_op;
3525− }
3526− let description = represented.metadata().description.lines().next().unwrap_or("").trim();
3527− if description.is_empty() {
3528− format!("operation {}", truncate_chars(&represented.id().hex(), 12))
3529− } else {
3530− terminal_text(description)
1532+ first: &OperationId,
1533+) -> Result<OperationId, OpenError> {
1534+ let mut id = first.clone();
1535+ loop {
1536+ let op = loader
1537+ .load_operation(&id)
1538+ .await
1539+ .map_err(|e| (2, format!("cannot load an operation: {}", e)))?;
1540+ match op_kind(op.metadata()) {
1541+ OpKind::Redo { redid } => id = redid,
1542+ _ => return Ok(id),
1543+ }
35311544 }
35321545}
35331546
@@ -3597,11 +1610,6 @@ fn parse_push_records(
35971610 let rec = vis.commits.get(&change_id).ok_or_else(|| {
35981611 (1, format!("push: `@{}` is not a visible commit", change_id))
35991612 })?;
3600− // jj's root has no Git object: its all-zero commit id is
3601− // interpreted by `git push` as a deletion, not a target.
3602− if rec.commit.id() == repo.store().root_commit_id() {
3603− return Err((1, "push: the root commit cannot be pushed".to_string()));
3604− }
36051613 out.push((name, Some(rec.commit.id().clone())));
36061614 }
36071615 None => {
@@ -3621,190 +1629,68 @@ fn parse_push_records(
36211629 Ok(out)
36221630}
36231631
3624−/// §7.6: refuse to move a bookmark whose current target is immutable to a
3625−/// commit that does not descend from that target, or to delete it while no
3626−/// bookmark left on `origin` after the push reaches that target; and refuse
3627−/// to send commits with unresolved files or empty descriptions. Returns
3628−/// whether such a delete is allowed only because a record's new target
3629−/// reaches it, so the deletes must wait for the updates (push_to_origin),
3630−/// and refuses a push whose deletes would wait for a bookmark git cannot
3631−/// create while one of them is still there
1632+/// §7.6: refuse to send commits with unresolved files or empty descriptions,
1633+/// and refuse to move/delete a bookmark whose current target is immutable
1634+/// away from a descendant of that target
36321635fn check_push_records(
36331636 records: &[(String, Option<CommitId>)],
3634− repo: &Arc<ReadonlyRepo>,
36351637 vis: &VisibleRepo,
3636− immutable: &BTreeSet<String>,
3637−) -> Result<bool, OpenError> {
1638+ repo: &Arc<ReadonlyRepo>,
1639+) -> Result<(), OpenError> {
36381640 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();
36481641 for (name, target) in records {
3649− let Some(current) = repo
1642+ let mut seeds: Vec<String> = Vec::new();
1643+ if let Some(commit_id) = target {
1644+ let commit = block_on(store.get_commit_async(commit_id))
1645+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1646+ let change_id = commit.change_id().reverse_hex();
1647+ if vis.commits.contains_key(&change_id) {
1648+ seeds.push(change_id);
1649+ }
1650+ }
1651+ if let Some(current) = repo
36501652 .view()
3651− .get_remote_bookmark(RefName::new(name).to_remote_symbol(origin))
1653+ .get_remote_bookmark(RefName::new(name).to_remote_symbol(RemoteName::new("origin")))
36521654 .target
36531655 .as_resolved()
36541656 .cloned()
36551657 .unwrap_or(None)
3656− else {
3657− continue;
3658− };
3659− let change_id = block_on(store.get_commit_async(¤t))
3660− .map_err(|e| (2, format!("cannot read a commit: {}", e)))?
3661− .change_id()
3662− .reverse_hex();
3663− // the immutable set holds visible commits: a target rewritten here
3664− // since the last fetch or push is hidden, and not in it although the
3665− // visible rewrite carries its change id (and its label)
3666− let visible = vis.commits.get(&change_id).is_some_and(|c| c.commit.id() == ¤t);
3667− if !visible || !immutable.contains(&change_id) {
3668− continue;
3669− }
3670− match target {
3671− None => {
3672− // a delete loses no history while another bookmark still
3673− // reaches the commit: a merged feature, a second name. When
3674− // only a bookmark this push sets reaches it (`rename`), the
3675− // remote may reject that update and accept the delete, so
3676− // the delete waits for the update to be accepted
3677− let unnamed = unnamed.get_or_insert_with(|| {
3678− let named: BTreeSet<&str> = records.iter().map(|(n, _)| n.as_str()).collect();
3679− repo.view()
3680− .remote_bookmarks(origin)
3681− .filter(|(n, _)| !named.contains(n.as_str()))
3682− .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3683− .cloned()
3684− .collect()
3685− });
3686− let reached = |heads: &[CommitId]| {
3687− ResolvedRevsetExpression::commits(vec![current.clone()])
3688− .intersection(&ResolvedRevsetExpression::commits(heads.to_vec()).ancestors())
3689− .evaluate(repo.as_ref())
3690− .and_then(|r| r.is_empty())
3691− .map(|empty| !empty)
3692− .map_err(|e| (2, format!("cannot read the index: {}", e)))
3693− };
3694− if !reached(unnamed)? {
3695− if !reached(&new_targets)? {
3696− return Err((
3697− 1,
3698− format!(
3699− "push: `{}` is on the immutable commit `@{}` and cannot be deleted while no bookmark left on origin reaches it",
3700− name, change_id
3701− ),
3702− ));
3703− }
3704− waiting.push(name);
3705− }
3706− }
3707− Some(new) => {
3708− let descends = block_on(repo.index().is_ancestor(¤t, new))
3709− .map_err(|e| (2, format!("cannot read the index: {}", e)))?;
3710− if !descends {
3711− return Err((
3712− 1,
3713− format!(
3714− "push: `{}` is on the immutable commit `@{}` and can only move to a descendant of it",
3715− name, change_id
3716− ),
3717− ));
3718− }
3719− }
3720− }
3721− }
3722− // git cannot hold a bookmark and one under its name at once (`master`
3723− // and `master/legacy`), so while the deletes wait, a first push that
3724− // creates a name nesting with a deleted one is rejected and the deletes
3725− // are never sent. With a bookmark no record names on the waiting
3726− // delete's commit, nothing waits and it is one git push (§7.6)
3727− let deletes_wait = !waiting.is_empty();
3728− if deletes_wait {
3729− let nests = |a: &str, b: &str| b.strip_prefix(a).is_some_and(|rest| rest.starts_with('/'));
3730− for (deleted, _) in records.iter().filter(|(_, t)| t.is_none()) {
3731− for (set, _) in records.iter().filter(|(_, t)| t.is_some()) {
3732− if nests(deleted, set) || nests(set, deleted) {
3733− let keep = if waiting.contains(&deleted.as_str()) { deleted.as_str() } else { waiting[0] };
3734− let literal = crate::show::text_literal(keep);
3735− // the temporary label: the first of `tmp`, `tmp-1`, ...
3736− // that is no bookmark, here or on origin, no record
3737− // names, and nests with none of those, so pushing and
3738− // deleting it moves none of the user's bookmarks, and
3739− // this push, which names nothing nesting with it, no
3740− // longer waits on `keep`'s commit
3741− let taken: Vec<&str> = repo
3742− .view()
3743− .bookmarks()
3744− .map(|(n, _)| n.as_str())
3745− .chain(records.iter().map(|(n, _)| n.as_str()))
3746− .collect();
3747− let tmp = std::iter::once("tmp".to_string())
3748− .chain((1..).map(|i| format!("tmp-{i}")))
3749− .find(|t| !taken.iter().any(|n| n == t || nests(n, t) || nests(t, n)))
3750− .expect("finitely many names are taken");
3751− let tmp = crate::show::text_literal(&tmp);
1658+ {
1659+ let commit = block_on(store.get_commit_async(¤t))
1660+ .map_err(|e| (2, format!("cannot read a commit: {}", e)))?;
1661+ let change_id = commit.change_id().reverse_hex();
1662+ if vis.commits.contains_key(&change_id) {
1663+ seeds.push(change_id);
1664+ }
1665+ }
1666+ let ancestors = ancestors_of_change_ids(vis, &seeds);
1667+ for change_id in &ancestors {
1668+ let rec = vis.commits.get(change_id).expect("ancestors are stored");
1669+ for (_path, value) in rec.commit.tree().entries() {
1670+ let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
1671+ if !value.is_resolved() {
37521672 return Err((
37531673 1,
37541674 format!(
3755− "push: `{deleted}` must stay on origin until `{set}` is accepted, and git cannot create `{set}` while `{deleted}` is there; first push a temporary label on `{keep}`'s commit, `push (label {tmp} (labelled {literal}))`, then this push, then `push (unlabel {tmp})`"
1675+ "push: commit `@{}` has unresolved files",
1676+ rec.change_id
37561677 ),
37571678 ));
37581679 }
37591680 }
3760− }
3761− }
3762− // the commits that would be sent: ancestors of the new targets that no
3763− // bookmark on the remote already reaches; a delete sends none
3764− if new_targets.is_empty() {
3765− return Ok(deletes_wait);
3766− }
3767− let on_remote: Vec<CommitId> = repo
3768− .view()
3769− .remote_bookmarks(origin)
3770− .flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
3771− .cloned()
3772− .collect();
3773− let sent = ResolvedRevsetExpression::commits(on_remote)
3774− .range(&ResolvedRevsetExpression::commits(new_targets))
3775− .evaluate(repo.as_ref())
3776− .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3777− let sent: Vec<Commit> = block_on(sent.stream().commits(&store).try_collect())
3778− .map_err(|e| (2, format!("cannot list the commits to send: {}", e)))?;
3779− for commit in &sent {
3780− let change_id = commit.change_id().reverse_hex();
3781− let mut unresolved_paths = Vec::new();
3782− for (path, value) in commit.tree().entries() {
3783− let value = value.map_err(|e| (2, format!("cannot read a tree: {}", e)))?;
3784− if !value.is_resolved() {
3785− unresolved_paths.push(path.as_internal_file_string().to_string());
3786− }
3787− }
3788− if !unresolved_paths.is_empty() {
3789− let mut paths = unresolved_paths.iter().take(5)
3790− .map(|path| format!("`{}`", terminal_text(path)))
3791− .collect::<Vec<_>>().join(", ");
3792− if unresolved_paths.len() > 5 {
3793− paths.push_str(&format!(" and {} more", unresolved_paths.len() - 5));
1681+ if rec.commit.description().trim().is_empty() && rec.commit.id() != store.root_commit_id() {
1682+ return Err((
1683+ 1,
1684+ format!("push: commit `@{}` has an empty description", rec.change_id),
1685+ ));
37941686 }
3795− return Err((1, format!(
3796− "push: commit `@{change_id}` has unresolved files: {paths}; run `j conflicts` to inspect conflicts, then `j by @{change_id}` to edit this commit. Resolve the listed files, run `j id` to record the resolution, and check `j conflicts` before pushing again"
3797− )));
3798− }
3799− if commit.description().trim().is_empty() && commit.id() != store.root_commit_id() {
3800− return Err((1, format!("push: commit `@{}` has an empty description", change_id)));
38011687 }
38021688 }
3803− Ok(deletes_wait)
1689+ Ok(())
38041690}
38051691
38061692/// change-id closure of jj-parents from the seeds (all jj parents, not just
3807−/// the first), for the immutable set (§7.5)
1693+/// the first), for the push content checks
38081694fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<String> {
38091695 let mut seen = BTreeSet::new();
38101696 let mut stack: Vec<String> = seeds.to_vec();
@@ -3825,11 +1711,23 @@ fn ancestors_of_change_ids(vis: &VisibleRepo, seeds: &[String]) -> BTreeSet<Stri
38251711
38261712/// git-check-ref-format --branch rules
38271713fn is_valid_git_branch_name(name: &str) -> bool {
3828− if name.starts_with('-') {
1714+ if name.is_empty()
1715+ || name.starts_with('-')
1716+ || name.starts_with('.')
1717+ || name.ends_with('.')
1718+ || name.ends_with('/')
1719+ || name.ends_with(".lock")
1720+ || name.contains("..")
1721+ || name.contains("//")
1722+ || name.contains("@{")
1723+ || name == "@"
1724+ {
38291725 return false;
38301726 }
3831− let full_name = format!("refs/heads/{name}");
3832− gix::validate::reference::branch_name(full_name.as_str().into()).is_ok()
1727+ name.chars().all(|c| {
1728+ !(c.is_ascii_control()
1729+ || matches!(c, ' ' | '~' | '^' | ':' | '?' | '*' | '[' | '\\'))
1730+ })
38331731}
38341732
38351733// ----------------------------------------------------------------------
@@ -3837,12 +1735,6 @@ fn is_valid_git_branch_name(name: &str) -> bool {
38371735// ----------------------------------------------------------------------
38381736
38391737impl Backend for JjBackend {
3840− fn validate_authoring(&self) -> Result<(), Crash> {
3841− let repo = self.current_repo();
3842− check_identity(repo.settings().user_name(), repo.settings().user_email())
3843− .map_err(|(_, message)| Crash::new(message))
3844− }
3845−
38461738 fn visible_ids(&self) -> Vec<String> {
38471739 match self.inner.visible.lock().unwrap().as_ref() {
38481740 Some(v) => v.commits.keys().cloned().collect(),
@@ -3877,93 +1769,38 @@ impl Backend for JjBackend {
38771769 vis.and_then(|v| v.commits.get(id).map(|r| r.commit.has_conflict()))
38781770 }
38791771
3880− fn is_empty(&self, id: &str, parent: &str) -> Option<bool> {
1772+ fn is_empty(&self, id: &str) -> Option<bool> {
38811773 let vis = self.inner.visible.lock().unwrap().clone();
3882− // O(1): empty iff the tree id equals the parent's tree id
1774+ // O(1): empty iff the tree id equals the first parent's tree id
38831775 vis.and_then(|v| {
38841776 let rec = v.commits.get(id)?;
3885− let parent = v.commits.get(parent)?;
1777+ let parent = v.commits.get(&rec.first_parent)?;
38861778 Some(rec.commit.tree_ids() == parent.commit.tree_ids())
38871779 })
38881780 }
38891781
3890− fn folds_case(&self) -> bool {
3891− // jj's checkout refuses a name whose file identity is that of
3892− // `.git` or `.jj` in the same directory (local_working_copy.rs), so
3893− // where `.JJ` is the workspace's own `.jj` it refuses `.GIT` too.
3894− // Where each spelling of a name gets its own inode number
3895− // (exfat-fuse) the identities differ and the checkout writes
3896− // `.GIT/x` into `.git`, so folding is told by how names resolve:
3897− // `.JJ` resolves, but the directory does not list it under that
3898− // spelling. A directory that cannot be listed is taken to fold
3899− // case, the answer that refuses more.
3900− *self.inner.folds_case.get_or_init(|| {
3901− use jj_lib::file_util::FileIdentity;
3902− let root = &self.inner.workspace_root;
3903− let Ok(upper) = FileIdentity::from_symlink_path(root.join(".JJ")) else {
3904− return false;
3905− };
3906− FileIdentity::from_symlink_path(root.join(".jj")).is_ok_and(|lower| lower == upper)
3907− || std::fs::read_dir(root).map_or(true, |mut entries| {
3908− !entries.any(|e| e.is_ok_and(|e| e.file_name() == ".JJ"))
3909− })
3910− })
3911− }
3912−
3913− fn name_fits(&self, name: &str) -> bool {
3914− if name.len() <= crate::repo::NAME_MAX {
3915− return true;
3916− }
3917− // a component, never a path: one with `/` would be created elsewhere
3918− if name.contains(['/', '\0']) {
3919− return false;
3920− }
3921− // no call tells whether the limit counts bytes (ext4) or UTF-16
3922− // units or characters (exFAT, NTFS, APFS), so the name is tried:
3923− // created in a directory of its own under `.jj`, which is then
3924− // removed, once per name and run
3925− let mut known = self.inner.long_names.lock().unwrap();
3926− if let Some(fits) = known.get(name) {
3927− return *fits;
3928− }
3929− let fits = match tempfile::Builder::new()
3930− .prefix("name-")
3931− .tempdir_in(self.inner.workspace_root.join(".jj"))
3932− {
3933− Ok(dir) => {
3934− let fits = std::fs::File::create(dir.path().join(name)).is_ok();
3935− drop(dir);
3936− fits
3937− }
3938− Err(_) => false,
3939− };
3940− known.insert(name.to_string(), fits);
3941− fits
3942− }
3943−
39441782 fn ancestors_closed(&self, ids: &BTreeSet<String>) -> BTreeSet<String> {
39451783 let vis = self.inner.visible.lock().unwrap().clone();
39461784 let Some(vis) = vis else {
39471785 return BTreeSet::new();
39481786 };
3949− // every jj parent, not just the first: rewriting a commit reached
3950− // only through a merge's second parent rewrites the merge too
3951− // (§7.5 step 3)
3952− let seeds: Vec<String> = ids.iter().cloned().collect();
3953− ancestors_of_change_ids(&vis, &seeds)
1787+ let mut seen = BTreeSet::new();
1788+ let mut stack: Vec<String> = ids.iter().cloned().collect();
1789+ while let Some(id) = stack.pop() {
1790+ if seen.insert(id.clone()) {
1791+ if let Some(rec) = vis.commits.get(&id) {
1792+ if !rec.first_parent.is_empty() {
1793+ stack.push(rec.first_parent.clone());
1794+ }
1795+ }
1796+ }
1797+ }
1798+ seen
39541799 }
39551800
39561801 fn replay(&self, onto: &[Value], from: &[Value], to: &[Value]) -> Result<Vec<Value>, Crash> {
3957− self.replay_snapshot(
3958− &Value::list(onto.to_vec()),
3959− &Value::list(from.to_vec()),
3960− &Value::list(to.to_vec()),
3961− )?.as_list().map(<[Value]>::to_vec)
3962− }
3963−
3964− fn replay_snapshot(&self, onto: &Value, from: &Value, to: &Value) -> Result<Value, Crash> {
39651802 let store = self.current_repo().store().clone();
3966− block_on(jj_replay(&store, &self.replay_cache, onto, from, to))
1803+ block_on(jj_replay(&store, onto, from, to))
39671804 }
39681805}
39691806
@@ -3971,197 +1808,30 @@ impl Backend for JjBackend {
39711808// jj three-way tree merge for `replay` (§7.3)
39721809// ----------------------------------------------------------------------
39731810
3974−/// Replay sees the same lists repeatedly: a parent's old and new files
3975−/// become each child's bases. Retain their validated trees by list identity,
3976−/// including its offset, without keeping discarded snapshots alive.
3977−#[derive(Default)]
3978−struct ReplayCache {
3979− trees: RefCell<HashMap<(*const Vec<Value>, usize), ReplayTree>>,
3980− empty_subtrees: RefCell<HashMap<TreeId, bool>>,
3981− next_sweep: Cell<usize>,
3982− blobs: BlobCache,
3983− entries: EntryCache,
3984−}
3985−
3986−struct ReplayTree {
3987− storage: Weak<Vec<Value>>,
3988− tree: MergedTree,
3989− /// A list read from a resolved tree, already in canonical path order.
3990− canonical: bool,
3991−}
3992−
3993−impl ReplayCache {
3994− fn remember(&self, list: &ListVal, tree: MergedTree, canonical: bool) {
3995− let mut trees = self.trees.borrow_mut();
3996− trees.insert(list.identity(), ReplayTree {
3997− storage: list.weak_storage(),
3998− tree,
3999− canonical,
4000− });
4001− if trees.len() >= self.next_sweep.get().max(256) {
4002− trees.retain(|_, t| t.storage.strong_count() != 0);
4003− self.next_sweep.set((trees.len() * 2).max(256));
4004− }
4005− }
4006−
4007− async fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<(MergedTree, bool), Crash> {
4008− let Value::List(list) = files else {
4009− files.as_list()?;
4010− unreachable!();
4011− };
4012− if let Some(cached) = self.trees.borrow().get(&list.identity()) {
4013− // The weak reference keeps its allocation reserved, so another
4014− // list cannot reuse this address while the key is cached.
4015− if cached.storage.strong_count() != 0 {
4016− return Ok((cached.tree.clone(), cached.canonical));
4017− }
4018− }
4019− // Validate every new list before caching it, even when from == to:
4020− // an unchanged change must still reject malformed snapshots.
4021− let tree = build_tree(store, files).await?;
4022− self.remember(list, tree.clone(), false);
4023− Ok((tree, false))
4024− }
4025−
4026− async fn files_of(&self, store: &Arc<Store>, tree: &MergedTree) -> Result<Value, Crash> {
4027− let (files, reusable) = tree_to_files_inner(store, tree, &self.blobs, &self.entries).await?;
4028− let list = ListVal::new(files);
4029− if reusable && !self.has_empty_subtrees(tree).await? {
4030− self.remember(&list, tree.clone(), true);
4031− }
4032− Ok(Value::List(list))
4033− }
4034−
4035− /// Flattening a Git tree omits empty directories. Reusing such a tree
4036− /// would let a replay conflict with a directory absent from its input
4037− /// snapshot. Inspect directories once per tree, without rebuilding files.
4038− async fn has_empty_subtrees(&self, tree: &MergedTree) -> Result<bool, Crash> {
4039− let Some(root) = tree.tree_ids().as_resolved() else { return Ok(true) };
4040− if let Some(found) = self.empty_subtrees.borrow().get(root) {
4041− return Ok(*found);
4042− }
4043− let store = tree.store();
4044− let mut pending = vec![(RepoPathBuf::root(), root.clone())];
4045− let mut visited = BTreeSet::new();
4046− while let Some((dir, id)) = pending.pop() {
4047− if !visited.insert(id.clone()) {
4048− continue;
4049− }
4050− let known = self.empty_subtrees.borrow().get(&id).copied();
4051− if let Some(found) = known {
4052− if found {
4053− self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4054− return Ok(true);
4055− }
4056− continue;
4057− }
4058− let loaded = store.get_tree(dir.clone(), &id).await
4059− .map_err(|e| Crash::new(format!("cannot read a tree: {}", e)))?;
4060− for entry in loaded.entries_non_recursive() {
4061− if let TreeValue::Tree(child) = entry.value() {
4062− if child == store.empty_tree_id() {
4063− self.empty_subtrees.borrow_mut().insert(root.clone(), true);
4064− return Ok(true);
4065− }
4066− pending.push((dir.join(entry.name()), child.clone()));
4067− }
4068− }
4069− }
4070− // Every subtree visited was nonempty all the way down. Sharing this
4071− // answer also avoids rescanning unchanged directories of later trees.
4072− self.empty_subtrees.borrow_mut().extend(visited.into_iter().map(|id| (id, false)));
4073− Ok(false)
4074− }
4075−}
4076−
40771811async fn jj_replay(
40781812 store: &Arc<Store>,
4079− cache: &ReplayCache,
4080− onto: &Value,
4081− from: &Value,
4082− to: &Value,
4083−) -> Result<Value, Crash> {
4084− let (onto_tree, onto_canonical) = cache.tree_of(store, onto).await?;
4085− let (from_tree, from_canonical) = cache.tree_of(store, from).await?;
4086− let (to_tree, to_canonical) = cache.tree_of(store, to).await?;
4087− // any of the three may itself be conflicted (a snapshot with unresolved
4088− // blobs, §7.3), so merge the trees the way jj's rebase does:
4089− // MergedTree::merge flattens conflicted inputs into one merge, simplifies
4090− // it, and resolves file contents line-level with tree_merge::merge_trees,
4091− // keeping unresolvable paths as conflicts. Empty labels leave the result
4092− // unlabeled; the labels would not survive tree_to_files anyway.
4093− let merged = MergedTree::merge(Merge::from_removes_adds(
4094− vec![(from_tree.clone(), String::new())],
4095− vec![(onto_tree.clone(), String::new()), (to_tree.clone(), String::new())],
4096− ))
4097− .await
4098− .map_err(|e| Crash::new(format!("replay: {}", e)))?;
4099− refuse_file_directory_clash(&merged, &onto_tree, &from_tree, &to_tree).await?;
4100− // Reusing a resolved tree's list preserves replay's sorted output. A
4101− // constructed list may have another order, and conflicts may simplify
4102− // on reading, so neither can take this shortcut.
4103− for (files, tree, canonical) in [
4104− (onto, &onto_tree, onto_canonical),
4105− (to, &to_tree, to_canonical),
4106− (from, &from_tree, from_canonical),
4107− ] {
4108− if canonical && merged.tree_ids() == tree.tree_ids() {
4109− return Ok(files.clone());
4110− }
4111− }
1813+ onto: &[Value],
1814+ from: &[Value],
1815+ to: &[Value],
1816+) -> Result<Vec<Value>, Crash> {
1817+ let onto_tree = build_tree(store, &Value::list(onto.to_vec())).await?;
1818+ let from_tree = build_tree(store, &Value::list(from.to_vec())).await?;
1819+ let to_tree = build_tree(store, &Value::list(to.to_vec())).await?;
1820+ let merge = Merge::from_removes_adds(
1821+ vec![from_tree.tree_ids().as_resolved().cloned().unwrap()],
1822+ vec![
1823+ onto_tree.tree_ids().as_resolved().cloned().unwrap(),
1824+ to_tree.tree_ids().as_resolved().cloned().unwrap(),
1825+ ],
1826+ );
1827+ // tree_merge::merge_trees resolves file contents line-level and keeps
1828+ // unresolvable paths as conflicts (tree_merge.rs:78)
1829+ let merged = jj_lib::tree_merge::merge_trees(store, merge)
1830+ .await
1831+ .map_err(|e| Crash::new(format!("replay: {}", e)))?;
41121832 // conflicts stay as unresolved blobs (§7.3)
4113− cache.files_of(store, &merged).await
4114−}
4115−
4116−/// Crash if the merge left a conflict with a file on one side and, on
4117−/// another, a directory `onto` or `to` holds at that path: one entry cannot
4118−/// list that directory's entries (§7.3). A conflict that `onto`, `from` or
4119−/// `to` already holds at the path is not made here: its sides, a directory
4120−/// side jj made earlier included, came in as an unresolved blob, and replay
4121−/// carries them beside the others, as when the child resolving that conflict
4122−/// is squashed into it.
4123−async fn refuse_file_directory_clash(
4124− merged: &MergedTree,
4125− onto: &MergedTree,
4126− from: &MergedTree,
4127− to: &MergedTree,
4128−) -> Result<(), Crash> {
4129− let value_at = async |tree: &MergedTree, path: &RepoPath| {
4130− tree.path_value(path).await.map_err(|e| Crash::new(format!("replay: {}", e)))
4131− };
4132− // walks only the unresolved part of the tree: nothing when it is resolved
4133− for (path, value) in merged.conflicts() {
4134− let value = value.map_err(|e| Crash::new(format!("replay: {}", e)))?;
4135− // sides that cancel are no sides
4136− let value = value.simplify();
4137− let has = |dir: bool| {
4138− value
4139− .adds()
4140− .any(|v| matches!(v, Some(t) if matches!(t, TreeValue::Tree(_)) == dir))
4141− };
4142− if !has(false) || !has(true) {
4143− continue;
4144− }
4145− let held = [
4146− value_at(onto, &path).await?,
4147− value_at(from, &path).await?,
4148− value_at(to, &path).await?,
4149− ];
4150− // an input's conflict at the path, as opposed to one inside the
4151− // directory there, which leaves the path a directory in the input
4152− if held.iter().any(|h| !h.is_resolved() && !h.is_tree()) {
4153− continue;
4154− }
4155− let side = |dir: &MergedTreeValueT| {
4156− dir.is_tree() && value.adds().any(|v| v.is_some() && dir.iter().any(|h| h == v))
4157− };
4158− if side(&held[0]) || side(&held[2]) {
4159− let comps: Vec<String> =
4160− path.components().map(|c| c.as_internal_str().to_string()).collect();
4161− return Err(crate::domain::file_directory_clash(&comps));
4162− }
4163− }
4164− Ok(())
1833+ let merged = MergedTree::new(store.clone(), merged, jj_lib::conflict_labels::ConflictLabels::unlabeled());
1834+ tree_to_files(store, &merged, &BlobCache::default(), &EntryCache::default()).await
41651835}
41661836
41671837// ----------------------------------------------------------------------
@@ -4182,107 +1852,6 @@ impl ConflictTreeValue {
41821852 }
41831853}
41841854
4185−/// The stored trees a program's commits may still hold the files of, to
4186−/// write each such commit with the tree itself (§7.5 step 4). A list holds
4187−/// each conflict simplified (`merged_value_to_blob`), and the tree it was
4188−/// loaded from need not, so a tree built from the list could differ from
4189−/// the stored one where the files do not: a child `new` made would not be
4190−/// empty, as jj's own `new` leaves one.
4191−struct LoadedTrees<'a> {
4192− /// the stored commits the program's repo was loaded from
4193− visible: Option<&'a VisibleRepo>,
4194− /// the lists the lazy `files` of the program's commits have loaded, by
4195− /// identity: `c.files` takes the list out, so a commit given it, as
4196− /// `new` gives its child its parent's, holds the list itself
4197− lists: HashMap<(*const Vec<Value>, usize), Rc<ThunkVal>>,
4198−}
4199−
4200−impl<'a> LoadedTrees<'a> {
4201− /// The trees `repos`' commits were loaded with; nothing is read
4202− fn new(visible: Option<&'a VisibleRepo>, repos: &[&Value]) -> Result<LoadedTrees<'a>, Crash> {
4203− let mut lists = HashMap::new();
4204− for repo in repos {
4205− for commit in crate::repo::all_commits(repo)? {
4206− if let Value::Thunk(t) = crate::repo::files_of(&commit)? {
4207− if let (Some(_), Some(Value::List(list))) = (t.tree(), t.peek()) {
4208− lists.insert(list.identity(), t);
4209− }
4210− }
4211− }
4212− }
4213− Ok(LoadedTrees { visible, lists })
4214− }
4215−
4216− /// The tree to write for a commit on `parents` whose files are `files`:
4217− /// where they are still a list loaded from a stored tree, that tree as
4218− /// jj stored it, and otherwise the one `build_tree` makes of them; but
4219− /// where either lists the parents' files, the parents' tree
4220− fn tree_to_write(&self, repo: &dyn Repo, files: &Value, parents: &[CommitId]) -> Result<MergedTree, Crash> {
4221− let tree = self.tree_of(repo.store(), files)?;
4222− Ok(block_on(parents_tree_if_same(repo, parents, tree)))
4223− }
4224−
4225− fn tree_of(&self, store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
4226− let loaded = match files {
4227− Value::Thunk(t) => Some(t),
4228− Value::List(list) => self.lists.get(&list.identity()),
4229− _ => None,
4230− };
4231− if let Some(t) = loaded {
4232− let stored = t.origin().zip(self.visible).and_then(|(id, vis)| vis.commits.get(id));
4233− if let Some(tree) = stored.map(|rec| rec.commit.tree()) {
4234− if t.tree() == Some(tree_name(&tree).as_str()) {
4235− return Ok(tree);
4236− }
4237− }
4238− }
4239− block_on(build_tree(store, &files.forced()?))
4240− }
4241−}
4242−
4243−/// The tree of `parents`, merged where there are several, as jj's emptiness
4244−/// takes it, where it lists the files `tree` lists, and otherwise `tree`
4245−/// (§7.5 step 4). A list holds each conflict less the sides that cancel
4246−/// (§7.3), and a tree built from it holds no labels, where a tree jj merged
4247−/// keeps both; so a commit left with its parent's files by `squash`,
4248−/// `rebase` or `abandon`, which build the files anew, got another tree for
4249−/// them: jj took it for a change at each conflicted path, the tree's empty
4250−/// mark (`is_empty`) did not show, and the checkout wrote the conflicts
4251−/// again with generic labels. So did a commit moved onto another tree for
4252−/// its files, which kept its own, and a snapshot that reverted the commit's
4253−/// change, which builds on the commit's tree (§7.4). Files with no conflict
4254−/// make one tree, so a resolved parents' tree is not compared, nor one of
4255−/// the same ids, and the comparison stops at the first path where the two
4256−/// differ. It only chooses between two trees for the same files, so where
4257−/// the parents' cannot be read, it is `tree`.
4258−async fn parents_tree_if_same(repo: &dyn Repo, parents: &[CommitId], tree: MergedTree) -> MergedTree {
4259− let mut commits = Vec::with_capacity(parents.len());
4260− for id in parents {
4261− match repo.store().get_commit_async(id).await {
4262− Ok(commit) => commits.push(commit),
4263− Err(_) => return tree,
4264− }
4265− }
4266− let Ok(parents_tree) = jj_lib::rewrite::merge_commit_trees(repo, &commits).await else {
4267− return tree;
4268− };
4269− // the same terms: the parents' tree, with jj's labels for them
4270− if parents_tree.tree_ids() == tree.tree_ids() {
4271− return parents_tree;
4272− }
4273− if parents_tree.tree_ids().is_resolved() {
4274− return tree;
4275− }
4276− let mut diff = parents_tree.diff_stream(&tree, &EverythingMatcher);
4277− while let Some(entry) = diff.next().await {
4278− match entry.values {
4279− Ok(Diff { before, after }) if value_as_read(&before) == value_as_read(&after) => {}
4280− _ => return tree,
4281− }
4282− }
4283− parents_tree
4284−}
4285−
42861855async fn build_tree(store: &Arc<Store>, files: &Value) -> Result<MergedTree, Crash> {
42871856 let entries = files
42881857 .as_list()
@@ -4342,41 +1911,19 @@ async fn blob_to_tree_value(store: &Arc<Store>, content: &Value) -> Result<Merge
43421911 return Err(Crash::new("persistence: malformed conflict blob"));
43431912 }
43441913 // jj order: [add, (remove, add)*] → Merge with interleaved adds
4345− // and removes; each side keeps its absence, its own file type or
4346− // its directory (§7.3), so the tree written is the one the
4347− // conflict was read from
1914+ // and removes
43481915 let mut values: Vec<Option<TreeValue>> = Vec::new();
43491916 for side in sides {
4350− let Some(side) = side else {
4351− values.push(None);
4352− continue;
4353− };
4354− let value = if let Some(hex) = &side.tree {
4355− TreeValue::Tree(
4356− TreeId::try_from_hex(hex)
4357− .ok_or_else(|| Crash::new("persistence: bad tree id"))?,
4358− )
4359− } else if side.kind == BlobKind::Symlink {
4360− let target = std::str::from_utf8(&side.bytes)
4361− .map_err(|_| Crash::new("persistence: symlink target is not UTF-8"))?;
4362− let id = store
4363− .write_symlink(&RepoPathBuf::root(), target)
4364− .await
4365− .map_err(|e| Crash::new(format!("cannot write a symlink: {}", e)))?;
4366− TreeValue::Symlink(id)
4367− } else {
4368− let mut slice: &[u8] = side.bytes.as_ref();
4369− let id = store
4370− .write_file(&RepoPathBuf::root(), &mut slice)
4371− .await
4372− .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
4373− TreeValue::File {
4374− id,
4375− executable: side.kind == BlobKind::Executable,
4376− copy_id: CopyId::placeholder(),
4377− }
4378− };
4379− values.push(Some(value));
1917+ let mut slice: &[u8] = side.as_ref();
1918+ let id = store
1919+ .write_file(&RepoPathBuf::root(), &mut slice)
1920+ .await
1921+ .map_err(|e| Crash::new(format!("cannot write a file: {}", e)))?;
1922+ values.push(Some(TreeValue::File {
1923+ id,
1924+ executable: b.kind == BlobKind::Executable,
1925+ copy_id: CopyId::placeholder(),
1926+ }));
43801927 }
43811928 let mut adds = Vec::new();
43821929 let mut removes = Vec::new();
@@ -4406,49 +1953,34 @@ fn to_repo_path(path: &[String]) -> Result<RepoPathBuf, Crash> {
44061953// jj trees -> values
44071954// ----------------------------------------------------------------------
44081955
4409−/// Blob contents keyed by file id, shared while snapshots hold their bytes.
4410−/// Weak entries let discarded replay results release their conflict sides.
1956+/// blob contents keyed by file id, shared across the whole repo load: most
1957+/// files are unchanged between commits, so inflating each blob once (instead
1958+/// of once per commit that references it) keeps loading linear in the
1959+/// repository's unique data rather than commits × files
44111960#[derive(Clone, Default)]
4412−struct BlobCache {
4413− bytes: Rc<RefCell<HashMap<FileId, Weak<Vec<u8>>>>>,
4414− next_sweep: Rc<Cell<usize>>,
4415−}
1961+struct BlobCache(Rc<RefCell<HashMap<FileId, Rc<Vec<u8>>>>>);
44161962
44171963impl BlobCache {
44181964 fn get(&self, id: &FileId) -> Option<Rc<Vec<u8>>> {
4419− self.bytes.borrow().get(id).and_then(Weak::upgrade)
1965+ self.0.borrow().get(id).cloned()
44201966 }
44211967 fn insert(&self, id: FileId, bytes: Rc<Vec<u8>>) {
4422− let mut cached = self.bytes.borrow_mut();
4423− cached.insert(id, Rc::downgrade(&bytes));
4424− if cached.len() >= self.next_sweep.get().max(256) {
4425− cached.retain(|_, bytes| bytes.strong_count() != 0);
4426− self.next_sweep.set((cached.len() * 2).max(256));
4427− }
1968+ self.0.borrow_mut().insert(id, bytes);
44281969 }
44291970}
44301971
4431−/// Whole file entries keyed by (path, blob identity), shared by loaded
4432−/// snapshots and replays. Weak references let transient replay results go
4433−/// away while unchanged entries still held by a snapshot are reused.
1972+/// whole file *entries* ({path, content}) keyed by (path, blob identity),
1973+/// shared across the whole repo load: an unchanged file is the same Value in
1974+/// every commit, so it is built once and cloned (an Rc bump) thereafter
44341975#[derive(Clone, Default)]
4435−struct EntryCache {
4436− entries: Rc<RefCell<HashMap<String, Weak<crate::value::RecordMap>>>>,
4437− next_sweep: Rc<Cell<usize>>,
4438−}
1976+struct EntryCache(Rc<RefCell<HashMap<String, Value>>>);
44391977
44401978impl EntryCache {
44411979 fn get(&self, key: &str) -> Option<Value> {
4442− self.entries.borrow().get(key).and_then(Weak::upgrade).map(Value::Record)
1980+ self.0.borrow().get(key).cloned()
44431981 }
44441982 fn insert(&self, key: String, entry: Value) {
4445− let Value::Record(record) = entry else { return };
4446− let mut entries = self.entries.borrow_mut();
4447− entries.insert(key, Rc::downgrade(&record));
4448− if entries.len() >= self.next_sweep.get().max(256) {
4449− entries.retain(|_, entry| entry.strong_count() != 0);
4450− self.next_sweep.set((entries.len() * 2).max(256));
4451− }
1983+ self.0.borrow_mut().insert(key, entry);
44521984 }
44531985}
44541986
@@ -4476,64 +2008,20 @@ async fn read_file_bytes(
44762008 Ok(buf)
44772009}
44782010
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(
2011+async fn side_to_bytes(
44822012 store: &Arc<Store>,
44832013 path: &RepoPath,
44842014 v: &Option<TreeValue>,
44852015 cache: &BlobCache,
4486−) -> Result<Option<ConflictSide>, Crash> {
2016+) -> Result<Rc<Vec<u8>>, Crash> {
44872017 match v {
4488− None => Ok(None),
4489− Some(TreeValue::File { id, executable, .. }) => Ok(Some(ConflictSide {
4490− kind: if *executable {
4491− BlobKind::Executable
4492− } else {
4493− BlobKind::Regular
4494− },
4495− bytes: read_file_bytes(store, path, id, cache).await?,
4496− tree: None,
4497− })),
4498− Some(TreeValue::Symlink(id)) => {
4499− let target = store
4500− .read_symlink(path, id)
4501− .await
4502− .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e)))?;
4503− Ok(Some(ConflictSide {
4504− kind: BlobKind::Symlink,
4505− bytes: Rc::new(target.into_bytes()),
4506− tree: None,
4507− }))
4508− }
4509− // a directory side, in a conflict between a file and a directory,
4510− // reads as empty content but keeps its tree, so writing the conflict
4511− // back cannot put an empty file where the directory was
4512− Some(TreeValue::Tree(id)) => Ok(Some(ConflictSide {
4513− kind: BlobKind::Regular,
4514− bytes: Rc::new(Vec::new()),
4515− tree: Some(id.hex()),
4516− })),
4517− // a submodule side reads as an empty file, as a resolved one does in
4518− // tree_value_to_blob (§12)
4519− Some(TreeValue::GitSubmodule(_)) => Ok(Some(ConflictSide::regular(&[]))),
4520− }
4521−}
4522−
4523−/// The value a list reads where a tree holds `value` (§7.3): a conflict less
4524−/// each pair of sides that cancel, one added and one removed the same, as jj
4525−/// simplifies a file conflict to write it out. jj keeps every term of a
4526−/// merge it cannot resolve, pairs that cancel included, so a replayed
4527−/// commit's conflict inherited from its parent was not the parent's, and
4528−/// each commit a rebase replayed above took two more sides for it. Where
4529−/// only directory sides would be left, the value is kept whole: jj keeps it
4530−/// as one conflict at the path, and the simplified one would be a conflict
4531−/// between directories, which jj merges entry by entry. Two values read
4532−/// the same are the same entry of a list.
4533−fn value_as_read(value: &MergedTreeValueT) -> std::borrow::Cow<'_, MergedTreeValueT> {
4534− match (!value.is_resolved()).then(|| value.simplify()).filter(|v| !v.is_tree()) {
4535− Some(simplified) => std::borrow::Cow::Owned(simplified),
4536− None => std::borrow::Cow::Borrowed(value),
2018+ Some(TreeValue::File { id, .. }) => read_file_bytes(store, path, id, cache).await,
2019+ Some(TreeValue::Symlink(id)) => store
2020+ .read_symlink(path, id)
2021+ .await
2022+ .map(|s| Rc::new(s.into_bytes()))
2023+ .map_err(|e| Crash::new(format!("cannot read a symlink: {}", e))),
2024+ _ => Ok(Rc::new(Vec::new())),
45372025 }
45382026}
45392027
@@ -4543,9 +2031,6 @@ async fn merged_value_to_blob(
45432031 value: &jj_lib::backend::MergedTreeValue,
45442032 cache: &BlobCache,
45452033) -> 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();
45492034 if let Some(v) = value.as_resolved() {
45502035 return match v {
45512036 Some(tv) => tree_value_to_blob(store, path, tv).await,
@@ -4556,11 +2041,11 @@ async fn merged_value_to_blob(
45562041 };
45572042 }
45582043 // conflict sides, jj order: [add, (remove, add)*] (§7.3)
4559− let mut sides: Vec<Option<ConflictSide>> = Vec::new();
2044+ let mut sides: Vec<Rc<Vec<u8>>> = Vec::new();
45602045 for (i, add) in value.adds().enumerate() {
4561− sides.push(conflict_side(store, path, add, cache).await?);
2046+ sides.push(side_to_bytes(store, path, add, cache).await?);
45622047 if let Some(remove) = value.get_remove(i) {
4563− sides.push(conflict_side(store, path, remove, cache).await?);
2048+ sides.push(side_to_bytes(store, path, remove, cache).await?);
45642049 }
45652050 }
45662051 Ok(Value::Blob(Rc::new(BlobVal {
@@ -4612,9 +2097,8 @@ async fn tree_value_to_blob(
46122097 content: BlobContent::Resolved(Rc::new(target.into_bytes())),
46132098 })))
46142099 }
4615− // MergedTree::entries recurses into a resolved directory, and a
4616− // directory side of a conflict goes through conflict_side, so this is
4617− // a submodule, which §12 puts out of scope
2100+ // a conflicted directory is emitted without its children by
2101+ // MergedTree::entries; §12 puts such repairs out of scope
46182102 TreeValue::Tree(_) | TreeValue::GitSubmodule(_) => Ok(Value::Blob(Rc::new(BlobVal {
46192103 kind: BlobKind::Regular,
46202104 content: BlobContent::Resolved(Rc::new(Vec::new())),
@@ -4628,27 +2112,9 @@ async fn tree_to_files(
46282112 cache: &BlobCache,
46292113 entries: &EntryCache,
46302114) -> Result<Vec<Value>, Crash> {
4631− tree_to_files_inner(store, tree, cache, entries).await.map(|(files, _)| files)
4632−}
4633−
4634−/// Whether these files can reuse the original tree in replay. Reading a
4635−/// conflict simplifies it, and an unsupported git submodule reads as an
4636−/// empty file, so their original trees cannot represent the resulting list.
4637−async fn tree_to_files_inner(
4638− store: &Arc<Store>,
4639− tree: &MergedTree,
4640− cache: &BlobCache,
4641− entries: &EntryCache,
4642−) -> Result<(Vec<Value>, bool), Crash> {
46432115 let mut out = Vec::new();
4644− let mut reusable = tree.tree_ids().is_resolved();
46452116 for (path, value) in tree.entries() {
46462117 let value = value.map_err(|e| Crash::new(format!("cannot read a tree entry: {}", e)))?;
4647− reusable &= match value.as_resolved() {
4648− Some(Some(TreeValue::File { copy_id, .. })) => *copy_id == CopyId::placeholder(),
4649− Some(Some(TreeValue::Symlink(_))) => true,
4650− _ => false,
4651− };
46522118 // cache key: the entry is fully determined by its path and the
46532119 // identity of its (resolved) content; conflicts are not cached
46542120 let key = match value.as_resolved() {
@@ -4679,22 +2145,13 @@ async fn tree_to_files_inner(
46792145 };
46802146 out.push(entry);
46812147 }
4682− Ok((out, reusable))
2148+ Ok(out)
46832149}
46842150
46852151// ----------------------------------------------------------------------
46862152// value helpers
46872153// ----------------------------------------------------------------------
46882154
4689−/// The name of a stored tree that a commit's lazy `files` carry
4690−/// (`ThunkVal::tree`): its tree ids, one per term of a conflicted tree. The
4691−/// list `tree_to_files` makes depends on nothing else, so two trees of one
4692−/// name list the same entries.
4693−fn tree_name(tree: &MergedTree) -> String {
4694− let ids: Vec<String> = tree.tree_ids().iter().map(|id| id.hex()).collect();
4695− ids.join(",")
4696−}
4697−
46982155fn commit_id_of(commit: &Value) -> Result<String, Crash> {
46992156 match commit.field("id")? {
47002157 Value::Id(i) => Ok(i.to_string()),
@@ -4722,13 +2179,15 @@ fn new_id_set(repo: &Value) -> Result<BTreeSet<String>, Crash> {
47222179// building the Repo value from the visible map (§7.2)
47232180// ----------------------------------------------------------------------
47242181
4725−fn build_repo_value(vis: &VisibleRepo, focus_change: &str, replay_cache: &Rc<ReplayCache>) -> Result<Value, OpenError> {
2182+fn build_repo_value(vis: &VisibleRepo, focus_change: &str) -> Result<Value, OpenError> {
47262183 let root_rec = vis
47272184 .commits
47282185 .get(ROOT_ID)
47292186 .ok_or_else(|| (2, "no root commit".to_string()))?;
47302187 let store = root_rec.commit.store().clone();
4731− let mut subtree = build_subtree(vis, root_rec, &store, replay_cache)?;
2188+ let cache = BlobCache::default();
2189+ let entries = EntryCache::default();
2190+ let mut subtree = build_subtree(vis, root_rec, &store, &cache, &entries)?;
47322191 sort_subtree(&mut subtree, vis);
47332192 let top = Value::record(&[
47342193 ("children", subtree.field("children").map_err(|e| (2, e.msg))?),
@@ -4745,20 +2204,19 @@ fn build_subtree(
47452204 vis: &VisibleRepo,
47462205 rec: &StoredCommit,
47472206 store: &Arc<Store>,
4748− replay_cache: &Rc<ReplayCache>,
2207+ cache: &BlobCache,
2208+ entries: &EntryCache,
47492209) -> Result<Value, OpenError> {
47502210 // the files list is lazy (§7.2): most commits are never inspected, so
4751− // their file entries are materialized only on first `field("files")`.
4752− // Tagged with the change id, it tells the renderer when the O(1)
4753− // `has_conflict`/`is_empty` answers still describe the commit's files;
4754− // named by its tree, it lets persistence compare it unread (§7.5)
2211+ // their file entries are materialized only on first `field("files")`
47552212 let files_thunk = {
47562213 let store = store.clone();
47572214 let tree = rec.commit.tree();
4758− let name = Some(tree_name(&tree));
4759− let replay_cache = replay_cache.clone();
4760− Value::Thunk(Rc::new(crate::value::ThunkVal::stored(rec.change_id.clone(), name, move || {
4761− block_on(replay_cache.files_of(&store, &tree))
2215+ let cache = cache.clone();
2216+ let entries = entries.clone();
2217+ Value::Thunk(Rc::new(crate::value::ThunkVal::new(move || {
2218+ let files = block_on(tree_to_files(&store, &tree, &cache, &entries))?;
2219+ Ok(Value::list(files))
47622220 })))
47632221 };
47642222 let labels: Vec<Value> = vis
@@ -4779,7 +2237,7 @@ fn build_subtree(
47792237 if let Some(kids) = vis.children.get(&rec.change_id) {
47802238 for kid in kids {
47812239 if let Some(krec) = vis.commits.get(kid) {
4782− children.push(build_subtree(vis, krec, store, replay_cache)?);
2240+ children.push(build_subtree(vis, krec, store, cache, entries)?);
47832241 }
47842242 }
47852243 }
@@ -4814,165 +2272,3 @@ fn sort_subtree(subtree: &mut Value, vis: &VisibleRepo) {
48142272 let root = subtree.field("root").expect("subtree has a root");
48152273 *subtree = Value::record(&[("children", Value::list(kids)), ("root", root)]);
48162274}
4817−
4818−#[cfg(test)]
4819−mod tests {
4820− use super::*;
4821− use jj_lib::backend::SymlinkId;
4822− use jj_lib::conflict_labels::ConflictLabels;
4823−
4824− fn replay_store() -> (tempfile::TempDir, Arc<Store>) {
4825− let dir = tempfile::tempdir().unwrap();
4826− let (_, repo) = block_on(Workspace::init_colocated_git(
4827− &user_settings(None), dir.path(), gix::hash::Kind::Sha1,
4828− )).unwrap();
4829− (dir, repo.store().clone())
4830− }
4831−
4832− fn file(path: &str, content: &str) -> Value {
4833− Value::record(&[
4834− ("path", Value::list(vec![Value::text(path)])),
4835− ("content", BlobVal::text_blob(content)),
4836− ])
4837− }
4838−
4839− fn loaded_files(store: &Arc<Store>, cache: &ReplayCache, files: Vec<Value>) -> Value {
4840− let tree = block_on(build_tree(store, &Value::list(files))).unwrap();
4841− block_on(cache.files_of(store, &tree)).unwrap()
4842− }
4843−
4844− #[test]
4845− fn replay_reuses_loaded_snapshots_and_unchanged_entries() {
4846− let (_dir, store) = replay_store();
4847− let cache = ReplayCache::default();
4848− let base = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "base b")]);
4849− let same = block_on(jj_replay(&store, &cache, &base, &base, &base)).unwrap();
4850− let (Value::List(before), Value::List(after)) = (&base, &same) else { panic!() };
4851− assert_eq!(before.identity(), after.identity(), "no-op replay must retain the snapshot");
4852−
4853− let onto = loaded_files(&store, &cache, vec![file("a", "onto"), file("b", "base b")]);
4854− let to = loaded_files(&store, &cache, vec![file("a", "base a"), file("b", "to")]);
4855− let merged = block_on(jj_replay(&store, &cache, &onto, &base, &to)).unwrap();
4856− let expected = Value::list(vec![file("a", "onto"), file("b", "to")]);
4857− assert!(crate::value::value_eq(&merged, &expected).unwrap());
4858− for (result, input) in [
4859− (&merged.as_list().unwrap()[0], &onto.as_list().unwrap()[0]),
4860− (&merged.as_list().unwrap()[1], &to.as_list().unwrap()[1]),
4861− ] {
4862− let (Value::Record(result), Value::Record(input)) = (result, input) else { panic!() };
4863− assert!(Rc::ptr_eq(result, input), "unchanged file entries must be shared");
4864− }
4865− }
4866−
4867− #[test]
4868− fn replay_cache_preserves_validation_ordering_and_list_offsets() {
4869− let (_dir, store) = replay_store();
4870− let cache = ReplayCache::default();
4871− let unsorted = Value::list(vec![file("b", "b"), file("a", "a")]);
4872− let empty = Value::list(vec![]);
4873− let sorted = block_on(jj_replay(&store, &cache, &unsorted, &empty, &empty)).unwrap();
4874− assert!(crate::value::value_eq(&sorted, &Value::list(vec![file("a", "a"), file("b", "b")])).unwrap());
4875−
4876− let Value::List(list) = &sorted else { panic!() };
4877− let tail = Value::List(list.skip(1));
4878− let replayed_tail = block_on(jj_replay(&store, &cache, &tail, &empty, &empty)).unwrap();
4879− assert!(crate::value::value_eq(&tail, &replayed_tail).unwrap());
4880− assert_eq!(replayed_tail.as_list().unwrap().len(), 1);
4881−
4882− let malformed = Value::list(vec![file("a", "one"), file("a", "two")]);
4883− let err = block_on(jj_replay(&store, &cache, &sorted, &malformed, &malformed)).unwrap_err();
4884− assert!(err.msg.contains("duplicate paths"), "{}", err.msg);
4885− }
4886−
4887− #[test]
4888− fn replay_ignores_empty_directories_not_represented_by_snapshots() {
4889− let (_dir, store) = replay_store();
4890− let cache = ReplayCache::default();
4891− let entry = |id| jj_lib::backend::Tree::from_sorted_entries(vec![
4892− (RepoPathComponentBuf::new("d".to_string()).unwrap(), TreeValue::Tree(id)),
4893− ]);
4894− let direct = block_on(store.write_tree(RepoPath::root(), entry(store.empty_tree_id().clone()))).unwrap();
4895− let nested = block_on(store.write_tree(RepoPath::root(), entry(direct.id().clone()))).unwrap();
4896− let empty = Value::list(vec![]);
4897− let to = Value::list(vec![file("d", "file")]);
4898− for tree in [direct, nested] {
4899− let tree = MergedTree::resolved(store.clone(), tree.id().clone());
4900− let onto = block_on(cache.files_of(&store, &tree)).unwrap();
4901− assert!(onto.as_list().unwrap().is_empty());
4902− let result = block_on(jj_replay(&store, &cache, &onto, &empty, &to)).unwrap();
4903− assert!(crate::value::value_eq(&result, &to).unwrap());
4904− }
4905− }
4906−
4907− #[test]
4908− fn replay_cache_does_not_keep_discarded_snapshots_alive() {
4909− let (_dir, store) = replay_store();
4910− let cache = ReplayCache::default();
4911− let files = loaded_files(&store, &cache, vec![file("a", "a")]);
4912− let Value::List(list) = &files else { panic!() };
4913− let storage = list.weak_storage();
4914− let Value::Record(entry) = &files.as_list().unwrap()[0] else { panic!() };
4915− let entry = Rc::downgrade(entry);
4916− drop(files);
4917− assert!(storage.upgrade().is_none());
4918− assert!(entry.upgrade().is_none());
4919−
4920− let bytes = Rc::new(vec![1, 2, 3]);
4921− let weak_bytes = Rc::downgrade(&bytes);
4922− let id = FileId::new(vec![1; 20]);
4923− cache.blobs.insert(id.clone(), bytes.clone());
4924− assert!(Rc::ptr_eq(&cache.blobs.get(&id).unwrap(), &bytes));
4925− drop(bytes);
4926− assert!(weak_bytes.upgrade().is_none());
4927− assert!(cache.blobs.get(&id).is_none());
4928− }
4929−
4930− #[test]
4931− fn user_settings_preserve_every_ascii_control_character() {
4932− let identity: String = (0u8..=127).map(char::from).collect();
4933− let text = format!(
4934− "[user]\nname = {}\nemail = {}\n",
4935− toml_string(&identity),
4936− toml_string(&identity)
4937− );
4938− let mut config = StackedConfig::with_defaults();
4939− config.add_layer(ConfigLayer::parse(ConfigSource::User, &text).unwrap());
4940− let settings = UserSettings::from_config(config).unwrap();
4941− assert_eq!(settings.user_name(), identity);
4942− assert_eq!(settings.user_email(), identity);
4943− }
4944−
4945− #[test]
4946− fn a_description_names_its_terms_whatever_their_labels_and_the_sides_that_cancel() {
4947− // what a checkout writes for a symlink/file conflict of five terms,
4948− // and for the three it simplifies to, which a snapshot may write
4949− // without writing the file again (§7.4)
4950− let link = Some(TreeValue::Symlink(SymlinkId::new(vec![1; 20])));
4951− let file = |n: u8| {
4952− let id = FileId::new(vec![n; 20]);
4953− Some(TreeValue::File { id, executable: false, copy_id: CopyId::placeholder() })
4954− };
4955− let five = Merge::from_vec(vec![link.clone(), link.clone(), file(2), file(1), link.clone()]);
4956− let three = five.simplify();
4957− assert_eq!(three.num_sides(), 2);
4958− let unlabeled = ConflictLabels::unlabeled();
4959− let written = five.describe(&unlabeled);
4960− let simpler = three.describe(&unlabeled);
4961− assert_ne!(written, simpler);
4962− let terms = described_terms(simpler.as_bytes());
4963− assert!(terms.is_some());
4964− assert_eq!(described_terms(written.as_bytes()), terms);
4965− let names = ["left (one)", "base", "middle", "base 2", "right"];
4966− let labels = ConflictLabels::from_vec(names.iter().map(|n| n.to_string()).collect());
4967− let labelled = five.describe(&labels);
4968− assert!(labelled.contains(" (left (one))\n"), "{}", labelled);
4969− assert_eq!(described_terms(labelled.as_bytes()), terms);
4970− // other sides, and text that is no description, are no such conflict
4971− let other = Merge::from_vec(vec![link.clone(), file(1), file(3)]).describe(&unlabeled);
4972− assert!(described_terms(other.as_bytes()).is_some_and(|t| Some(t) != terms));
4973− assert_eq!(described_terms(b"resolved\n"), None);
4974− assert_eq!(described_terms(written.replace("Conflict:", "CONFLICT:").as_bytes()), None);
4975− assert_eq!(described_terms(written.trim_end().as_bytes()), None);
4976− assert_eq!(described_terms(format!("{} Adding file with id 0101 and more\n", written).as_bytes()), None);
4977− }
4978−}