diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e2d1c32..05d51f911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The default editor on Windows is now `Notepad` instead of `pico`. +* `jj` will fail attempts to snapshot new files larger than 1MiB by default. This behavior + can be customized with the `snapshot.max-new-file-size` config option. + + ### New features * Default template for `jj log` now does not show irrelevant information diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 58d29afc3..3bf7c2e53 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1289,6 +1289,7 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin base_ignores, fsmonitor_kind: self.settings.fsmonitor_kind()?, progress: progress.as_ref().map(|x| x as _), + max_new_file_size: self.settings.max_new_file_size()?, })?; drop(progress); if new_tree_id != *wc_commit.tree_id() { diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index a8711ad37..328ae074f 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1338,6 +1338,7 @@ fn cmd_untrack( base_ignores, fsmonitor_kind: command.settings().fsmonitor_kind()?, progress: None, + max_new_file_size: command.settings().max_new_file_size()?, })?; if wc_tree_id != new_tree_id { let wc_tree = store.get_tree(&RepoPath::root(), &wc_tree_id)?; diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index da9b55b6a..dd69dd3a1 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -329,6 +329,17 @@ "type": "string" } } + }, + "snapshot": { + "type": "object", + "description": "Parameters governing automatic capture of files into the working copy commit", + "properties": { + "max-new-file-size": { + "type": "integer", + "description": "New files with a size in bytes above this threshold are not snapshotted, unless the threshold is 0", + "default": "1048576" + } + } } } } diff --git a/cli/src/config/misc.toml b/cli/src/config/misc.toml index dbd203eee..a53e16fd5 100644 --- a/cli/src/config/misc.toml +++ b/cli/src/config/misc.toml @@ -8,3 +8,6 @@ paginate = "auto" pager = { command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8" } } log-word-wrap = false + +[snapshot] +max-new-file-size = 1048576 diff --git a/cli/src/merge_tools/external.rs b/cli/src/merge_tools/external.rs index 84acdff41..68bbfa9d5 100644 --- a/cli/src/merge_tools/external.rs +++ b/cli/src/merge_tools/external.rs @@ -414,6 +414,7 @@ pub fn edit_diff_external( base_ignores, fsmonitor_kind: settings.fsmonitor_kind()?, progress: None, + max_new_file_size: settings.max_new_file_size()?, })?; Ok(right_tree_state.current_tree_id().clone()) } diff --git a/lib/src/settings.rs b/lib/src/settings.rs index dac2bf13a..4c7b21162 100644 --- a/lib/src/settings.rs +++ b/lib/src/settings.rs @@ -196,6 +196,16 @@ impl UserSettings { .get_string("ui.graph.style") .unwrap_or_else(|_| "curved".to_string()) } + + pub fn max_new_file_size(&self) -> Result { + let cfg = self.config.get::("snapshot.max-new-file-size"); + match cfg { + Ok(0) => Ok(u64::MAX), + x @ Ok(_) => x, + Err(config::ConfigError::NotFound(_)) => Ok(1024 * 1024), + e @ Err(_) => e, + } + } } /// This Rng uses interior mutability to allow generating random values using an diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 4f95b65ca..e2a6a69ed 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -310,6 +310,12 @@ pub enum SnapshotError { InternalBackendError(#[from] BackendError), #[error(transparent)] TreeStateError(#[from] TreeStateError), + #[error("New file {path} of size {size} exceeds snapshot.max-new-file-size ({max_size})")] + NewFileTooLarge { + path: PathBuf, + size: u64, + max_size: u64, + }, } #[derive(Debug, Error)] @@ -358,6 +364,7 @@ pub struct SnapshotOptions<'a> { pub base_ignores: Arc, pub fsmonitor_kind: Option, pub progress: Option<&'a SnapshotProgress<'a>>, + pub max_new_file_size: u64, } impl SnapshotOptions<'_> { @@ -366,6 +373,7 @@ impl SnapshotOptions<'_> { base_ignores: GitIgnoreFile::empty(), fsmonitor_kind: None, progress: None, + max_new_file_size: u64::MAX, } } } @@ -630,6 +638,7 @@ impl TreeState { base_ignores, fsmonitor_kind, progress, + max_new_file_size, } = options; let sparse_matcher = self.sparse_matcher(); @@ -665,6 +674,7 @@ impl TreeState { present_files_tx, directory_to_visit, progress, + max_new_file_size, ) })?; @@ -736,6 +746,7 @@ impl TreeState { present_files_tx: Sender, directory_to_visit: DirectoryToVisit, progress: Option<&SnapshotProgress>, + max_new_file_size: u64, ) -> Result<(), SnapshotError> { let DirectoryToVisit { dir, @@ -841,6 +852,7 @@ impl TreeState { present_files_tx.clone(), directory_to_visit, progress, + max_new_file_size, )?; } } else if matcher.matches(&path) { @@ -859,6 +871,14 @@ impl TreeState { message: format!("Failed to stat file {}", entry.path().display()), err, })?; + if maybe_current_file_state.is_none() && metadata.len() > max_new_file_size + { + return Err(SnapshotError::NewFileTooLarge { + path: entry.path().clone(), + size: metadata.len(), + max_size: max_new_file_size, + }); + } if let Some(new_file_state) = file_state(&metadata) { present_files_tx.send(path.clone()).ok(); let update = self.get_updated_tree_value(