just playing with tangled
at main 190 lines 7.1 kB view raw
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 clap_complete::ArgValueCompleter; 16use indexmap::IndexSet; 17use itertools::Itertools as _; 18use jj_lib::copies::CopyRecords; 19use jj_lib::repo::Repo as _; 20use jj_lib::rewrite::merge_commit_trees; 21use tracing::instrument; 22 23use crate::cli_util::print_unmatched_explicit_paths; 24use crate::cli_util::short_commit_hash; 25use crate::cli_util::CommandHelper; 26use crate::cli_util::RevisionArg; 27use crate::command_error::user_error_with_hint; 28use crate::command_error::CommandError; 29use crate::complete; 30use crate::diff_util::get_copy_records; 31use crate::diff_util::DiffFormatArgs; 32use crate::ui::Ui; 33 34/// Compare file contents between two revisions 35/// 36/// With the `-r` option, shows the changes compared to the parent revision. 37/// If there are several parent revisions (i.e., the given revision is a 38/// merge), then they will be merged and the changes from the result to the 39/// given revision will be shown. 40/// 41/// With the `--from` and/or `--to` options, shows the difference from/to the 42/// given revisions. If either is left out, it defaults to the working-copy 43/// commit. For example, `jj diff --from main` shows the changes from "main" 44/// (perhaps a bookmark name) to the working-copy commit. 45/// 46/// If no option is specified, it defaults to `-r @`. 47#[derive(clap::Args, Clone, Debug)] 48#[command(mut_arg("ignore_all_space", |a| a.short('w')))] 49#[command(mut_arg("ignore_space_change", |a| a.short('b')))] 50pub(crate) struct DiffArgs { 51 /// Show changes in these revisions 52 /// 53 /// If there are multiple revisions, then then total diff for all of them 54 /// will be shown. For example, if you have a linear chain of revisions 55 /// A..D, then `jj diff -r B::D` equals `jj diff --from A --to D`. Multiple 56 /// heads and/or roots are supported, but gaps in the revset are not 57 /// supported (e.g. `jj diff -r 'A|C'` in a linear chain A..C). 58 /// 59 /// If a revision is a merge commit, this shows changes *from* the 60 /// automatic merge of the contents of all of its parents *to* the contents 61 /// of the revision itself. 62 /// 63 /// If none of `-r`, `-f`, or `-t` is provided, then the default is `-r @`. 64 #[arg( 65 long, 66 short, 67 value_name = "REVSETS", 68 alias = "revision", 69 add = ArgValueCompleter::new(complete::revset_expression_all), 70 )] 71 revisions: Option<Vec<RevisionArg>>, 72 /// Show changes from this revision 73 /// 74 /// If none of `-r`, `-f`, or `-t` is provided, then the default is `-r @`. 75 #[arg( 76 long, 77 short, 78 conflicts_with = "revisions", 79 value_name = "REVSET", 80 add = ArgValueCompleter::new(complete::revset_expression_all), 81 )] 82 from: Option<RevisionArg>, 83 /// Show changes to this revision 84 /// 85 /// If none of `-r`, `-f`, or `-t` is provided, then the default is `-r @`. 86 #[arg( 87 long, 88 short, 89 conflicts_with = "revisions", 90 value_name = "REVSET", 91 add = ArgValueCompleter::new(complete::revset_expression_all), 92 )] 93 to: Option<RevisionArg>, 94 /// Restrict the diff to these paths 95 #[arg( 96 value_name = "FILESETS", 97 value_hint = clap::ValueHint::AnyPath, 98 add = ArgValueCompleter::new(complete::modified_revision_or_range_files), 99 )] 100 paths: Vec<String>, 101 #[command(flatten)] 102 format: DiffFormatArgs, 103} 104 105#[instrument(skip_all)] 106pub(crate) fn cmd_diff( 107 ui: &mut Ui, 108 command: &CommandHelper, 109 args: &DiffArgs, 110) -> Result<(), CommandError> { 111 let workspace_command = command.workspace_helper(ui)?; 112 let repo = workspace_command.repo(); 113 let fileset_expression = workspace_command.parse_file_patterns(ui, &args.paths)?; 114 let matcher = fileset_expression.to_matcher(); 115 116 let from_tree; 117 let to_tree; 118 let mut copy_records = CopyRecords::default(); 119 if args.from.is_some() || args.to.is_some() { 120 let resolve_revision = |r: &Option<RevisionArg>| { 121 workspace_command.resolve_single_rev(ui, r.as_ref().unwrap_or(&RevisionArg::AT)) 122 }; 123 let from = resolve_revision(&args.from)?; 124 let to = resolve_revision(&args.to)?; 125 from_tree = from.tree()?; 126 to_tree = to.tree()?; 127 128 let records = get_copy_records(repo.store(), from.id(), to.id(), &matcher)?; 129 copy_records.add_records(records)?; 130 } else { 131 let revision_args = args 132 .revisions 133 .as_deref() 134 .unwrap_or(std::slice::from_ref(&RevisionArg::AT)); 135 let revisions_evaluator = workspace_command.parse_union_revsets(ui, revision_args)?; 136 let target_expression = revisions_evaluator.expression(); 137 let mut gaps_revset = workspace_command 138 .attach_revset_evaluator(target_expression.connected().minus(target_expression)) 139 .evaluate_to_commit_ids()?; 140 if let Some(commit_id) = gaps_revset.next() { 141 return Err(user_error_with_hint( 142 "Cannot diff revsets with gaps in.", 143 format!( 144 "Revision {} would need to be in the set.", 145 short_commit_hash(&commit_id?) 146 ), 147 )); 148 } 149 let heads: Vec<_> = workspace_command 150 .attach_revset_evaluator(target_expression.heads()) 151 .evaluate_to_commits()? 152 .try_collect()?; 153 let roots: Vec<_> = workspace_command 154 .attach_revset_evaluator(target_expression.roots()) 155 .evaluate_to_commits()? 156 .try_collect()?; 157 158 // Collect parents outside of revset to preserve parent order 159 let parents: IndexSet<_> = roots.iter().flat_map(|c| c.parents()).try_collect()?; 160 let parents = parents.into_iter().collect_vec(); 161 from_tree = merge_commit_trees(repo.as_ref(), &parents)?; 162 to_tree = merge_commit_trees(repo.as_ref(), &heads)?; 163 164 for p in &parents { 165 for to in &heads { 166 let records = get_copy_records(repo.store(), p.id(), to.id(), &matcher)?; 167 copy_records.add_records(records)?; 168 } 169 } 170 } 171 172 let diff_renderer = workspace_command.diff_renderer_for(&args.format)?; 173 ui.request_pager(); 174 diff_renderer.show_diff( 175 ui, 176 ui.stdout_formatter().as_mut(), 177 &from_tree, 178 &to_tree, 179 &matcher, 180 &copy_records, 181 ui.term_width(), 182 )?; 183 print_unmatched_explicit_paths( 184 ui, 185 &workspace_command, 186 &fileset_expression, 187 [&from_tree, &to_tree], 188 )?; 189 Ok(()) 190}