just playing with tangled
at ig/vimdiffwarn 267 lines 9.4 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 std::io::Write as _; 16use std::rc::Rc; 17 18use itertools::Itertools as _; 19use jj_lib::backend::CommitId; 20use jj_lib::commit::Commit; 21use jj_lib::repo::Repo as _; 22use jj_lib::revset::ResolvedRevsetExpression; 23use jj_lib::revset::RevsetExpression; 24use jj_lib::revset::RevsetFilterPredicate; 25use jj_lib::revset::RevsetIteratorExt as _; 26 27use crate::cli_util::short_commit_hash; 28use crate::cli_util::CommandHelper; 29use crate::cli_util::WorkspaceCommandHelper; 30use crate::command_error::user_error; 31use crate::command_error::CommandError; 32use crate::ui::Ui; 33 34#[derive(Clone, Debug, Eq, PartialEq)] 35pub(crate) struct MovementArgs { 36 pub offset: u64, 37 pub edit: bool, 38 pub no_edit: bool, 39 pub conflict: bool, 40} 41 42#[derive(Clone, Debug, Eq, PartialEq)] 43struct MovementArgsInternal { 44 offset: u64, 45 should_edit: bool, 46 conflict: bool, 47} 48 49#[derive(Clone, Copy, Debug, Eq, PartialEq)] 50pub(crate) enum Direction { 51 Next, 52 Prev, 53} 54 55impl Direction { 56 fn cmd(&self) -> &'static str { 57 match self { 58 Direction::Next => "next", 59 Direction::Prev => "prev", 60 } 61 } 62 63 fn target_not_found_error( 64 &self, 65 workspace_command: &WorkspaceCommandHelper, 66 args: &MovementArgsInternal, 67 commits: &[Commit], 68 ) -> CommandError { 69 let offset = args.offset; 70 let err_msg = match (self, args.should_edit, args.conflict) { 71 // in edit mode, start_revset is the WC, so we only look for direct descendants. 72 (Direction::Next, true, true) => { 73 String::from("The working copy has no descendants with conflicts") 74 } 75 (Direction::Next, true, false) => { 76 format!("No descendant found {offset} commit(s) forward from the working copy",) 77 } 78 // in non-edit mode, start_revset is the parent of WC, so we look for other descendants 79 // of start_revset. 80 (Direction::Next, false, true) => { 81 String::from("The working copy parent(s) have no other descendants with conflicts") 82 } 83 (Direction::Next, false, false) => format!( 84 "No other descendant found {offset} commit(s) forward from the working copy \ 85 parent(s)", 86 ), 87 // The WC can never be an ancestor of the start_revset since start_revset is either 88 // itself or it's parent. 89 (Direction::Prev, true, true) => { 90 String::from("The working copy has no ancestors with conflicts") 91 } 92 (Direction::Prev, true, false) => { 93 format!("No ancestor found {offset} commit(s) back from the working copy",) 94 } 95 (Direction::Prev, false, true) => { 96 String::from("The working copy parent(s) have no ancestors with conflicts") 97 } 98 (Direction::Prev, false, false) => format!( 99 "No ancestor found {offset} commit(s) back from the working copy parents(s)", 100 ), 101 }; 102 103 let template = workspace_command.commit_summary_template(); 104 let mut cmd_err = user_error(err_msg); 105 for commit in commits { 106 cmd_err.add_formatted_hint_with(|formatter| { 107 if args.should_edit { 108 write!(formatter, "Working copy: ")?; 109 } else { 110 write!(formatter, "Working copy parent: ")?; 111 } 112 template.format(commit, formatter) 113 }); 114 } 115 116 cmd_err 117 } 118 119 fn build_target_revset( 120 &self, 121 working_revset: &Rc<ResolvedRevsetExpression>, 122 start_revset: &Rc<ResolvedRevsetExpression>, 123 args: &MovementArgsInternal, 124 ) -> Result<Rc<ResolvedRevsetExpression>, CommandError> { 125 let nth = match (self, args.should_edit) { 126 (Direction::Next, true) => start_revset.descendants_at(args.offset), 127 (Direction::Next, false) => start_revset 128 .children() 129 .minus(working_revset) 130 .descendants_at(args.offset - 1), 131 (Direction::Prev, _) => start_revset.ancestors_at(args.offset), 132 }; 133 134 let target_revset = match (self, args.conflict) { 135 (_, false) => nth, 136 (Direction::Next, true) => nth 137 .descendants() 138 .filtered(RevsetFilterPredicate::HasConflict) 139 .roots(), 140 // If people desire to move to the root conflict, replace the `heads()` below 141 // with `roots(). But let's wait for feedback. 142 (Direction::Prev, true) => nth 143 .ancestors() 144 .filtered(RevsetFilterPredicate::HasConflict) 145 .heads(), 146 }; 147 148 Ok(target_revset) 149 } 150} 151 152fn get_target_commit( 153 ui: &mut Ui, 154 workspace_command: &WorkspaceCommandHelper, 155 direction: Direction, 156 working_commit_id: &CommitId, 157 args: &MovementArgsInternal, 158) -> Result<Commit, CommandError> { 159 let wc_revset = RevsetExpression::commit(working_commit_id.clone()); 160 // If we're editing, start at the working-copy commit. Otherwise, start from 161 // its direct parent(s). 162 let start_revset = if args.should_edit { 163 wc_revset.clone() 164 } else { 165 wc_revset.parents() 166 }; 167 168 let target_revset = direction.build_target_revset(&wc_revset, &start_revset, args)?; 169 170 let targets: Vec<Commit> = target_revset 171 .evaluate(workspace_command.repo().as_ref())? 172 .iter() 173 .commits(workspace_command.repo().store()) 174 .try_collect()?; 175 176 let target = match targets.as_slice() { 177 [target] => target, 178 [] => { 179 // We found no ancestor/descendant. 180 let start_commits: Vec<Commit> = start_revset 181 .evaluate(workspace_command.repo().as_ref())? 182 .iter() 183 .commits(workspace_command.repo().store()) 184 .try_collect()?; 185 return Err(direction.target_not_found_error(workspace_command, args, &start_commits)); 186 } 187 commits => choose_commit(ui, workspace_command, direction, commits)?, 188 }; 189 190 Ok(target.clone()) 191} 192 193fn choose_commit<'a>( 194 ui: &Ui, 195 workspace_command: &WorkspaceCommandHelper, 196 direction: Direction, 197 commits: &'a [Commit], 198) -> Result<&'a Commit, CommandError> { 199 writeln!( 200 ui.stderr(), 201 "ambiguous {} commit, choose one to target:", 202 direction.cmd() 203 )?; 204 let mut formatter = ui.stderr_formatter(); 205 let template = workspace_command.commit_summary_template(); 206 let mut choices: Vec<String> = Default::default(); 207 for (i, commit) in commits.iter().enumerate() { 208 write!(formatter, "{}: ", i + 1)?; 209 template.format(commit, formatter.as_mut())?; 210 writeln!(formatter)?; 211 choices.push(format!("{}", i + 1)); 212 } 213 writeln!(formatter, "q: quit the prompt")?; 214 choices.push("q".to_string()); 215 drop(formatter); 216 217 let index = ui.prompt_choice( 218 "enter the index of the commit you want to target", 219 &choices, 220 None, 221 )?; 222 commits 223 .get(index) 224 .ok_or_else(|| user_error("ambiguous target commit")) 225} 226 227pub(crate) fn move_to_commit( 228 ui: &mut Ui, 229 command: &CommandHelper, 230 direction: Direction, 231 args: &MovementArgs, 232) -> Result<(), CommandError> { 233 let mut workspace_command = command.workspace_helper(ui)?; 234 235 let current_wc_id = workspace_command 236 .get_wc_commit_id() 237 .ok_or_else(|| user_error("This command requires a working copy"))?; 238 239 let config_edit_flag = workspace_command.settings().get_bool("ui.movement.edit")?; 240 let args = MovementArgsInternal { 241 should_edit: args.edit || (!args.no_edit && config_edit_flag), 242 offset: args.offset, 243 conflict: args.conflict, 244 }; 245 246 let target = get_target_commit(ui, &workspace_command, direction, current_wc_id, &args)?; 247 let current_short = short_commit_hash(current_wc_id); 248 let target_short = short_commit_hash(target.id()); 249 let cmd = direction.cmd(); 250 // We're editing, just move to the target commit. 251 if args.should_edit { 252 // We're editing, the target must be rewritable. 253 workspace_command.check_rewritable([target.id()])?; 254 let mut tx = workspace_command.start_transaction(); 255 tx.edit(&target)?; 256 tx.finish( 257 ui, 258 format!("{cmd}: {current_short} -> editing {target_short}"), 259 )?; 260 return Ok(()); 261 } 262 let mut tx = workspace_command.start_transaction(); 263 // Move the working-copy commit to the new parent. 264 tx.check_out(&target)?; 265 tx.finish(ui, format!("{cmd}: {current_short} -> {target_short}"))?; 266 Ok(()) 267}