Skip to content

Commit

Permalink
cli: make paths to auto-track configurable, add jj track
Browse files Browse the repository at this point in the history
It's a pretty frequent request to have support for turning off
auto-tracking of new files and to have a command to manually track
them instead. This patch adds a `snapshot.auto-track` config to decide
which paths to auto-track (defaults to `all()`). It also adds a `jj
track` command to manually track the untracked paths.

This patch does not include displaying the untracked paths in `jj
status`, so for now this is probably only useful in colocated repos
where you can run `git status` to find the untracked files.

#323
  • Loading branch information
martinvonz committed Sep 7, 2024
1 parent 4730755 commit 2817cbc
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 12 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ 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 file track`
command to manually tracks path that were not automatically tracked. There is
no way to list untracked files yet. Use `git status` in a colocated workspace
as a workaround.
[#323](https://github.com/martinvonz/jj/issues/323)

* `jj fix` now allows fixing unchanged files with the `--include-unchanged-files` flag. This
can be used to more easily introduce automatic formatting changes in a new
commit separate from other changes.
Expand Down
14 changes: 14 additions & 0 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,18 @@ impl WorkspaceCommandHelper {
Ok(FilesetExpression::union_all(expressions))
}

pub fn auto_tracking_matcher(&self) -> Result<Box<dyn Matcher>, 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
}
Expand Down Expand Up @@ -1316,6 +1328,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 fsmonitor_settings = self.settings().fsmonitor_settings()?;
Expand Down Expand Up @@ -1371,6 +1384,7 @@ See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
base_ignores,
fsmonitor_settings,
progress: progress.as_ref().map(|x| x as _),
start_tracking_matcher: &auto_tracking_matcher,
max_new_file_size,
})?;
drop(progress);
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
pub mod chmod;
pub mod list;
pub mod show;
pub mod track;
pub mod untrack;

use crate::cli_util::CommandHelper;
Expand All @@ -27,6 +28,7 @@ pub enum FileCommand {
Chmod(chmod::FileChmodArgs),
List(list::FileListArgs),
Show(show::FileShowArgs),
Track(track::FileTrackArgs),
Untrack(untrack::FileUntrackArgs),
}

Expand All @@ -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),
}
}
68 changes: 68 additions & 0 deletions cli/src/commands/file/track.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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.
///
/// New files in the working copy can be automatically tracked.
/// You can configure which paths to automatically track by setting
/// `snapshot.auto-track` (e.g. to `"none()"` or `"glob:**/*.rs"`). Files that
/// don't match the pattern can be manually tracked using this command. The
/// default pattern is `all()` and this command has no effect.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct FileTrackArgs {
/// Paths to track
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}

#[instrument(skip_all)]
pub(crate) fn cmd_file_track(
ui: &mut Ui,
command: &CommandHelper,
args: &FileTrackArgs,
) -> 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.repo_mut().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(())
}
2 changes: 2 additions & 0 deletions cli/src/commands/file/untrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions cli/src/config/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ edit = false

[snapshot]
max-new-file-size = "1MiB"
auto-track = "all()"
2 changes: 2 additions & 0 deletions cli/src/merge_tools/diff_working_copies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
Expand Down
18 changes: 18 additions & 0 deletions cli/tests/cli-reference@.md.snap
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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)
Expand Down Expand Up @@ -739,6 +740,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
Expand Down Expand Up @@ -809,6 +811,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.
New files in the working copy can be automatically tracked. You can configure which paths to automatically track by setting `snapshot.auto-track` (e.g. to `"none()"` or `"glob:**/*.rs"`). Files that don't match the pattern can be manually tracked using this command. The default pattern is `all()` and this command has no effect.
**Usage:** `jj file track <PATHS>...`
###### **Arguments:**
* `<PATHS>` — Paths to track
## `jj file untrack`
Stop tracking specified paths in the working copy
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mod test_edit_command;
mod test_evolog_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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -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"]);
Expand All @@ -128,4 +128,95 @@ 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
"###);

// Can manually untrack paths
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "untrack", "file3.md"]);
insta::assert_snapshot!(stdout, @"");
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]);
insta::assert_snapshot!(stdout, @r###"
file1.rs
"###);

// 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.replace('\\', "/"), @r###"
../file1.rs
"###);

// But `jj file 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.replace('\\', "/"), @r###"
../file1.rs
file1.rs
"###);
}

#[test]
fn test_track_ignored() {
let test_env = TestEnvironment::default();
test_env.add_config(r#"snapshot.auto-track = 'none()'"#);
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(".gitignore"), "*.bak\n").unwrap();
std::fs::write(repo_path.join("file1"), "initial").unwrap();
std::fs::write(repo_path.join("file1.bak"), "initial").unwrap();

// Track an unignored path
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "track", "file1"]);
insta::assert_snapshot!(stdout, @"");
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]);
insta::assert_snapshot!(stdout, @r###"
file1
"###);
// Track an ignored path
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "track", "file1.bak"]);
insta::assert_snapshot!(stdout, @"");
// TODO: We should teach `jj file track` to track ignored paths (possibly
// requiring a flag)
let stdout = test_env.jj_cmd_success(&repo_path, &["file", "list"]);
insta::assert_snapshot!(stdout, @r###"
file1
"###);
}
5 changes: 5 additions & 0 deletions cli/tests/test_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, @"");
}
4 changes: 4 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ To squash or split commits, use `jj squash` and `jj split`.

### How can I keep my scratch files in the repository without committing them?

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.

You can keep your notes and other scratch files in the repository, if you add
a wildcard pattern to either the repo's `gitignore` or your global `gitignore`.
Something like `*.scratch` or `*.scratchpad` should do, after that rename the
Expand Down
Loading

0 comments on commit 2817cbc

Please sign in to comment.