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