Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: Make fix tools run from the repo root. #4867

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixed bugs

* `jj config unset <TABLE-NAME>` no longer removes a table (such as `[ui]`.)
* Formatters called by `jj fix` now always run from the repo root
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: insert blank line before

([#4616](https://github.com/martinvonz/jj/issues/4616))

## [0.23.0] - 2024-11-06

Expand Down
13 changes: 12 additions & 1 deletion cli/src/commands/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::Write;
use std::path::Path;
use std::process::Stdio;
use std::sync::mpsc::channel;

Expand Down Expand Up @@ -158,6 +159,11 @@ pub(crate) fn cmd_fix(
.parse_file_patterns(ui, &args.paths)?
.to_matcher();

// See #4866.
// Fix commands must run from the repo root, as it may read files such as
// .clang-format that depend on the working directory being correct.
let working_dir = workspace_command.repo_path().to_path_buf();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be workspace_root()? Maybe we can add a fake-formatter option that stat/read a file to test the behavior.

fwiw, this can be inlined as tx.base_workspace_helper()....


let mut tx = workspace_command.start_transaction();

// Collect all of the unique `ToolInput`s we're going to use. Tools should be
Expand Down Expand Up @@ -239,6 +245,7 @@ pub(crate) fn cmd_fix(
tx.repo().store().as_ref(),
&tools_config,
&unique_tool_inputs,
&working_dir,
)?;

// Substitute the fixed file IDs into all of the affected commits. Currently,
Expand Down Expand Up @@ -326,6 +333,7 @@ fn fix_file_ids<'a>(
store: &Store,
tools_config: &ToolsConfig,
tool_inputs: &'a HashSet<ToolInput>,
working_dir: &Path,
) -> Result<HashMap<&'a ToolInput, FileId>, CommandError> {
let (updates_tx, updates_rx) = channel();
// TODO: Switch to futures, or document the decision not to. We don't need
Expand All @@ -347,7 +355,8 @@ fn fix_file_ids<'a>(
read.read_to_end(&mut old_content)?;
let new_content =
matching_tools.fold(old_content.clone(), |prev_content, tool_config| {
match run_tool(&tool_config.command, tool_input, &prev_content) {
match run_tool(&tool_config.command, tool_input, &prev_content, working_dir)
{
Ok(next_content) => next_content,
// TODO: Because the stderr is passed through, this isn't always failing
// silently, but it should do something better will the exit code, tool
Expand Down Expand Up @@ -386,13 +395,15 @@ fn run_tool(
tool_command: &CommandNameAndArgs,
tool_input: &ToolInput,
old_content: &[u8],
working_dir: &Path,
) -> Result<Vec<u8>, ()> {
// TODO: Pipe stderr so we can tell the user which commit, file, and tool it is
// associated with.
let mut vars: HashMap<&str, &str> = HashMap::new();
vars.insert("path", tool_input.repo_path.as_internal_file_string());
let mut child = tool_command
.to_command_with_variables(&vars)
.current_dir(working_dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
Expand Down
56 changes: 37 additions & 19 deletions cli/tests/test_fix_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;

use itertools::Itertools;
use jj_lib::file_util::try_symlink;

use crate::common::TestEnvironment;

fn redact_path(s: &str, path: &Path, label: &str) -> String {
// When the test runs on Windows, backslashes in the path complicate things by
// changing the double quotes to single quotes in the serialized TOML.
s.replace(
&if cfg!(windows) {
format!(r#"'{}'"#, path.to_str().unwrap())
} else {
format!(r#""{}""#, path.to_str().unwrap())
},
&format!("<redacted {label} path>"),
)
}

/// Set up a repo where the `jj fix` command uses the fake editor with the given
/// flags. Returns a function that redacts the formatter executable's path from
/// a given string for test determinism.
Expand All @@ -42,16 +56,7 @@ fn init_with_fake_formatter(args: &[&str]) -> (TestEnvironment, PathBuf, impl Fn
.join(r#"', '"#)
));
(test_env, repo_path, move |snapshot: &str| {
// When the test runs on Windows, backslashes in the path complicate things by
// changing the double quotes to single quotes in the serialized TOML.
snapshot.replace(
&if cfg!(windows) {
format!(r#"'{}'"#, formatter_path.to_str().unwrap())
} else {
format!(r#""{}""#, formatter_path.to_str().unwrap())
},
"<redacted formatter path>",
)
redact_path(snapshot, &formatter_path, "formatter")
})
}

Expand Down Expand Up @@ -734,11 +739,18 @@ fn test_fix_cyclic() {

#[test]
fn test_deduplication() {
let logfile = tempfile::Builder::new()
.prefix("jj-fix-log")
.tempfile()
.unwrap();
// Append all fixed content to a log file. This assumes we're running the tool
// in the root directory of the repo, which is worth reconsidering if we
// establish a contract regarding cwd.
let (test_env, repo_path, redact) =
init_with_fake_formatter(&["--uppercase", "--tee", "$path-fixlog"]);
let (test_env, repo_path, redact) = init_with_fake_formatter(&[
"--uppercase",
"--tee",
logfile.path().as_os_str().to_str().unwrap(),
]);

// There are at least two interesting cases: the content is repeated immediately
// in the child commit, or later in another descendant.
Expand All @@ -756,11 +768,11 @@ fn test_deduplication() {

let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["fix", "-s", "a"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(redact(&stderr), @r###"
insta::assert_snapshot!(redact(&redact_path(&stderr, logfile.path(), "logfile")), @r###"
Warning: The `fix.tool-command` config option is deprecated and will be removed in a future version.
Hint: Replace it with the following:
[fix.tools.legacy-tool-command]
command = [<redacted formatter path>, "--uppercase", "--tee", "$path-fixlog"]
command = [<redacted formatter path>, "--uppercase", "--tee", <redacted logfile path>]
patterns = ["all()"]

Fixed 4 commits of 4 checked.
Expand All @@ -780,7 +792,7 @@ fn test_deduplication() {
// Each new content string only appears once in the log, because all the other
// inputs (like file name) were identical, and so the results were re-used. We
// sort the log because the order of execution inside `jj fix` is undefined.
insta::assert_snapshot!(sorted_lines(repo_path.join("file-fixlog")), @"BAR\nFOO\n");
insta::assert_snapshot!(sorted_lines(logfile.path().to_path_buf()), @"BAR\nFOO\n");
}

fn sorted_lines(path: PathBuf) -> String {
Expand All @@ -795,26 +807,32 @@ fn sorted_lines(path: PathBuf) -> String {

#[test]
fn test_executed_but_nothing_changed() {
let logfile = tempfile::Builder::new()
.prefix("jj-fix-log")
.tempfile()
.unwrap();

// Show that the tool ran by causing a side effect with --tee, and test that we
// do the right thing when the tool's output is exactly equal to its input.
let (test_env, repo_path, redact) = init_with_fake_formatter(&["--tee", "$path-copy"]);
let (test_env, repo_path, redact) =
init_with_fake_formatter(&["--tee", logfile.path().as_os_str().to_str().unwrap()]);
std::fs::write(repo_path.join("file"), "content\n").unwrap();

let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["fix", "-s", "@"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(redact(&stderr), @r###"
insta::assert_snapshot!(redact(&redact_path(&stderr, logfile.path(), "logfile")), @r###"
Warning: The `fix.tool-command` config option is deprecated and will be removed in a future version.
Hint: Replace it with the following:
[fix.tools.legacy-tool-command]
command = [<redacted formatter path>, "--tee", "$path-copy"]
command = [<redacted formatter path>, "--tee", <redacted logfile path>]
patterns = ["all()"]

Fixed 0 commits of 1 checked.
Nothing changed.
"###);
let content = test_env.jj_cmd_success(&repo_path, &["file", "show", "file", "-r", "@"]);
insta::assert_snapshot!(content, @"content\n");
let copy_content = std::fs::read_to_string(repo_path.join("file-copy").as_os_str()).unwrap();
let copy_content = std::fs::read_to_string(logfile.path()).unwrap();
insta::assert_snapshot!(copy_content, @"content\n");
}

Expand Down