diff --git a/cli/src/command_error.rs b/cli/src/command_error.rs index 6f95390aa43..0a27199143a 100644 --- a/cli/src/command_error.rs +++ b/cli/src/command_error.rs @@ -32,7 +32,7 @@ use jj_lib::revset::{ }; use jj_lib::signing::SignInitError; use jj_lib::str_util::StringPatternParseError; -use jj_lib::working_copy::{ResetError, SnapshotError, WorkingCopyStateError}; +use jj_lib::working_copy::{NewFileTooLarge, ResetError, SnapshotError, WorkingCopyStateError}; use jj_lib::workspace::WorkspaceInitError; use thiserror::Error; @@ -301,11 +301,12 @@ impl From for CommandError { impl From for CommandError { fn from(err: SnapshotError) -> Self { match err { - SnapshotError::NewFileTooLarge { - path, - size, - max_size, - } => { + SnapshotError::NewFileTooLarge(ref e) => { + let NewFileTooLarge { + path, + size, + max_size, + } = e.first().unwrap(); // if the size difference is < 1KiB, then show exact bytes. // otherwise, show in human-readable form; this avoids weird cases // where a file is 400 bytes too large but the error says something @@ -320,12 +321,16 @@ impl From for CommandError { format!("it is {}; the maximum size allowed is ~{}.", size, max_size,) }; - user_error(format!( - "Failed to snapshot the working copy\nThe file '{}' is too large to be \ - snapshotted: {}", - path.display(), - err_str, - )) + let size = size.0; + user_error_with_message( + format!( + "Failed to snapshot the working copy\nThe file '{}' is too large to be \ + snapshotted: {}", + path.display(), + err_str, + ), + err, + ) .hinted(format!( "This is to prevent large files from being added on accident. You can fix \ this error by: @@ -334,7 +339,7 @@ impl From for CommandError { This will increase the maximum file size allowed for new files, in this repository only. - Run `jj --config-toml 'snapshot.max-new-file-size={}' st` This will increase the maximum file size allowed for new files, for this command only.", - size.0, size.0 + size, size )) } err => internal_error_with_message("Failed to snapshot the working copy", err), diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 4498ed398ce..9bd37852e47 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -40,6 +40,7 @@ mod obslog; mod operation; mod parallelize; mod prev; +mod purge; mod rebase; mod resolve; mod restore; @@ -123,6 +124,7 @@ enum Command { Operation(operation::OperationCommand), Parallelize(parallelize::ParallelizeArgs), Prev(prev::PrevArgs), + Purge(purge::PurgeArgs), Rebase(rebase::RebaseArgs), Resolve(resolve::ResolveArgs), Restore(restore::RestoreArgs), @@ -204,6 +206,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Resolve(args) => resolve::cmd_resolve(ui, command_helper, args), Command::Restore(args) => restore::cmd_restore(ui, command_helper, args), Command::Revert(_args) => revert(), + Command::Purge(args) => purge::cmd_purge(ui, command_helper, args), Command::Root(args) => root::cmd_root(ui, command_helper, args), Command::Run(args) => run::cmd_run(ui, command_helper, args), Command::Show(args) => show::cmd_show(ui, command_helper, args), diff --git a/cli/src/commands/purge.rs b/cli/src/commands/purge.rs new file mode 100644 index 00000000000..d6cdf7b14f2 --- /dev/null +++ b/cli/src/commands/purge.rs @@ -0,0 +1,71 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::error::Error; +use std::fs; +use std::io::Write; + +use jj_lib::settings::HumanByteSize; +use jj_lib::working_copy::SnapshotError; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Removes files not tracked by Jujutsu +/// Note: snapshot won't be taken before purging, so there is no way to undo +/// this operation +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct PurgeArgs { + /// Dry run, don't actually remove files + #[arg(short, long, default_value = "false")] + dry_run: bool, +} + +pub(crate) fn cmd_purge( + ui: &mut Ui, + command: &CommandHelper, + args: &PurgeArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui); + if let Err(e) = workspace_command { + let Some(e) = e.error.source() else { + return Ok(()); + }; + let e = e.downcast_ref::(); + if let Some(SnapshotError::NewFileTooLarge(files)) = e { + writeln!( + ui.status(), + "The following files are too large to be added to the working copy:" + )?; + for file in files { + writeln!(ui.status(), " {}", &file.path.display())?; + } + if !args.dry_run { + for file in files { + fs::remove_file(&file.path)?; + } + } + let total_size: u64 = files.iter().map(|file| file.size.0).sum(); + + writeln!( + ui.status(), + "Removed {} files totaling {}", + files.len(), + HumanByteSize(total_size) + )?; + } + } + Ok(()) +} diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index b06a2f5e0f5..4d793ef2422 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -68,6 +68,7 @@ This document contains the help content for the `jj` command-line program. * [`jj operation undo`↴](#jj-operation-undo) * [`jj parallelize`↴](#jj-parallelize) * [`jj prev`↴](#jj-prev) +* [`jj purge`↴](#jj-purge) * [`jj rebase`↴](#jj-rebase) * [`jj resolve`↴](#jj-resolve) * [`jj restore`↴](#jj-restore) @@ -132,6 +133,7 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d * `operation` — Commands for working with the operation log * `parallelize` — Parallelize revisions by making them siblings * `prev` — Change the working copy revision relative to the parent revision +* `purge` — Removes files not tracked by Jujutsu Note: snapshot won't be taken before purging, so there is no way to undo this operation * `rebase` — Move revisions to different parent(s) * `resolve` — Resolve a conflicted file with an external merge tool * `restore` — Restore paths from another revision @@ -1528,6 +1530,20 @@ implied. +## `jj purge` + +Removes files not tracked by Jujutsu Note: snapshot won't be taken before purging, so there is no way to undo this operation + +**Usage:** `jj purge [OPTIONS]` + +###### **Options:** + +* `-d`, `--dry-run` — Dry run, don't actually remove files + + Default value: `false` + + + ## `jj rebase` Move revisions to different parent(s) diff --git a/lib/src/local_working_copy.rs b/lib/src/local_working_copy.rs index 7a52af951dc..e0462b75f11 100644 --- a/lib/src/local_working_copy.rs +++ b/lib/src/local_working_copy.rs @@ -64,8 +64,8 @@ use crate::settings::HumanByteSize; use crate::store::Store; use crate::tree::Tree; use crate::working_copy::{ - CheckoutError, CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions, - SnapshotProgress, WorkingCopy, WorkingCopyFactory, WorkingCopyStateError, + CheckoutError, CheckoutStats, LockedWorkingCopy, NewFileTooLarge, ResetError, SnapshotError, + SnapshotOptions, SnapshotProgress, WorkingCopy, WorkingCopyFactory, WorkingCopyStateError, }; #[cfg(unix)] @@ -790,6 +790,8 @@ impl TreeState { let (file_states_tx, file_states_rx) = channel(); let (present_files_tx, present_files_rx) = channel(); + let (files_to_big_tx, files_to_big_rx) = channel(); + trace_span!("traverse filesystem").in_scope(|| -> Result<(), SnapshotError> { let current_tree = self.current_tree()?; let directory_to_visit = DirectoryToVisit { @@ -807,6 +809,7 @@ impl TreeState { directory_to_visit, progress, max_new_file_size, + files_to_big_tx, ) })?; @@ -865,6 +868,11 @@ impl TreeState { let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect(); assert_eq!(state_paths, tree_paths); } + let failed_files: Vec<_> = files_to_big_rx.iter().collect(); + if !failed_files.is_empty() { + return Err(SnapshotError::NewFileTooLarge(failed_files)); + } + self.watchman_clock = watchman_clock; Ok(is_dirty) } @@ -880,6 +888,7 @@ impl TreeState { directory_to_visit: DirectoryToVisit, progress: Option<&SnapshotProgress>, max_new_file_size: u64, + files_to_big: Sender, ) -> Result<(), SnapshotError> { let DirectoryToVisit { dir, @@ -989,6 +998,7 @@ impl TreeState { directory_to_visit, progress, max_new_file_size, + files_to_big.clone(), )?; } } else if matcher.matches(&path) { @@ -1008,11 +1018,13 @@ impl TreeState { })?; if maybe_current_file_state.is_none() && metadata.len() > max_new_file_size { - return Err(SnapshotError::NewFileTooLarge { - path: entry.path().clone(), - size: HumanByteSize(metadata.len()), - max_size: HumanByteSize(max_new_file_size), - }); + files_to_big + .send(NewFileTooLarge { + path: entry.path().clone(), + size: HumanByteSize(metadata.len()), + max_size: HumanByteSize(max_new_file_size), + }) + .ok(); } if let Some(new_file_state) = file_state(&metadata) { present_files_tx.send(path.clone()).ok(); diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index cb52c0e27d7..548b13ca5a2 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -152,17 +152,10 @@ pub enum SnapshotError { /// Reading or writing from the commit backend failed. #[error(transparent)] BackendError(#[from] BackendError), - /// A file was larger than the specified maximum file size for new + #[error("New file too large")] + /// Files were larger than the specified maximum file size for new /// (previously untracked) files. - #[error("New file {path} of size ~{size} exceeds snapshot.max-new-file-size ({max_size})")] - NewFileTooLarge { - /// The path of the large file. - path: PathBuf, - /// The size of the large file. - size: HumanByteSize, - /// The maximum allowed size. - max_size: HumanByteSize, - }, + NewFileTooLarge(Vec), /// Checking path with ignore patterns failed. #[error(transparent)] GitIgnoreError(#[from] GitIgnoreError), @@ -177,6 +170,19 @@ pub enum SnapshotError { }, } +#[derive(Debug, Error)] +/// A file was larger than the specified maximum file size for new +/// (previously untracked) files. +#[error("New file {path} of size ~{size} exceeds snapshot.max-new-file-size ({max_size})")] +pub struct NewFileTooLarge { + /// The path of the large file. + pub path: PathBuf, + /// The size of the large file. + pub size: HumanByteSize, + /// The maximum allowed size. + pub max_size: HumanByteSize, +} + /// Options used when snapshotting the working copy. Some of them may be ignored /// by some `WorkingCopy` implementations. pub struct SnapshotOptions<'a> {