diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c40cd3723..3a1f263f7ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New features +* The new config option `snapshot.auto-track` lets you automatically track only + the specified paths (all paths by default). Use the new `jj track` command to + manually tracks path that were not automatically tracked. + [#323](https://github.com/martinvonz/jj/issues/323) + * Add new boolean config knob, `ui.movement.edit` for controlling the behaviour of `prev/next`. The flag turns `edit` mode `on` and `off` permanently when set respectively to `true` or `false`. diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index d46d22e5fd4..1782d842a2b 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -856,6 +856,18 @@ impl WorkspaceCommandHelper { Ok(FilesetExpression::union_all(expressions)) } + pub fn auto_tracking_matcher(&self) -> Result, CommandError> { + let pattern = self.settings.config().get_string("snapshot.auto-track")?; + let expression = fileset::parse( + &pattern, + &RepoPathUiConverter::Fs { + cwd: "".into(), + base: "".into(), + }, + )?; + Ok(expression.to_matcher()) + } + pub(crate) fn path_converter(&self) -> &RepoPathUiConverter { &self.path_converter } @@ -1289,6 +1301,7 @@ impl WorkspaceCommandHelper { return Ok(()); }; let base_ignores = self.base_ignores()?; + let auto_tracking_matcher = self.auto_tracking_matcher()?; // Compare working-copy tree and operation with repo's, and reload as needed. let mut locked_ws = self.workspace.start_working_copy_mutation()?; @@ -1341,6 +1354,7 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin base_ignores, fsmonitor_settings: self.settings.fsmonitor_settings()?, progress: progress.as_ref().map(|x| x as _), + start_tracking_matcher: &auto_tracking_matcher, max_new_file_size: self.settings.max_new_file_size()?, })?; drop(progress); diff --git a/cli/src/commands/file/mod.rs b/cli/src/commands/file/mod.rs index edd43abbeb6..173e0cc5a1d 100644 --- a/cli/src/commands/file/mod.rs +++ b/cli/src/commands/file/mod.rs @@ -15,6 +15,7 @@ pub mod chmod; pub mod list; pub mod show; +pub mod track; pub mod untrack; use crate::cli_util::CommandHelper; @@ -27,6 +28,7 @@ pub enum FileCommand { Chmod(chmod::FileChmodArgs), List(list::FileListArgs), Show(show::FileShowArgs), + Track(track::TrackArgs), Untrack(untrack::FileUntrackArgs), } @@ -39,6 +41,7 @@ pub fn cmd_file( FileCommand::Chmod(args) => chmod::cmd_file_chmod(ui, command, args), FileCommand::List(args) => list::cmd_file_list(ui, command, args), FileCommand::Show(args) => show::cmd_file_show(ui, command, args), + FileCommand::Track(args) => track::cmd_file_track(ui, command, args), FileCommand::Untrack(args) => untrack::cmd_file_untrack(ui, command, args), } } diff --git a/cli/src/commands/file/track.rs b/cli/src/commands/file/track.rs new file mode 100644 index 00000000000..88908a33196 --- /dev/null +++ b/cli/src/commands/file/track.rs @@ -0,0 +1,67 @@ +// 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::io::Write; + +use jj_lib::working_copy::SnapshotOptions; +use tracing::instrument; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Start tracking specified paths in the working copy +/// +/// Without arguments, all paths that are not ignored will be tracked. +/// +/// By default, Jujutsu starts tracking all new files automatically. This +/// command is not useful then. You can configure which paths to automatically +/// track by setting e.g. `snapshot.auto-track = 'none()'`. You will then need +/// to run this command to start tracking new files. +#[derive(clap::Args, Clone, Debug)] +pub(crate) struct TrackArgs { + /// Paths to track + #[arg(value_hint = clap::ValueHint::AnyPath)] + paths: Vec, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_file_track( + ui: &mut Ui, + command: &CommandHelper, + args: &TrackArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let matcher = workspace_command + .parse_file_patterns(&args.paths)? + .to_matcher(); + + let mut tx = workspace_command.start_transaction().into_inner(); + let base_ignores = workspace_command.base_ignores()?; + let (mut locked_ws, _wc_commit) = workspace_command.start_working_copy_mutation()?; + locked_ws.locked_wc().snapshot(SnapshotOptions { + base_ignores, + fsmonitor_settings: command.settings().fsmonitor_settings()?, + progress: None, + start_tracking_matcher: &matcher, + max_new_file_size: command.settings().max_new_file_size()?, + })?; + let num_rebased = tx.mut_repo().rebase_descendants(command.settings())?; + if num_rebased > 0 { + writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?; + } + let repo = tx.commit("track paths"); + locked_ws.finish(repo.op_id().clone())?; + Ok(()) +} diff --git a/cli/src/commands/file/untrack.rs b/cli/src/commands/file/untrack.rs index 25b84544bc1..7a4fda2f3c3 100644 --- a/cli/src/commands/file/untrack.rs +++ b/cli/src/commands/file/untrack.rs @@ -51,6 +51,7 @@ pub(crate) fn cmd_file_untrack( let mut tx = workspace_command.start_transaction().into_inner(); let base_ignores = workspace_command.base_ignores()?; + let auto_tracking_matcher = workspace_command.auto_tracking_matcher()?; let (mut locked_ws, wc_commit) = workspace_command.start_working_copy_mutation()?; // Create a new tree without the unwanted files let mut tree_builder = MergedTreeBuilder::new(wc_commit.tree_id().clone()); @@ -72,6 +73,7 @@ pub(crate) fn cmd_file_untrack( base_ignores, fsmonitor_settings: command.settings().fsmonitor_settings()?, progress: None, + start_tracking_matcher: &auto_tracking_matcher, max_new_file_size: command.settings().max_new_file_size()?, })?; if wc_tree_id != *new_commit.tree_id() { diff --git a/cli/src/config/misc.toml b/cli/src/config/misc.toml index a2a7f321a04..23a40f03591 100644 --- a/cli/src/config/misc.toml +++ b/cli/src/config/misc.toml @@ -23,3 +23,4 @@ edit = false [snapshot] max-new-file-size = "1MiB" +auto-track = "all()" diff --git a/cli/src/merge_tools/diff_working_copies.rs b/cli/src/merge_tools/diff_working_copies.rs index 0845ce2366a..6322f4d84db 100644 --- a/cli/src/merge_tools/diff_working_copies.rs +++ b/cli/src/merge_tools/diff_working_copies.rs @@ -12,6 +12,7 @@ use jj_lib::fsmonitor::FsmonitorSettings; use jj_lib::gitignore::GitIgnoreFile; use jj_lib::local_working_copy::TreeState; use jj_lib::local_working_copy::TreeStateError; +use jj_lib::matchers::EverythingMatcher; use jj_lib::matchers::Matcher; use jj_lib::merged_tree::MergedTree; use jj_lib::merged_tree::TreeDiffEntry; @@ -286,6 +287,7 @@ diff editing in mind and be a little inaccurate. base_ignores, fsmonitor_settings: FsmonitorSettings::None, progress: None, + start_tracking_matcher: &EverythingMatcher, max_new_file_size: u64::MAX, })?; Ok(output_tree_state.current_tree_id().clone()) diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index a9eba717f55..982cff171ea 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -39,6 +39,7 @@ This document contains the help content for the `jj` command-line program. * [`jj file chmod`↴](#jj-file-chmod) * [`jj file list`↴](#jj-file-list) * [`jj file show`↴](#jj-file-show) +* [`jj file track`↴](#jj-file-track) * [`jj file untrack`↴](#jj-file-untrack) * [`jj fix`↴](#jj-fix) * [`jj git`↴](#jj-git) @@ -703,6 +704,7 @@ File operations * `chmod` — Sets or removes the executable bit for paths in the repo * `list` — List files in a revision * `show` — Print contents of files in a revision +* `track` — Start tracking specified paths in the working copy * `untrack` — Stop tracking specified paths in the working copy @@ -773,6 +775,22 @@ If the given path is a directory, files in the directory will be visited recursi +## `jj file track` + +Start tracking specified paths in the working copy + +Without arguments, all paths that are not ignored will be tracked. + +By default, Jujutsu starts tracking all new files automatically. This command is not useful then. You can configure which paths to automatically track by setting e.g. `snapshot.auto-track = 'none()'`. You will then need to run this command to start tracking new files. + +**Usage:** `jj file track [PATHS]...` + +###### **Arguments:** + +* `` — Paths to track + + + ## `jj file untrack` Stop tracking specified paths in the working copy diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs index b1fcc5b3a50..a5320da4939 100644 --- a/cli/tests/runner.rs +++ b/cli/tests/runner.rs @@ -29,7 +29,7 @@ mod test_duplicate_command; mod test_edit_command; mod test_file_chmod_command; mod test_file_print_command; -mod test_file_untrack_command; +mod test_file_track_untrack_commands; mod test_fix_command; mod test_generate_md_cli_help; mod test_git_clone; diff --git a/cli/tests/test_file_untrack_command.rs b/cli/tests/test_file_track_untrack_commands.rs similarity index 66% rename from cli/tests/test_file_untrack_command.rs rename to cli/tests/test_file_track_untrack_commands.rs index 3d72179616e..17cf7f3cf6a 100644 --- a/cli/tests/test_file_untrack_command.rs +++ b/cli/tests/test_file_track_untrack_commands.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; use crate::common::TestEnvironment; #[test] -fn test_untrack() { +fn test_track_untrack() { let test_env = TestEnvironment::default(); test_env.add_config(r#"ui.allow-init-native = true"#); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo"]); @@ -103,7 +103,7 @@ fn test_untrack() { } #[test] -fn test_untrack_sparse() { +fn test_track_untrack_sparse() { let test_env = TestEnvironment::default(); test_env.add_config(r#"ui.allow-init-native = true"#); test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo"]); @@ -128,4 +128,79 @@ fn test_untrack_sparse() { insta::assert_snapshot!(stdout, @r###" file1 "###); + // Trying to manually track a file that's not included in the sparse working has + // no effect. TODO: At least a warning would be useful + let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["file", "track", "file2"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @""); + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + file1 + "###); +} + +#[test] +fn test_auto_track() { + let test_env = TestEnvironment::default(); + test_env.add_config(r#"snapshot.auto-track = 'glob:*.rs'"#); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + std::fs::write(repo_path.join("file1.rs"), "initial").unwrap(); + std::fs::write(repo_path.join("file2.md"), "initial").unwrap(); + std::fs::write(repo_path.join("file3.md"), "initial").unwrap(); + + // Only configured paths get auto-tracked + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + file1.rs + "###); + + // Can manually track paths + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "track", "file3.md"]); + insta::assert_snapshot!(stdout, @""); + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + file1.rs + file3.md + "###); + + // Defaults to tracking all paths + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "track"]); + insta::assert_snapshot!(stdout, @""); + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + file1.rs + file2.md + file3.md + "###); + + // Can manually untrack paths + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "untrack", "file2.md"]); + insta::assert_snapshot!(stdout, @""); + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + file1.rs + file3.md + "###); + + // CWD-relative paths in `snapshot.auto-track` are evaluated from the repo root + let subdir = repo_path.join("sub"); + std::fs::create_dir(&subdir).unwrap(); + std::fs::write(subdir.join("file1.rs"), "initial").unwrap(); + let stdout = test_env.jj_cmd_success(&subdir, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + ../file1.rs + ../file3.md + "###); + + // But `jj track` wants CWD-relative paths + let stdout = test_env.jj_cmd_success(&subdir, &["file", "track", "file1.rs"]); + insta::assert_snapshot!(stdout, @""); + let stdout = test_env.jj_cmd_success(&subdir, &["file", "list"]); + insta::assert_snapshot!(stdout, @r###" + ../file1.rs + ../file3.md + file1.rs + "###); } diff --git a/cli/tests/test_working_copy.rs b/cli/tests/test_working_copy.rs index 55a859ca991..d826638858a 100644 --- a/cli/tests/test_working_copy.rs +++ b/cli/tests/test_working_copy.rs @@ -51,4 +51,9 @@ fn test_snapshot_large_file() { - Run `jj --config-toml 'snapshot.max-new-file-size=11264' st` This will increase the maximum file size allowed for new files, for this command only. "###); + + // No error if we disable auto-tracking of the path + test_env.add_config(r#"snapshot.auto-track = 'none()'"#); + let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]); + insta::assert_snapshot!(stdout, @""); } diff --git a/docs/FAQ.md b/docs/FAQ.md index 07f5b72d31f..b0860e6c294 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -63,8 +63,9 @@ easy way to see the evolution of the commit's contents. ### Can I prevent Jujutsu from recording my unfinished work? I'm not ready to commit it. -Jujutsu automatically records new files in the current working-copy commit and -doesn't provide a way to prevent that. +You can set `snapshot.auto-track` to only start tracking new files matching the +configured pattern (e.g. `"none()"`). Changes to already tracked files will +still be snapshotted by every command. However, you can easily record intermediate drafts of your work. If you think you might want to go back to the current state of the working-copy commit, diff --git a/docs/working-copy.md b/docs/working-copy.md index 1fbf2ad5e75..6ceca69deea 100644 --- a/docs/working-copy.md +++ b/docs/working-copy.md @@ -12,12 +12,16 @@ working-copy contents when they have changed. Most `jj` commands you run will commit the working-copy changes if they have changed. The resulting revision will replace the previous working-copy revision. -Also unlike most other VCSs, added files are implicitly tracked. That means that -if you add a new file to the working copy, it will be automatically committed -once you run e.g. `jj st`. Similarly, if you remove a file from the working -copy, it will implicitly be untracked. To untrack a file while keeping it in -the working copy, first make sure it's [ignored](#ignored-files) and then run -`jj file untrack `. +Also unlike most other VCSs, added files are implicitly tracked by default. That +means that if you add a new file to the working copy, it will be automatically +committed once you run e.g. `jj st`. Similarly, if you remove a file from the +working copy, it will implicitly be untracked. The `snapshot.auto-track` config +option controls which paths get automatically tracked when they're added. See +the [fileset documentation](filesets.md) for the syntax. + +You can use `jj untrack` to untrack a file while keeping it in the working copy. +However, first [ignore](#ignored-files) them or remove them from the +`snapshot.auto-track` patterns; otherwise they will be immediately tracked again. ## Conflicts diff --git a/lib/src/local_working_copy.rs b/lib/src/local_working_copy.rs index d01b547f2b9..e9f94db35ba 100644 --- a/lib/src/local_working_copy.rs +++ b/lib/src/local_working_copy.rs @@ -798,6 +798,7 @@ impl TreeState { base_ignores, fsmonitor_settings, progress, + start_tracking_matcher, max_new_file_size, } = options; @@ -835,6 +836,7 @@ impl TreeState { }; self.visit_directory( &matcher, + start_tracking_matcher, ¤t_tree, tree_entries_tx, file_states_tx, @@ -908,6 +910,7 @@ impl TreeState { fn visit_directory( &self, matcher: &dyn Matcher, + start_tracking_matcher: &dyn Matcher, current_tree: &MergedTree, tree_entries_tx: Sender<(RepoPathBuf, MergedTreeValue)>, file_states_tx: Sender<(RepoPathBuf, FileState)>, @@ -1008,6 +1011,14 @@ impl TreeState { } } } + } else if start_tracking_matcher.visit(&path).is_nothing() + && current_tree.path_value(&path)?.is_absent() + { + // Don't visit subdirectory if it's not already tracked + // and we should not start tracking it. The user might + // have a huge target/ directory that they have not yet + // added to their ignore patterns. + // TODO: Report this directory to the caller } else { let directory_to_visit = DirectoryToVisit { dir: path, @@ -1017,6 +1028,7 @@ impl TreeState { }; self.visit_directory( matcher, + start_tracking_matcher, current_tree, tree_entries_tx.clone(), file_states_tx.clone(), @@ -1034,15 +1046,22 @@ impl TreeState { && git_ignore.matches(path.as_internal_file_string()) { // If it wasn't already tracked and it matches - // the ignored paths, then - // ignore it. + // the ignored paths, then ignore it. } else { + if maybe_current_file_state.is_none() + && !start_tracking_matcher.matches(&path) + { + // Leave the file untracked + // TODO: Report this path to the caller + return Ok(()); + } let metadata = entry.metadata().map_err(|err| SnapshotError::Other { message: format!("Failed to stat file {}", entry.path().display()), err: err.into(), })?; if maybe_current_file_state.is_none() && metadata.len() > max_new_file_size { + // TODO: Maybe leave the file untracked instead return Err(SnapshotError::NewFileTooLarge { path: entry.path().clone(), size: HumanByteSize(metadata.len()), diff --git a/lib/src/working_copy.rs b/lib/src/working_copy.rs index 1ea3107d261..944804b0de0 100644 --- a/lib/src/working_copy.rs +++ b/lib/src/working_copy.rs @@ -28,6 +28,8 @@ use crate::commit::Commit; use crate::fsmonitor::FsmonitorSettings; use crate::gitignore::GitIgnoreError; use crate::gitignore::GitIgnoreFile; +use crate::matchers::EverythingMatcher; +use crate::matchers::Matcher; use crate::op_store::OperationId; use crate::op_store::WorkspaceId; use crate::repo_path::RepoPath; @@ -193,6 +195,9 @@ pub struct SnapshotOptions<'a> { pub fsmonitor_settings: FsmonitorSettings, /// A callback for the UI to display progress. pub progress: Option<&'a SnapshotProgress<'a>>, + /// For new files that are not already tracked, start tracking them if they + /// match this. + pub start_tracking_matcher: &'a dyn Matcher, /// The size of the largest file that should be allowed to become tracked /// (already tracked files are always snapshotted). If there are larger /// files in the working copy, then `LockedWorkingCopy::snapshot()` may @@ -208,6 +213,7 @@ impl SnapshotOptions<'_> { base_ignores: GitIgnoreFile::empty(), fsmonitor_settings: FsmonitorSettings::None, progress: None, + start_tracking_matcher: &EverythingMatcher, max_new_file_size: u64::MAX, } }