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;
16use std::io::Write as _;
17
18use clap_complete::ArgValueCandidates;
19use clap_complete::ArgValueCompleter;
20use jj_lib::backend::BackendResult;
21use jj_lib::conflicts::materialize_merge_result;
22use jj_lib::conflicts::materialize_tree_value;
23use jj_lib::conflicts::MaterializedTreeValue;
24use jj_lib::fileset::FilePattern;
25use jj_lib::fileset::FilesetExpression;
26use jj_lib::merge::MergedTreeValue;
27use jj_lib::repo::Repo as _;
28use jj_lib::repo_path::RepoPath;
29use pollster::FutureExt as _;
30use tracing::instrument;
31
32use crate::cli_util::print_unmatched_explicit_paths;
33use crate::cli_util::CommandHelper;
34use crate::cli_util::RevisionArg;
35use crate::cli_util::WorkspaceCommandHelper;
36use crate::command_error::user_error;
37use crate::command_error::CommandError;
38use crate::complete;
39use crate::ui::Ui;
40
41/// Print contents of files in a revision
42///
43/// If the given path is a directory, files in the directory will be visited
44/// recursively.
45#[derive(clap::Args, Clone, Debug)]
46pub(crate) struct FileShowArgs {
47 /// The revision to get the file contents from
48 #[arg(
49 long, short,
50 default_value = "@",
51 value_name = "REVSET",
52 add = ArgValueCandidates::new(complete::all_revisions),
53 )]
54 revision: RevisionArg,
55 /// Paths to print
56 #[arg(
57 required = true,
58 value_name = "FILESETS",
59 value_hint = clap::ValueHint::FilePath,
60 add = ArgValueCompleter::new(complete::all_revision_files),
61 )]
62 paths: Vec<String>,
63}
64
65#[instrument(skip_all)]
66pub(crate) fn cmd_file_show(
67 ui: &mut Ui,
68 command: &CommandHelper,
69 args: &FileShowArgs,
70) -> Result<(), CommandError> {
71 let workspace_command = command.workspace_helper(ui)?;
72 let commit = workspace_command.resolve_single_rev(ui, &args.revision)?;
73 let tree = commit.tree()?;
74 // TODO: No need to add special case for empty paths when switching to
75 // parse_union_filesets(). paths = [] should be "none()" if supported.
76 let fileset_expression = workspace_command.parse_file_patterns(ui, &args.paths)?;
77
78 // Try fast path for single file entry
79 if let Some(path) = get_single_path(&fileset_expression) {
80 let value = tree.path_value(path)?;
81 if value.is_absent() {
82 let ui_path = workspace_command.format_file_path(path);
83 return Err(user_error(format!("No such path: {ui_path}")));
84 }
85 if !value.is_tree() {
86 ui.request_pager();
87 write_tree_entries(ui, &workspace_command, [(path, Ok(value))])?;
88 return Ok(());
89 }
90 }
91
92 let matcher = fileset_expression.to_matcher();
93 ui.request_pager();
94 write_tree_entries(
95 ui,
96 &workspace_command,
97 tree.entries_matching(matcher.as_ref()),
98 )?;
99 print_unmatched_explicit_paths(ui, &workspace_command, &fileset_expression, [&tree])?;
100 Ok(())
101}
102
103fn get_single_path(expression: &FilesetExpression) -> Option<&RepoPath> {
104 match &expression {
105 FilesetExpression::Pattern(pattern) => match pattern {
106 // Not using pattern.as_path() because files-in:<path> shouldn't
107 // select the literal <path> itself.
108 FilePattern::FilePath(path) | FilePattern::PrefixPath(path) => Some(path),
109 FilePattern::FileGlob { .. } => None,
110 },
111 _ => None,
112 }
113}
114
115fn write_tree_entries<P: AsRef<RepoPath>>(
116 ui: &Ui,
117 workspace_command: &WorkspaceCommandHelper,
118 entries: impl IntoIterator<Item = (P, BackendResult<MergedTreeValue>)>,
119) -> Result<(), CommandError> {
120 let repo = workspace_command.repo();
121 for (path, result) in entries {
122 let value = result?;
123 let materialized = materialize_tree_value(repo.store(), path.as_ref(), value).block_on()?;
124 match materialized {
125 MaterializedTreeValue::Absent => panic!("absent values should be excluded"),
126 MaterializedTreeValue::AccessDenied(err) => {
127 let ui_path = workspace_command.format_file_path(path.as_ref());
128 writeln!(
129 ui.warning_default(),
130 "Path '{ui_path}' exists but access is denied: {err}"
131 )?;
132 }
133 MaterializedTreeValue::File(mut file) => {
134 io::copy(&mut file.reader, &mut ui.stdout_formatter().as_mut())?;
135 }
136 MaterializedTreeValue::FileConflict { contents, .. } => {
137 materialize_merge_result(
138 &contents,
139 workspace_command.env().conflict_marker_style(),
140 &mut ui.stdout_formatter(),
141 )?;
142 }
143 MaterializedTreeValue::OtherConflict { id } => {
144 ui.stdout_formatter().write_all(id.describe().as_bytes())?;
145 }
146 MaterializedTreeValue::Symlink { .. } | MaterializedTreeValue::GitSubmodule(_) => {
147 let ui_path = workspace_command.format_file_path(path.as_ref());
148 writeln!(
149 ui.warning_default(),
150 "Path '{ui_path}' exists but is not a file"
151 )?;
152 }
153 MaterializedTreeValue::Tree(_) => panic!("entries should not contain trees"),
154 }
155 }
156 Ok(())
157}