working_copy: plumbing to propagate untracked paths to caller

This commit is contained in:
Yuya Nishihara 2024-12-10 17:10:31 +09:00
parent e1936a2e8b
commit f4fdc19d9e
9 changed files with 70 additions and 21 deletions

View file

@ -41,6 +41,7 @@ use jj_lib::working_copy::LockedWorkingCopy;
use jj_lib::working_copy::ResetError; use jj_lib::working_copy::ResetError;
use jj_lib::working_copy::SnapshotError; use jj_lib::working_copy::SnapshotError;
use jj_lib::working_copy::SnapshotOptions; use jj_lib::working_copy::SnapshotOptions;
use jj_lib::working_copy::SnapshotStats;
use jj_lib::working_copy::WorkingCopy; use jj_lib::working_copy::WorkingCopy;
use jj_lib::working_copy::WorkingCopyFactory; use jj_lib::working_copy::WorkingCopyFactory;
use jj_lib::working_copy::WorkingCopyStateError; use jj_lib::working_copy::WorkingCopyStateError;
@ -233,7 +234,10 @@ impl LockedWorkingCopy for LockedConflictsWorkingCopy {
self.inner.old_tree_id() self.inner.old_tree_id()
} }
fn snapshot(&mut self, options: &SnapshotOptions) -> Result<MergedTreeId, SnapshotError> { fn snapshot(
&mut self,
options: &SnapshotOptions,
) -> Result<(MergedTreeId, SnapshotStats), SnapshotError> {
let options = SnapshotOptions { let options = SnapshotOptions {
base_ignores: options.base_ignores.chain("", "/.conflicts".as_bytes())?, base_ignores: options.base_ignores.chain("", "/.conflicts".as_bytes())?,
..options.clone() ..options.clone()

View file

@ -1806,7 +1806,8 @@ See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
}; };
self.user_repo = ReadonlyUserRepo::new(repo); self.user_repo = ReadonlyUserRepo::new(repo);
let progress = crate::progress::snapshot_progress(ui); let progress = crate::progress::snapshot_progress(ui);
let new_tree_id = locked_ws // TODO: print stats
let (new_tree_id, _stats) = locked_ws
.locked_wc() .locked_wc()
.snapshot(&SnapshotOptions { .snapshot(&SnapshotOptions {
base_ignores, base_ignores,

View file

@ -52,7 +52,8 @@ pub(crate) fn cmd_file_track(
let mut tx = workspace_command.start_transaction().into_inner(); let mut tx = workspace_command.start_transaction().into_inner();
let base_ignores = workspace_command.base_ignores()?; let base_ignores = workspace_command.base_ignores()?;
let (mut locked_ws, _wc_commit) = workspace_command.start_working_copy_mutation()?; let (mut locked_ws, _wc_commit) = workspace_command.start_working_copy_mutation()?;
locked_ws.locked_wc().snapshot(&SnapshotOptions { // TODO: print stats
let (_tree_id, _stats) = locked_ws.locked_wc().snapshot(&SnapshotOptions {
base_ignores, base_ignores,
fsmonitor_settings: command.settings().fsmonitor_settings()?, fsmonitor_settings: command.settings().fsmonitor_settings()?,
progress: None, progress: None,

View file

@ -76,7 +76,8 @@ pub(crate) fn cmd_file_untrack(
locked_ws.locked_wc().reset(&new_commit)?; locked_ws.locked_wc().reset(&new_commit)?;
// Commit the working copy again so we can inform the user if paths couldn't be // Commit the working copy again so we can inform the user if paths couldn't be
// untracked because they're not ignored. // untracked because they're not ignored.
let wc_tree_id = locked_ws.locked_wc().snapshot(&SnapshotOptions { // TODO: print stats
let (wc_tree_id, _stats) = locked_ws.locked_wc().snapshot(&SnapshotOptions {
base_ignores, base_ignores,
fsmonitor_settings: command.settings().fsmonitor_settings()?, fsmonitor_settings: command.settings().fsmonitor_settings()?,
progress: None, progress: None,

View file

@ -108,6 +108,8 @@ use crate::working_copy::ResetError;
use crate::working_copy::SnapshotError; use crate::working_copy::SnapshotError;
use crate::working_copy::SnapshotOptions; use crate::working_copy::SnapshotOptions;
use crate::working_copy::SnapshotProgress; use crate::working_copy::SnapshotProgress;
use crate::working_copy::SnapshotStats;
use crate::working_copy::UntrackedReason;
use crate::working_copy::WorkingCopy; use crate::working_copy::WorkingCopy;
use crate::working_copy::WorkingCopyFactory; use crate::working_copy::WorkingCopyFactory;
use crate::working_copy::WorkingCopyStateError; use crate::working_copy::WorkingCopyStateError;
@ -908,7 +910,10 @@ impl TreeState {
/// Look for changes to the working copy. If there are any changes, create /// Look for changes to the working copy. If there are any changes, create
/// a new tree from it. /// a new tree from it.
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn snapshot(&mut self, options: &SnapshotOptions) -> Result<bool, SnapshotError> { pub fn snapshot(
&mut self,
options: &SnapshotOptions,
) -> Result<(bool, SnapshotStats), SnapshotError> {
let &SnapshotOptions { let &SnapshotOptions {
ref base_ignores, ref base_ignores,
ref fsmonitor_settings, ref fsmonitor_settings,
@ -935,11 +940,12 @@ impl TreeState {
if matcher.visit(RepoPath::root()).is_nothing() { if matcher.visit(RepoPath::root()).is_nothing() {
// No need to load the current tree, set up channels, etc. // No need to load the current tree, set up channels, etc.
self.watchman_clock = watchman_clock; self.watchman_clock = watchman_clock;
return Ok(is_dirty); return Ok((is_dirty, SnapshotStats::default()));
} }
let (tree_entries_tx, tree_entries_rx) = channel(); let (tree_entries_tx, tree_entries_rx) = channel();
let (file_states_tx, file_states_rx) = channel(); let (file_states_tx, file_states_rx) = channel();
let (untracked_paths_tx, untracked_paths_rx) = channel();
let (deleted_files_tx, deleted_files_rx) = channel(); let (deleted_files_tx, deleted_files_rx) = channel();
trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> { trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> {
@ -951,6 +957,7 @@ impl TreeState {
// Move tx sides so they'll be dropped at the end of the scope. // Move tx sides so they'll be dropped at the end of the scope.
tree_entries_tx, tree_entries_tx,
file_states_tx, file_states_tx,
untracked_paths_tx,
deleted_files_tx, deleted_files_tx,
error: OnceLock::new(), error: OnceLock::new(),
progress, progress,
@ -972,6 +979,9 @@ impl TreeState {
snapshotter.into_result() snapshotter.into_result()
})?; })?;
let stats = SnapshotStats {
untracked_paths: untracked_paths_rx.into_iter().collect(),
};
let mut tree_builder = MergedTreeBuilder::new(self.tree_id.clone()); let mut tree_builder = MergedTreeBuilder::new(self.tree_id.clone());
trace_span!("process tree entries").in_scope(|| { trace_span!("process tree entries").in_scope(|| {
for (path, tree_values) in &tree_entries_rx { for (path, tree_values) in &tree_entries_rx {
@ -1011,7 +1021,7 @@ impl TreeState {
assert_eq!(state_paths, tree_paths); assert_eq!(state_paths, tree_paths);
} }
self.watchman_clock = watchman_clock; self.watchman_clock = watchman_clock;
Ok(is_dirty) Ok((is_dirty, stats))
} }
#[instrument(skip_all)] #[instrument(skip_all)]
@ -1087,6 +1097,8 @@ struct FileSnapshotter<'a> {
start_tracking_matcher: &'a dyn Matcher, start_tracking_matcher: &'a dyn Matcher,
tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>, tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>,
file_states_tx: Sender<(RepoPathBuf, FileState)>, file_states_tx: Sender<(RepoPathBuf, FileState)>,
#[allow(unused)] // TODO
untracked_paths_tx: Sender<(RepoPathBuf, UntrackedReason)>,
deleted_files_tx: Sender<RepoPathBuf>, deleted_files_tx: Sender<RepoPathBuf>,
error: OnceLock<SnapshotError>, error: OnceLock<SnapshotError>,
progress: Option<&'a SnapshotProgress<'a>>, progress: Option<&'a SnapshotProgress<'a>>,
@ -2150,7 +2162,10 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy {
&self.old_tree_id &self.old_tree_id
} }
fn snapshot(&mut self, options: &SnapshotOptions) -> Result<MergedTreeId, SnapshotError> { fn snapshot(
&mut self,
options: &SnapshotOptions,
) -> Result<(MergedTreeId, SnapshotStats), SnapshotError> {
let tree_state = self let tree_state = self
.wc .wc
.tree_state_mut() .tree_state_mut()
@ -2158,8 +2173,9 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy {
message: "Failed to read the working copy state".to_string(), message: "Failed to read the working copy state".to_string(),
err: err.into(), err: err.into(),
})?; })?;
self.tree_state_dirty |= tree_state.snapshot(options)?; let (is_dirty, stats) = tree_state.snapshot(options)?;
Ok(tree_state.current_tree_id().clone()) self.tree_state_dirty |= is_dirty;
Ok((tree_state.current_tree_id().clone(), stats))
} }
fn check_out( fn check_out(

View file

@ -16,6 +16,7 @@
//! default local-disk implementation. //! default local-disk implementation.
use std::any::Any; use std::any::Any;
use std::collections::BTreeMap;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -113,8 +114,11 @@ pub trait LockedWorkingCopy {
/// The tree at the time the lock was taken /// The tree at the time the lock was taken
fn old_tree_id(&self) -> &MergedTreeId; fn old_tree_id(&self) -> &MergedTreeId;
/// Snapshot the working copy and return the tree id. /// Snapshot the working copy. Returns the tree id and stats.
fn snapshot(&mut self, options: &SnapshotOptions) -> Result<MergedTreeId, SnapshotError>; fn snapshot(
&mut self,
options: &SnapshotOptions,
) -> Result<(MergedTreeId, SnapshotStats), SnapshotError>;
/// Check out the specified commit in the working copy. /// Check out the specified commit in the working copy.
fn check_out( fn check_out(
@ -249,6 +253,25 @@ impl SnapshotOptions<'_> {
/// A callback for getting progress updates. /// A callback for getting progress updates.
pub type SnapshotProgress<'a> = dyn Fn(&RepoPath) + 'a + Sync; pub type SnapshotProgress<'a> = dyn Fn(&RepoPath) + 'a + Sync;
/// Stats about a snapshot operation on a working copy.
#[derive(Clone, Debug, Default)]
pub struct SnapshotStats {
/// List of new (previously untracked) files which are still untracked.
pub untracked_paths: BTreeMap<RepoPathBuf, UntrackedReason>,
}
/// Reason why the new path isn't tracked.
#[derive(Clone, Debug)]
pub enum UntrackedReason {
/// File was larger than the specified maximum file size.
FileTooLarge {
/// Actual size of the large file.
size: u64,
/// Maximum allowed size.
max_size: u64,
},
}
/// Options used when checking out a tree in the working copy. /// Options used when checking out a tree in the working copy.
#[derive(Clone)] #[derive(Clone)]
pub struct CheckoutOptions { pub struct CheckoutOptions {

View file

@ -939,7 +939,7 @@ fn test_snapshot_racy_timestamps() {
.workspace .workspace
.start_working_copy_mutation() .start_working_copy_mutation()
.unwrap(); .unwrap();
let new_tree_id = locked_ws let (new_tree_id, _stats) = locked_ws
.locked_wc() .locked_wc()
.snapshot(&SnapshotOptions::empty_for_test()) .snapshot(&SnapshotOptions::empty_for_test())
.unwrap(); .unwrap();
@ -973,7 +973,7 @@ fn test_snapshot_special_file() {
// Snapshot the working copy with the socket file // Snapshot the working copy with the socket file
let mut locked_ws = ws.start_working_copy_mutation().unwrap(); let mut locked_ws = ws.start_working_copy_mutation().unwrap();
let tree_id = locked_ws let (tree_id, _stats) = locked_ws
.locked_wc() .locked_wc()
.snapshot(&SnapshotOptions::empty_for_test()) .snapshot(&SnapshotOptions::empty_for_test())
.unwrap(); .unwrap();
@ -2052,7 +2052,7 @@ fn test_fsmonitor() {
.iter() .iter()
.map(|p| p.to_fs_path_unchecked(Path::new(""))) .map(|p| p.to_fs_path_unchecked(Path::new("")))
.collect(); .collect();
locked_ws let (tree_id, _stats) = locked_ws
.locked_wc() .locked_wc()
.snapshot(&SnapshotOptions { .snapshot(&SnapshotOptions {
fsmonitor_settings: FsmonitorSettings::Test { fsmonitor_settings: FsmonitorSettings::Test {
@ -2060,7 +2060,8 @@ fn test_fsmonitor() {
}, },
..SnapshotOptions::empty_for_test() ..SnapshotOptions::empty_for_test()
}) })
.unwrap() .unwrap();
tree_id
}; };
{ {

View file

@ -169,7 +169,7 @@ fn test_checkout_parallel() {
// &CheckoutOptions::empty_for_test()) should never produce a // &CheckoutOptions::empty_for_test()) should never produce a
// different tree. // different tree.
let mut locked_ws = workspace.start_working_copy_mutation().unwrap(); let mut locked_ws = workspace.start_working_copy_mutation().unwrap();
let new_tree_id = locked_ws let (new_tree_id, _stats) = locked_ws
.locked_wc() .locked_wc()
.snapshot(&SnapshotOptions::empty_for_test()) .snapshot(&SnapshotOptions::empty_for_test())
.unwrap(); .unwrap();

View file

@ -60,6 +60,7 @@ use jj_lib::tree::Tree;
use jj_lib::tree_builder::TreeBuilder; use jj_lib::tree_builder::TreeBuilder;
use jj_lib::working_copy::SnapshotError; use jj_lib::working_copy::SnapshotError;
use jj_lib::working_copy::SnapshotOptions; use jj_lib::working_copy::SnapshotOptions;
use jj_lib::working_copy::SnapshotStats;
use jj_lib::workspace::Workspace; use jj_lib::workspace::Workspace;
use pollster::FutureExt; use pollster::FutureExt;
use tempfile::TempDir; use tempfile::TempDir;
@ -302,17 +303,18 @@ impl TestWorkspace {
pub fn snapshot_with_options( pub fn snapshot_with_options(
&mut self, &mut self,
options: &SnapshotOptions, options: &SnapshotOptions,
) -> Result<MergedTree, SnapshotError> { ) -> Result<(MergedTree, SnapshotStats), SnapshotError> {
let mut locked_ws = self.workspace.start_working_copy_mutation().unwrap(); let mut locked_ws = self.workspace.start_working_copy_mutation().unwrap();
let tree_id = locked_ws.locked_wc().snapshot(options)?; let (tree_id, stats) = locked_ws.locked_wc().snapshot(options)?;
// arbitrary operation id // arbitrary operation id
locked_ws.finish(self.repo.op_id().clone()).unwrap(); locked_ws.finish(self.repo.op_id().clone()).unwrap();
Ok(self.repo.store().get_root_tree(&tree_id).unwrap()) Ok((self.repo.store().get_root_tree(&tree_id).unwrap(), stats))
} }
/// Like `snapshot_with_option()` but with default options /// Like `snapshot_with_option()` but with default options
pub fn snapshot(&mut self) -> Result<MergedTree, SnapshotError> { pub fn snapshot(&mut self) -> Result<MergedTree, SnapshotError> {
self.snapshot_with_options(&SnapshotOptions::empty_for_test()) let (tree_id, _stats) = self.snapshot_with_options(&SnapshotOptions::empty_for_test())?;
Ok(tree_id)
} }
} }