diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index ae2c52c77..f7573b2f7 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -45,11 +45,10 @@ mod resolve; mod restore; mod run; mod show; +mod sparse; -use std::collections::HashSet; use std::fmt::Debug; -use std::io::{BufRead, Seek, SeekFrom, Write}; -use std::path::Path; +use std::io::Write; use std::{fmt, fs, io}; use clap::parser::ValueSource; @@ -62,7 +61,6 @@ use jj_lib::merge::Merge; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; use jj_lib::op_store::WorkspaceId; use jj_lib::repo::{ReadonlyRepo, Repo}; -use jj_lib::repo_path::RepoPath; use jj_lib::rewrite::{merge_commit_trees, DescendantRebaser}; use jj_lib::settings::UserSettings; use jj_lib::working_copy::SnapshotOptions; @@ -135,7 +133,7 @@ enum Commands { Run(run::RunArgs), Show(show::ShowArgs), #[command(subcommand)] - Sparse(SparseArgs), + Sparse(sparse::SparseArgs), Split(SplitArgs), Squash(SquashArgs), Status(StatusArgs), @@ -309,46 +307,6 @@ struct WorkspaceRootArgs {} #[derive(clap::Args, Clone, Debug)] struct WorkspaceUpdateStaleArgs {} -/// Manage which paths from the working-copy commit are present in the working -/// copy -#[derive(Subcommand, Clone, Debug)] -enum SparseArgs { - List(SparseListArgs), - Set(SparseSetArgs), -} - -/// List the patterns that are currently present in the working copy -/// -/// By default, a newly cloned or initialized repo will have have a pattern -/// matching all files from the repo root. That pattern is rendered as `.` (a -/// single period). -#[derive(clap::Args, Clone, Debug)] -struct SparseListArgs {} - -/// Update the patterns that are present in the working copy -/// -/// For example, if all you need is the `README.md` and the `lib/` -/// directory, use `jj sparse set --clear --add README.md --add lib`. -/// If you no longer need the `lib` directory, use `jj sparse set --remove lib`. -#[derive(clap::Args, Clone, Debug)] -struct SparseSetArgs { - /// Patterns to add to the working copy - #[arg(long, value_hint = clap::ValueHint::AnyPath)] - add: Vec, - /// Patterns to remove from the working copy - #[arg(long, conflicts_with = "clear", value_hint = clap::ValueHint::AnyPath)] - remove: Vec, - /// Include no files in the working copy (combine with --add) - #[arg(long)] - clear: bool, - /// Edit patterns with $EDITOR - #[arg(long)] - edit: bool, - /// Include all files in the working copy - #[arg(long, conflicts_with_all = &["add", "remove", "clear"])] - reset: bool, -} - /// Infrequently used commands such as for generating shell completions #[derive(Subcommand, Clone, Debug)] enum UtilCommands { @@ -660,73 +618,6 @@ fn edit_description( Ok(text_util::complete_newline(description.trim_matches('\n'))) } -fn edit_sparse( - workspace_root: &Path, - repo_path: &Path, - sparse: &[RepoPath], - settings: &UserSettings, -) -> Result, CommandError> { - let file = (|| -> Result<_, io::Error> { - let mut file = tempfile::Builder::new() - .prefix("editor-") - .suffix(".jjsparse") - .tempfile_in(repo_path)?; - for sparse_path in sparse { - let workspace_relative_sparse_path = - file_util::relative_path(workspace_root, &sparse_path.to_fs_path(workspace_root)); - file.write_all( - workspace_relative_sparse_path - .to_str() - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - format!( - "stored sparse path is not valid utf-8: {}", - workspace_relative_sparse_path.display() - ), - ) - })? - .as_bytes(), - )?; - file.write_all(b"\n")?; - } - file.seek(SeekFrom::Start(0))?; - Ok(file) - })() - .map_err(|e| { - user_error(format!( - r#"Failed to create sparse patterns file in "{path}": {e}"#, - path = repo_path.display() - )) - })?; - let file_path = file.path().to_owned(); - - run_ui_editor(settings, &file_path)?; - - // Read and parse patterns. - io::BufReader::new(file) - .lines() - .filter(|line| { - line.as_ref() - .map(|line| !line.starts_with("JJ: ") && !line.trim().is_empty()) - .unwrap_or(true) - }) - .map(|line| { - let line = line.map_err(|e| { - user_error(format!( - r#"Failed to read sparse patterns file "{path}": {e}"#, - path = file_path.display() - )) - })?; - Ok::<_, CommandError>(RepoPath::parse_fs_path( - workspace_root, - workspace_root, - line.trim(), - )?) - }) - .try_collect() -} - fn combine_messages( repo: &ReadonlyRepo, source: &Commit, @@ -1377,92 +1268,6 @@ fn cmd_workspace_update_stale( Ok(()) } -#[instrument(skip_all)] -fn cmd_sparse(ui: &mut Ui, command: &CommandHelper, args: &SparseArgs) -> Result<(), CommandError> { - match args { - SparseArgs::List(sub_args) => cmd_sparse_list(ui, command, sub_args), - SparseArgs::Set(sub_args) => cmd_sparse_set(ui, command, sub_args), - } -} - -#[instrument(skip_all)] -fn cmd_sparse_list( - ui: &mut Ui, - command: &CommandHelper, - _args: &SparseListArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - for path in workspace_command.working_copy().sparse_patterns()? { - let ui_path = workspace_command.format_file_path(path); - writeln!(ui.stdout(), "{ui_path}")?; - } - Ok(()) -} - -#[instrument(skip_all)] -fn cmd_sparse_set( - ui: &mut Ui, - command: &CommandHelper, - args: &SparseSetArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let paths_to_add: Vec<_> = args - .add - .iter() - .map(|v| workspace_command.parse_file_path(v)) - .try_collect()?; - let paths_to_remove: Vec<_> = args - .remove - .iter() - .map(|v| workspace_command.parse_file_path(v)) - .try_collect()?; - // Determine inputs of `edit` operation now, since `workspace_command` is - // inaccessible while the working copy is locked. - let edit_inputs = args.edit.then(|| { - ( - workspace_command.repo().clone(), - workspace_command.workspace_root().clone(), - ) - }); - let (mut locked_ws, wc_commit) = workspace_command.start_working_copy_mutation()?; - let mut new_patterns = HashSet::new(); - if args.reset { - new_patterns.insert(RepoPath::root()); - } else { - if !args.clear { - new_patterns.extend(locked_ws.locked_wc().sparse_patterns()?.iter().cloned()); - for path in paths_to_remove { - new_patterns.remove(&path); - } - } - for path in paths_to_add { - new_patterns.insert(path); - } - } - let mut new_patterns = new_patterns.into_iter().collect_vec(); - new_patterns.sort(); - if let Some((repo, workspace_root)) = edit_inputs { - new_patterns = edit_sparse( - &workspace_root, - repo.repo_path(), - &new_patterns, - command.settings(), - )?; - new_patterns.sort(); - } - let stats = locked_ws - .locked_wc() - .set_sparse_patterns(new_patterns) - .map_err(|err| { - CommandError::InternalError(format!("Failed to update working copy paths: {err}")) - })?; - let operation_id = locked_ws.locked_wc().old_operation_id().clone(); - locked_ws.finish(operation_id)?; - print_checkout_stats(ui, stats, &wc_commit)?; - - Ok(()) -} - pub fn default_app() -> Command { Commands::augment_subcommands(Args::command()) } @@ -1508,7 +1313,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Commands::Undo(sub_args) => operation::cmd_op_undo(ui, command_helper, sub_args), Commands::Operation(sub_args) => operation::cmd_operation(ui, command_helper, sub_args), Commands::Workspace(sub_args) => cmd_workspace(ui, command_helper, sub_args), - Commands::Sparse(sub_args) => cmd_sparse(ui, command_helper, sub_args), + Commands::Sparse(sub_args) => sparse::cmd_sparse(ui, command_helper, sub_args), Commands::Chmod(sub_args) => chmod::cmd_chmod(ui, command_helper, sub_args), Commands::Git(sub_args) => git::cmd_git(ui, command_helper, sub_args), Commands::Util(sub_args) => cmd_util(ui, command_helper, sub_args), diff --git a/cli/src/commands/sparse.rs b/cli/src/commands/sparse.rs new file mode 100644 index 000000000..37787756c --- /dev/null +++ b/cli/src/commands/sparse.rs @@ -0,0 +1,226 @@ +// Copyright 2020 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::collections::HashSet; +use std::io::{self, BufRead, Seek, SeekFrom, Write}; +use std::path::Path; + +use clap::Subcommand; +use itertools::Itertools; +use jj_lib::file_util; +use jj_lib::repo_path::RepoPath; +use jj_lib::settings::UserSettings; +use tracing::instrument; + +use crate::cli_util::{ + print_checkout_stats, run_ui_editor, user_error, CommandError, CommandHelper, +}; +use crate::ui::Ui; + +/// Manage which paths from the working-copy commit are present in the working +/// copy +#[derive(Subcommand, Clone, Debug)] +pub(crate) enum SparseArgs { + List(SparseListArgs), + Set(SparseSetArgs), +} + +/// List the patterns that are currently present in the working copy +/// +/// By default, a newly cloned or initialized repo will have have a pattern +/// matching all files from the repo root. That pattern is rendered as `.` (a +/// single period). +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct SparseListArgs {} + +/// Update the patterns that are present in the working copy +/// +/// For example, if all you need is the `README.md` and the `lib/` +/// directory, use `jj sparse set --clear --add README.md --add lib`. +/// If you no longer need the `lib` directory, use `jj sparse set --remove lib`. +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct SparseSetArgs { + /// Patterns to add to the working copy + #[arg(long, value_hint = clap::ValueHint::AnyPath)] + add: Vec, + /// Patterns to remove from the working copy + #[arg(long, conflicts_with = "clear", value_hint = clap::ValueHint::AnyPath)] + remove: Vec, + /// Include no files in the working copy (combine with --add) + #[arg(long)] + clear: bool, + /// Edit patterns with $EDITOR + #[arg(long)] + edit: bool, + /// Include all files in the working copy + #[arg(long, conflicts_with_all = &["add", "remove", "clear"])] + reset: bool, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_sparse( + ui: &mut Ui, + command: &CommandHelper, + args: &SparseArgs, +) -> Result<(), CommandError> { + match args { + SparseArgs::List(sub_args) => cmd_sparse_list(ui, command, sub_args), + SparseArgs::Set(sub_args) => cmd_sparse_set(ui, command, sub_args), + } +} + +#[instrument(skip_all)] +fn cmd_sparse_list( + ui: &mut Ui, + command: &CommandHelper, + _args: &SparseListArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + for path in workspace_command.working_copy().sparse_patterns()? { + let ui_path = workspace_command.format_file_path(path); + writeln!(ui.stdout(), "{ui_path}")?; + } + Ok(()) +} + +#[instrument(skip_all)] +fn cmd_sparse_set( + ui: &mut Ui, + command: &CommandHelper, + args: &SparseSetArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let paths_to_add: Vec<_> = args + .add + .iter() + .map(|v| workspace_command.parse_file_path(v)) + .try_collect()?; + let paths_to_remove: Vec<_> = args + .remove + .iter() + .map(|v| workspace_command.parse_file_path(v)) + .try_collect()?; + // Determine inputs of `edit` operation now, since `workspace_command` is + // inaccessible while the working copy is locked. + let edit_inputs = args.edit.then(|| { + ( + workspace_command.repo().clone(), + workspace_command.workspace_root().clone(), + ) + }); + let (mut locked_ws, wc_commit) = workspace_command.start_working_copy_mutation()?; + let mut new_patterns = HashSet::new(); + if args.reset { + new_patterns.insert(RepoPath::root()); + } else { + if !args.clear { + new_patterns.extend(locked_ws.locked_wc().sparse_patterns()?.iter().cloned()); + for path in paths_to_remove { + new_patterns.remove(&path); + } + } + for path in paths_to_add { + new_patterns.insert(path); + } + } + let mut new_patterns = new_patterns.into_iter().collect_vec(); + new_patterns.sort(); + if let Some((repo, workspace_root)) = edit_inputs { + new_patterns = edit_sparse( + &workspace_root, + repo.repo_path(), + &new_patterns, + command.settings(), + )?; + new_patterns.sort(); + } + let stats = locked_ws + .locked_wc() + .set_sparse_patterns(new_patterns) + .map_err(|err| { + CommandError::InternalError(format!("Failed to update working copy paths: {err}")) + })?; + let operation_id = locked_ws.locked_wc().old_operation_id().clone(); + locked_ws.finish(operation_id)?; + print_checkout_stats(ui, stats, &wc_commit)?; + + Ok(()) +} + +fn edit_sparse( + workspace_root: &Path, + repo_path: &Path, + sparse: &[RepoPath], + settings: &UserSettings, +) -> Result, CommandError> { + let file = (|| -> Result<_, io::Error> { + let mut file = tempfile::Builder::new() + .prefix("editor-") + .suffix(".jjsparse") + .tempfile_in(repo_path)?; + for sparse_path in sparse { + let workspace_relative_sparse_path = + file_util::relative_path(workspace_root, &sparse_path.to_fs_path(workspace_root)); + file.write_all( + workspace_relative_sparse_path + .to_str() + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "stored sparse path is not valid utf-8: {}", + workspace_relative_sparse_path.display() + ), + ) + })? + .as_bytes(), + )?; + file.write_all(b"\n")?; + } + file.seek(SeekFrom::Start(0))?; + Ok(file) + })() + .map_err(|e| { + user_error(format!( + r#"Failed to create sparse patterns file in "{path}": {e}"#, + path = repo_path.display() + )) + })?; + let file_path = file.path().to_owned(); + + run_ui_editor(settings, &file_path)?; + + // Read and parse patterns. + io::BufReader::new(file) + .lines() + .filter(|line| { + line.as_ref() + .map(|line| !line.starts_with("JJ: ") && !line.trim().is_empty()) + .unwrap_or(true) + }) + .map(|line| { + let line = line.map_err(|e| { + user_error(format!( + r#"Failed to read sparse patterns file "{path}": {e}"#, + path = file_path.display() + )) + })?; + Ok::<_, CommandError>(RepoPath::parse_fs_path( + workspace_root, + workspace_root, + line.trim(), + )?) + }) + .try_collect() +}