just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::{self, Write};
16
17use jj_lib::backend::BackendResult;
18use jj_lib::conflicts::{materialize_tree_value, MaterializedTreeValue};
19use jj_lib::fileset::{FilePattern, FilesetExpression};
20use jj_lib::merge::MergedTreeValue;
21use jj_lib::repo::Repo;
22use jj_lib::repo_path::RepoPath;
23use pollster::FutureExt;
24use tracing::instrument;
25
26use crate::cli_util::{
27 print_unmatched_explicit_paths, CommandHelper, RevisionArg, WorkspaceCommandHelper,
28};
29use crate::command_error::{user_error, CommandError};
30use crate::ui::Ui;
31
32/// Print contents of files in a revision
33///
34/// If the given path is a directory, files in the directory will be visited
35/// recursively.
36#[derive(clap::Args, Clone, Debug)]
37pub(crate) struct CatArgs {
38 /// The revision to get the file contents from
39 #[arg(long, short, default_value = "@")]
40 revision: RevisionArg,
41 /// Paths to print
42 #[arg(required = true, value_hint = clap::ValueHint::FilePath)]
43 paths: Vec<String>,
44}
45
46#[instrument(skip_all)]
47pub(crate) fn cmd_cat(
48 ui: &mut Ui,
49 command: &CommandHelper,
50 args: &CatArgs,
51) -> Result<(), CommandError> {
52 let workspace_command = command.workspace_helper(ui)?;
53 let commit = workspace_command.resolve_single_rev(&args.revision)?;
54 let tree = commit.tree()?;
55 // TODO: No need to add special case for empty paths when switching to
56 // parse_union_filesets(). paths = [] should be "none()" if supported.
57 let fileset_expression = workspace_command.parse_file_patterns(&args.paths)?;
58
59 // Try fast path for single file entry
60 if let Some(path) = get_single_path(&fileset_expression) {
61 let value = tree.path_value(path);
62 if value.is_absent() {
63 let ui_path = workspace_command.format_file_path(path);
64 return Err(user_error(format!("No such path: {ui_path}")));
65 }
66 if !value.is_tree() {
67 ui.request_pager();
68 write_tree_entries(ui, &workspace_command, [(path, Ok(value))])?;
69 return Ok(());
70 }
71 }
72
73 let matcher = fileset_expression.to_matcher();
74 ui.request_pager();
75 write_tree_entries(
76 ui,
77 &workspace_command,
78 tree.entries_matching(matcher.as_ref()),
79 )?;
80 print_unmatched_explicit_paths(ui, &workspace_command, &fileset_expression, [&tree])?;
81 Ok(())
82}
83
84fn get_single_path(expression: &FilesetExpression) -> Option<&RepoPath> {
85 match &expression {
86 FilesetExpression::Pattern(pattern) => match pattern {
87 // Not using pattern.as_path() because files-in:<path> shouldn't
88 // select the literal <path> itself.
89 FilePattern::FilePath(path) | FilePattern::PrefixPath(path) => Some(path),
90 FilePattern::FileGlob { .. } => None,
91 },
92 _ => None,
93 }
94}
95
96fn write_tree_entries<P: AsRef<RepoPath>>(
97 ui: &Ui,
98 workspace_command: &WorkspaceCommandHelper,
99 entries: impl IntoIterator<Item = (P, BackendResult<MergedTreeValue>)>,
100) -> Result<(), CommandError> {
101 let repo = workspace_command.repo();
102 for (path, result) in entries {
103 let value = result?;
104 let materialized = materialize_tree_value(repo.store(), path.as_ref(), value).block_on()?;
105 match materialized {
106 MaterializedTreeValue::Absent => panic!("absent values should be excluded"),
107 MaterializedTreeValue::File { mut reader, .. } => {
108 io::copy(&mut reader, &mut ui.stdout_formatter().as_mut())?;
109 }
110 MaterializedTreeValue::Conflict { contents, .. } => {
111 ui.stdout_formatter().write_all(&contents)?;
112 }
113 MaterializedTreeValue::Symlink { .. } | MaterializedTreeValue::GitSubmodule(_) => {
114 let ui_path = workspace_command.format_file_path(path.as_ref());
115 writeln!(
116 ui.warning_default(),
117 "Path exists but is not a file: {ui_path}"
118 )?;
119 }
120 MaterializedTreeValue::Tree(_) => panic!("entries should not contain trees"),
121 }
122 }
123 Ok(())
124}