Skip to content

Commit

Permalink
Implement hacky variant of jj purge
Browse files Browse the repository at this point in the history
  • Loading branch information
0xdeafbeef committed Aug 10, 2024
1 parent 304f6df commit 3154e5e
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 11 deletions.
3 changes: 3 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod obslog;
mod operation;
mod parallelize;
mod prev;
mod purge;
mod rebase;
mod resolve;
mod restore;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
71 changes: 71 additions & 0 deletions cli/src/commands/purge.rs
Original file line number Diff line number Diff line change
@@ -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::<SnapshotError>();
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(())
}
16 changes: 16 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 45 additions & 11 deletions lib/src/local_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -758,7 +758,7 @@ impl TreeState {
/// Look for changes to the working copy. If there are any changes, create
/// a new tree from it and return it, and also update the dirstate on disk.
#[instrument(skip_all)]
pub fn snapshot(&mut self, options: SnapshotOptions) -> Result<bool, SnapshotError> {
pub fn snapshot(&mut self, options: SnapshotOptions) -> Result<SnapshotStats, SnapshotError> {
let SnapshotOptions {
base_ignores,
fsmonitor_settings,
Expand All @@ -783,13 +783,15 @@ impl TreeState {
if matcher.visit(RepoPath::root()).is_nothing() {
// No need to iterate file states to build empty deleted_files.
self.watchman_clock = watchman_clock;
return Ok(is_dirty);
return Ok(SnapshotStats::with_status(is_dirty));
}

let (tree_entries_tx, tree_entries_rx) = channel();
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 {
Expand All @@ -807,6 +809,7 @@ impl TreeState {
directory_to_visit,
progress,
max_new_file_size,
files_to_big_tx,
)
})?;

Expand Down Expand Up @@ -865,8 +868,13 @@ impl TreeState {
let state_paths: HashSet<_> = file_states.paths().map(|path| path.to_owned()).collect();
assert_eq!(state_paths, tree_paths);
}
let files_to_large: Vec<_> = files_to_big_rx.iter().collect();

self.watchman_clock = watchman_clock;
Ok(is_dirty)
Ok(SnapshotStats {
success: is_dirty,
files_to_large,
})
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -880,6 +888,7 @@ impl TreeState {
directory_to_visit: DirectoryToVisit,
progress: Option<&SnapshotProgress>,
max_new_file_size: u64,
files_to_big: Sender<NewFileTooLarge>,
) -> Result<(), SnapshotError> {
let DirectoryToVisit {
dir,
Expand Down Expand Up @@ -989,6 +998,7 @@ impl TreeState {
directory_to_visit,
progress,
max_new_file_size,
files_to_big.clone(),
)?;
}
} else if matcher.matches(&path) {
Expand All @@ -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();
Expand Down Expand Up @@ -1498,6 +1510,28 @@ impl TreeState {
}
}

pub struct SnapshotStats {
files_to_large: Vec<NewFileTooLarge>,
success: bool,
}

impl SnapshotStats {
fn with_status(success: bool) -> Self {
SnapshotStats {
files_to_large: Vec::new(),
success,
}
}

pub fn success(&self) -> bool {
self.success
}

pub fn files_to_large(&self) -> &[NewFileTooLarge] {
&self.files_to_large
}
}

fn checkout_error_for_stat_error(err: std::io::Error, path: &Path) -> CheckoutError {
CheckoutError::Other {
message: format!("Failed to stat file {}", path.display()),
Expand Down Expand Up @@ -1797,7 +1831,7 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy {
message: "Failed to read the working copy state".to_string(),
err: err.into(),
})?;
self.tree_state_dirty |= tree_state.snapshot(options)?;
self.tree_state_dirty |= tree_state.snapshot(options)?.success();
Ok(tree_state.current_tree_id().clone())
}

Expand Down
13 changes: 13 additions & 0 deletions lib/src/working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,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> {
Expand Down

0 comments on commit 3154e5e

Please sign in to comment.