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: migrate "cat" to matcher API #3497

Merged
merged 2 commits into from
Apr 16, 2024
Merged
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
99 changes: 75 additions & 24 deletions cli/src/commands/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::Write;
use std::io::{self, Write};

use jj_lib::conflicts::{materialize_tree_value, MaterializedTreeValue};
use jj_lib::fileset::{FilePattern, FilesetExpression};
use jj_lib::merge::MergedTreeValue;
use jj_lib::repo::Repo;
use jj_lib::repo_path::RepoPath;
use pollster::FutureExt;
use tracing::instrument;

use crate::cli_util::{CommandHelper, RevisionArg};
use crate::cli_util::{
print_unmatched_explicit_paths, CommandHelper, RevisionArg, WorkspaceCommandHelper,
};
use crate::command_error::{user_error, CommandError};
use crate::ui::Ui;

/// Print contents of a file in a revision
/// Print contents of files in a revision
///
/// If the given path is a directory, files in the directory will be visited
/// recursively.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct CatArgs {
/// The revision to get the file contents from
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// The file to print
#[arg(value_hint = clap::ValueHint::FilePath)]
path: String,
/// Paths to print
#[arg(required = true, value_hint = clap::ValueHint::FilePath)]
paths: Vec<String>,
}

#[instrument(skip_all)]
Expand All @@ -43,27 +51,70 @@ pub(crate) fn cmd_cat(
let workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision)?;
let tree = commit.tree()?;
// TODO: migrate to .parse_file_patterns()?.to_matcher()?
let path = workspace_command.parse_file_path(&args.path)?;
let repo = workspace_command.repo();
let value = tree.path_value(&path);
let materialized = materialize_tree_value(repo.store(), &path, value).block_on()?;
match materialized {
MaterializedTreeValue::Absent => {
return Err(user_error("No such path"));
}
MaterializedTreeValue::File { mut reader, .. } => {
ui.request_pager();
std::io::copy(&mut reader, &mut ui.stdout_formatter().as_mut())?;
// TODO: No need to add special case for empty paths when switching to
// parse_union_filesets(). paths = [] should be "none()" if supported.
let fileset_expression = workspace_command.parse_file_patterns(&args.paths)?;

// Try fast path for single file entry
if let Some(path) = get_single_path(&fileset_expression) {
let value = tree.path_value(path);
if value.is_absent() {
let ui_path = workspace_command.format_file_path(path);
return Err(user_error(format!("No such path: {ui_path}")));
}
MaterializedTreeValue::Conflict { contents, .. } => {
if !value.is_tree() {
ui.request_pager();
ui.stdout_formatter().write_all(&contents)?;
write_tree_entries(ui, &workspace_command, [(path, value)])?;
return Ok(());
}
MaterializedTreeValue::Symlink { .. }
| MaterializedTreeValue::Tree(_)
| MaterializedTreeValue::GitSubmodule(_) => {
return Err(user_error("Path exists but is not a file"));
}

let matcher = fileset_expression.to_matcher();
ui.request_pager();
write_tree_entries(
ui,
&workspace_command,
tree.entries_matching(matcher.as_ref()),
)?;
print_unmatched_explicit_paths(ui, &workspace_command, &fileset_expression, [&tree])?;
Ok(())
}

fn get_single_path(expression: &FilesetExpression) -> Option<&RepoPath> {
match &expression {
FilesetExpression::Pattern(pattern) => match pattern {
// Not using pattern.as_path() because files-in:<path> shouldn't
// select the literal <path> itself.
FilePattern::FilePath(path) | FilePattern::PrefixPath(path) => Some(path),
},
_ => None,
}
}

fn write_tree_entries<P: AsRef<RepoPath>>(
ui: &Ui,
workspace_command: &WorkspaceCommandHelper,
entries: impl IntoIterator<Item = (P, MergedTreeValue)>,
) -> Result<(), CommandError> {
let repo = workspace_command.repo();
for (path, value) in entries {
let materialized = materialize_tree_value(repo.store(), path.as_ref(), value).block_on()?;
match materialized {
MaterializedTreeValue::Absent => panic!("absent values should be excluded"),
MaterializedTreeValue::File { mut reader, .. } => {
io::copy(&mut reader, &mut ui.stdout_formatter().as_mut())?;
}
MaterializedTreeValue::Conflict { contents, .. } => {
ui.stdout_formatter().write_all(&contents)?;
}
MaterializedTreeValue::Symlink { .. } | MaterializedTreeValue::GitSubmodule(_) => {
let ui_path = workspace_command.format_file_path(path.as_ref());
writeln!(
ui.warning_default(),
"Path exists but is not a file: {ui_path}"
)?;
}
MaterializedTreeValue::Tree(_) => panic!("entries should not contain trees"),
}
}
Ok(())
Expand Down
10 changes: 6 additions & 4 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d
* `abandon` — Abandon a revision
* `backout` — Apply the reverse of a revision on top of another revision
* `branch` — Manage branches
* `cat` — Print contents of a file in a revision
* `cat` — Print contents of files in a revision
* `chmod` — Sets or removes the executable bit for paths in the repo
* `commit` — Update the description and create a new change on top
* `config` — Manage config options
Expand Down Expand Up @@ -384,13 +384,15 @@ A non-tracking remote branch is just a pointer to the last-fetched remote branch

## `jj cat`

Print contents of a file in a revision
Print contents of files in a revision

**Usage:** `jj cat [OPTIONS] <PATH>`
If the given path is a directory, files in the directory will be visited recursively.

**Usage:** `jj cat [OPTIONS] <PATHS>...`

###### **Arguments:**

* `<PATH>` — The file to print
* `<PATHS>` — Paths to print

###### **Options:**

Expand Down
47 changes: 43 additions & 4 deletions cli/tests/test_cat_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,29 @@ fn test_cat() {
// Error if the path doesn't exist
let stderr = test_env.jj_cmd_failure(&repo_path, &["cat", "nonexistent"]);
insta::assert_snapshot!(stderr, @r###"
Error: No such path
Error: No such path: nonexistent
"###);

// Error if the path is not a file
let stderr = test_env.jj_cmd_failure(&repo_path, &["cat", "dir"]);
// Can print files under the specified directory
let stdout = test_env.jj_cmd_success(&repo_path, &["cat", "dir"]);
insta::assert_snapshot!(stdout, @r###"
c
"###);

// Can print multiple files
let stdout = test_env.jj_cmd_success(&repo_path, &["cat", "."]);
insta::assert_snapshot!(stdout, @r###"
c
b
"###);

// Unmatched paths should generate warnings
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["cat", "file1", "non-existent"]);
insta::assert_snapshot!(stdout, @r###"
b
"###);
insta::assert_snapshot!(stderr, @r###"
Error: Path exists but is not a file
Warning: No matching entries for paths: non-existent
"###);

// Can print a conflict
Expand All @@ -82,3 +98,26 @@ fn test_cat() {
>>>>>>>
"###);
}

#[cfg(unix)]
#[test]
fn test_cat_symlink() {
let test_env = TestEnvironment::default();
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"), "a\n").unwrap();
std::fs::create_dir(repo_path.join("dir")).unwrap();
std::fs::write(repo_path.join("dir").join("file2"), "c\n").unwrap();
std::os::unix::fs::symlink("symlink1_target", repo_path.join("symlink1")).unwrap();

// Can print multiple files
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["cat", "."]);
insta::assert_snapshot!(stdout, @r###"
c
a
"###);
insta::assert_snapshot!(stderr, @r###"
Warning: Path exists but is not a file: symlink1
"###);
}