197 lines · 7.3 KB · Rust
RawDownload
// Copyright 2020 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 as _; use clap_complete::ArgValueCompleter;use itertools::Itertools as _;use jj_lib::backend::BackendResult;use jj_lib::conflicts::ConflictMaterializeOptions;use jj_lib::conflicts::MaterializedTreeValue;use jj_lib::conflicts::materialize_merge_result;use jj_lib::conflicts::materialize_tree_value;use jj_lib::file_util::copy_async_to_sync;use jj_lib::fileset::FilePattern;use jj_lib::fileset::FilesetExpression;use jj_lib::repo::Repo as _;use jj_lib::repo_path::RepoPath;use pollster::FutureExt as _;use tracing::instrument; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::WorkspaceCommandHelper;use crate::cli_util::print_unmatched_explicit_paths;use crate::command_error::CommandError;use crate::command_error::user_error;use crate::commit_templater::TreeEntry;use crate::complete;use crate::templater::TemplateRenderer;use crate::ui::Ui; /// 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 FileShowArgs {    /// The revision to get the file contents from    #[arg(        long, short,        default_value = "@",        value_name = "REVSET",        add = ArgValueCompleter::new(complete::revset_expression_all),    )]    revision: RevisionArg,     /// Render each file metadata using the given template    ///    /// All 0-argument methods of the [`TreeEntry` type] are available as    /// keywords in the template expression. See [`jj help -k templates`] for    /// more information.    ///    /// If not specified, this defaults to the `templates.file_show` setting.    ///    /// [`TreeEntry` type]:    ///     https://jj-vcs.github.io/jj/latest/templates/#treeentry-type    ///    /// [`jj help -k templates`]:    ///     https://jj-vcs.github.io/jj/latest/templates/    #[arg(long, short = 'T')]    template: Option<String>,     /// Paths to print    #[arg(        required = true,        value_name = "FILESETS",        value_hint = clap::ValueHint::FilePath,        add = ArgValueCompleter::new(complete::all_revision_files),    )]    paths: Vec<String>,} #[instrument(skip_all)]pub(crate) fn cmd_file_show(    ui: &mut Ui,    command: &CommandHelper,    args: &FileShowArgs,) -> Result<(), CommandError> {    let workspace_command = command.workspace_helper(ui)?;    let commit = workspace_command.resolve_single_rev(ui, &args.revision)?;    let tree = commit.tree()?;    // 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(ui, &args.paths)?;    let template = {        let language = workspace_command.commit_template_language();        let text = match &args.template {            Some(value) => value.to_owned(),            None => workspace_command.settings().get("templates.file_show")?,        };        workspace_command            .parse_template(ui, &language, &text)?            .labeled(["file_show"])    };     // 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}")));        }        if !value.is_tree() {            ui.request_pager();            let entry = TreeEntry {                path: path.to_owned(),                value,            };            write_tree_entries(ui, &workspace_command, &template, [Ok(entry)])?;            return Ok(());        }    }     let matcher = fileset_expression.to_matcher();    ui.request_pager();    write_tree_entries(        ui,        &workspace_command,        &template,        tree.entries_matching(matcher.as_ref())            .map(|(path, value)| Ok((path, value?)))            .map_ok(|(path, value)| TreeEntry { path, value }),    )?;    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),            FilePattern::FileGlob { .. } => None,        },        _ => None,    }} fn write_tree_entries(    ui: &Ui,    workspace_command: &WorkspaceCommandHelper,    template: &TemplateRenderer<TreeEntry>,    entries: impl IntoIterator<Item = BackendResult<TreeEntry>>,) -> Result<(), CommandError> {    let repo = workspace_command.repo();    for entry in entries {        let entry = entry?;        template.format(&entry, ui.stdout_formatter().as_mut())?;        let materialized =            materialize_tree_value(repo.store(), &entry.path, entry.value).block_on()?;        match materialized {            MaterializedTreeValue::Absent => panic!("absent values should be excluded"),            MaterializedTreeValue::AccessDenied(err) => {                let ui_path = workspace_command.format_file_path(&entry.path);                writeln!(                    ui.warning_default(),                    "Path '{ui_path}' exists but access is denied: {err}"                )?;            }            MaterializedTreeValue::File(file) => {                copy_async_to_sync(file.reader, ui.stdout_formatter().as_mut()).block_on()?;            }            MaterializedTreeValue::FileConflict(file) => {                let options = ConflictMaterializeOptions {                    marker_style: workspace_command.env().conflict_marker_style(),                    marker_len: None,                    merge: repo.store().merge_options().clone(),                };                materialize_merge_result(&file.contents, &mut ui.stdout_formatter(), &options)?;            }            MaterializedTreeValue::OtherConflict { id } => {                ui.stdout_formatter().write_all(id.describe().as_bytes())?;            }            MaterializedTreeValue::Symlink { .. } | MaterializedTreeValue::GitSubmodule(_) => {                let ui_path = workspace_command.format_file_path(&entry.path);                writeln!(                    ui.warning_default(),                    "Path '{ui_path}' exists but is not a file"                )?;            }            MaterializedTreeValue::Tree(_) => panic!("entries should not contain trees"),        }    }    Ok(())}