just playing with tangled
at diffedit3 441 lines 16 kB view raw
1// Copyright 2023 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::any::Any; 16use std::fmt::Debug; 17use std::io::Write as _; 18 19use clap::Subcommand; 20use jj_lib::backend::TreeId; 21use jj_lib::default_index::{AsCompositeIndex as _, DefaultIndexStore, DefaultReadonlyIndex}; 22use jj_lib::local_working_copy::LocalWorkingCopy; 23use jj_lib::merged_tree::MergedTree; 24use jj_lib::object_id::ObjectId; 25use jj_lib::repo::Repo; 26use jj_lib::repo_path::RepoPathBuf; 27use jj_lib::working_copy::WorkingCopy; 28use jj_lib::{fileset, op_walk, revset}; 29 30use crate::cli_util::{CommandHelper, RevisionArg}; 31use crate::command_error::{internal_error, user_error, CommandError}; 32use crate::ui::Ui; 33use crate::{revset_util, template_parser}; 34 35/// Low-level commands not intended for users 36#[derive(Subcommand, Clone, Debug)] 37#[command(hide = true)] 38pub enum DebugCommand { 39 Fileset(DebugFilesetArgs), 40 Revset(DebugRevsetArgs), 41 #[command(name = "workingcopy")] 42 WorkingCopy(DebugWorkingCopyArgs), 43 Template(DebugTemplateArgs), 44 Index(DebugIndexArgs), 45 #[command(name = "reindex")] 46 ReIndex(DebugReIndexArgs), 47 #[command(visible_alias = "view")] 48 Operation(DebugOperationArgs), 49 Tree(DebugTreeArgs), 50 #[command(subcommand)] 51 Watchman(DebugWatchmanSubcommand), 52} 53 54/// Parse fileset expression 55#[derive(clap::Args, Clone, Debug)] 56pub struct DebugFilesetArgs { 57 #[arg(value_hint = clap::ValueHint::AnyPath)] 58 path: String, 59} 60 61/// Evaluate revset to full commit IDs 62#[derive(clap::Args, Clone, Debug)] 63pub struct DebugRevsetArgs { 64 revision: String, 65} 66 67/// Show information about the working copy state 68#[derive(clap::Args, Clone, Debug)] 69pub struct DebugWorkingCopyArgs {} 70 71/// Parse a template 72#[derive(clap::Args, Clone, Debug)] 73pub struct DebugTemplateArgs { 74 template: String, 75} 76 77/// Show commit index stats 78#[derive(clap::Args, Clone, Debug)] 79pub struct DebugIndexArgs {} 80 81/// Rebuild commit index 82#[derive(clap::Args, Clone, Debug)] 83pub struct DebugReIndexArgs {} 84 85/// Show information about an operation and its view 86#[derive(clap::Args, Clone, Debug)] 87pub struct DebugOperationArgs { 88 #[arg(default_value = "@")] 89 operation: String, 90 #[arg(long, value_enum, default_value = "all")] 91 display: DebugOperationDisplay, 92} 93 94#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] 95pub enum DebugOperationDisplay { 96 /// Show only the operation details. 97 Operation, 98 /// Show the operation id only 99 Id, 100 /// Show only the view details 101 View, 102 /// Show both the view and the operation 103 All, 104} 105 106/// List the recursive entries of a tree. 107#[derive(clap::Args, Clone, Debug)] 108pub struct DebugTreeArgs { 109 #[arg(long, short = 'r')] 110 revision: Option<RevisionArg>, 111 #[arg(long, conflicts_with = "revision")] 112 id: Option<String>, 113 #[arg(long, requires = "id")] 114 dir: Option<String>, 115 paths: Vec<String>, 116 // TODO: Add an option to include trees that are ancestors of the matched paths 117} 118 119#[derive(Subcommand, Clone, Debug)] 120pub enum DebugWatchmanSubcommand { 121 /// Check whether `watchman` is enabled and whether it's correctly installed 122 Status, 123 QueryClock, 124 QueryChangedFiles, 125 ResetClock, 126} 127 128pub fn cmd_debug( 129 ui: &mut Ui, 130 command: &CommandHelper, 131 subcommand: &DebugCommand, 132) -> Result<(), CommandError> { 133 match subcommand { 134 DebugCommand::Fileset(args) => cmd_debug_fileset(ui, command, args), 135 DebugCommand::Revset(args) => cmd_debug_revset(ui, command, args), 136 DebugCommand::WorkingCopy(args) => cmd_debug_working_copy(ui, command, args), 137 DebugCommand::Template(args) => cmd_debug_template(ui, command, args), 138 DebugCommand::Index(args) => cmd_debug_index(ui, command, args), 139 DebugCommand::ReIndex(args) => cmd_debug_reindex(ui, command, args), 140 DebugCommand::Operation(args) => cmd_debug_operation(ui, command, args), 141 DebugCommand::Tree(args) => cmd_debug_tree(ui, command, args), 142 DebugCommand::Watchman(args) => cmd_debug_watchman(ui, command, args), 143 } 144} 145 146fn cmd_debug_fileset( 147 ui: &mut Ui, 148 command: &CommandHelper, 149 args: &DebugFilesetArgs, 150) -> Result<(), CommandError> { 151 let workspace_command = command.workspace_helper(ui)?; 152 let ctx = workspace_command.fileset_parse_context(); 153 154 let expression = fileset::parse_maybe_bare(&args.path, &ctx)?; 155 writeln!(ui.stdout(), "-- Parsed:")?; 156 writeln!(ui.stdout(), "{expression:#?}")?; 157 writeln!(ui.stdout())?; 158 159 let matcher = expression.to_matcher(); 160 writeln!(ui.stdout(), "-- Matcher:")?; 161 writeln!(ui.stdout(), "{matcher:#?}")?; 162 Ok(()) 163} 164 165fn cmd_debug_revset( 166 ui: &mut Ui, 167 command: &CommandHelper, 168 args: &DebugRevsetArgs, 169) -> Result<(), CommandError> { 170 let workspace_command = command.workspace_helper(ui)?; 171 let workspace_ctx = workspace_command.revset_parse_context(); 172 let repo = workspace_command.repo().as_ref(); 173 174 let expression = revset::parse(&args.revision, &workspace_ctx)?; 175 writeln!(ui.stdout(), "-- Parsed:")?; 176 writeln!(ui.stdout(), "{expression:#?}")?; 177 writeln!(ui.stdout())?; 178 179 let expression = revset::optimize(expression); 180 writeln!(ui.stdout(), "-- Optimized:")?; 181 writeln!(ui.stdout(), "{expression:#?}")?; 182 writeln!(ui.stdout())?; 183 184 let symbol_resolver = revset_util::default_symbol_resolver( 185 repo, 186 command.revset_extensions().symbol_resolvers(), 187 workspace_command.id_prefix_context()?, 188 ); 189 let expression = expression.resolve_user_expression(repo, &symbol_resolver)?; 190 writeln!(ui.stdout(), "-- Resolved:")?; 191 writeln!(ui.stdout(), "{expression:#?}")?; 192 writeln!(ui.stdout())?; 193 194 let revset = expression.evaluate(repo)?; 195 writeln!(ui.stdout(), "-- Evaluated:")?; 196 writeln!(ui.stdout(), "{revset:#?}")?; 197 writeln!(ui.stdout())?; 198 199 writeln!(ui.stdout(), "-- Commit IDs:")?; 200 for commit_id in revset.iter() { 201 writeln!(ui.stdout(), "{}", commit_id.hex())?; 202 } 203 Ok(()) 204} 205 206fn cmd_debug_working_copy( 207 ui: &mut Ui, 208 command: &CommandHelper, 209 _args: &DebugWorkingCopyArgs, 210) -> Result<(), CommandError> { 211 let workspace_command = command.workspace_helper(ui)?; 212 let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; 213 writeln!(ui.stdout(), "Current operation: {:?}", wc.operation_id())?; 214 writeln!(ui.stdout(), "Current tree: {:?}", wc.tree_id()?)?; 215 for (file, state) in wc.file_states()? { 216 writeln!( 217 ui.stdout(), 218 "{:?} {:13?} {:10?} {:?}", 219 state.file_type, 220 state.size, 221 state.mtime.0, 222 file 223 )?; 224 } 225 Ok(()) 226} 227 228fn cmd_debug_template( 229 ui: &mut Ui, 230 _command: &CommandHelper, 231 args: &DebugTemplateArgs, 232) -> Result<(), CommandError> { 233 let node = template_parser::parse_template(&args.template)?; 234 writeln!(ui.stdout(), "{node:#?}")?; 235 Ok(()) 236} 237 238fn cmd_debug_index( 239 ui: &mut Ui, 240 command: &CommandHelper, 241 _args: &DebugIndexArgs, 242) -> Result<(), CommandError> { 243 // Resolve the operation without loading the repo, so this command won't 244 // merge concurrent operations and update the index. 245 let workspace = command.load_workspace()?; 246 let repo_loader = workspace.repo_loader(); 247 let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; 248 let index_store = repo_loader.index_store(); 249 let index = index_store 250 .get_index_at_op(&op, repo_loader.store()) 251 .map_err(internal_error)?; 252 if let Some(default_index) = index.as_any().downcast_ref::<DefaultReadonlyIndex>() { 253 let stats = default_index.as_composite().stats(); 254 writeln!(ui.stdout(), "Number of commits: {}", stats.num_commits)?; 255 writeln!(ui.stdout(), "Number of merges: {}", stats.num_merges)?; 256 writeln!( 257 ui.stdout(), 258 "Max generation number: {}", 259 stats.max_generation_number 260 )?; 261 writeln!(ui.stdout(), "Number of heads: {}", stats.num_heads)?; 262 writeln!(ui.stdout(), "Number of changes: {}", stats.num_changes)?; 263 writeln!(ui.stdout(), "Stats per level:")?; 264 for (i, level) in stats.levels.iter().enumerate() { 265 writeln!(ui.stdout(), " Level {i}:")?; 266 writeln!(ui.stdout(), " Number of commits: {}", level.num_commits)?; 267 writeln!(ui.stdout(), " Name: {}", level.name.as_ref().unwrap())?; 268 } 269 } else { 270 return Err(user_error(format!( 271 "Cannot get stats for indexes of type '{}'", 272 index_store.name() 273 ))); 274 } 275 Ok(()) 276} 277 278fn cmd_debug_reindex( 279 ui: &mut Ui, 280 command: &CommandHelper, 281 _args: &DebugReIndexArgs, 282) -> Result<(), CommandError> { 283 // Resolve the operation without loading the repo. The index might have to 284 // be rebuilt while loading the repo. 285 let workspace = command.load_workspace()?; 286 let repo_loader = workspace.repo_loader(); 287 let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; 288 let index_store = repo_loader.index_store(); 289 if let Some(default_index_store) = index_store.as_any().downcast_ref::<DefaultIndexStore>() { 290 default_index_store.reinit().map_err(internal_error)?; 291 let default_index = default_index_store 292 .build_index_at_operation(&op, repo_loader.store()) 293 .map_err(internal_error)?; 294 writeln!( 295 ui.status(), 296 "Finished indexing {:?} commits.", 297 default_index.as_composite().stats().num_commits 298 )?; 299 } else { 300 return Err(user_error(format!( 301 "Cannot reindex indexes of type '{}'", 302 index_store.name() 303 ))); 304 } 305 Ok(()) 306} 307 308fn cmd_debug_operation( 309 ui: &mut Ui, 310 command: &CommandHelper, 311 args: &DebugOperationArgs, 312) -> Result<(), CommandError> { 313 // Resolve the operation without loading the repo, so this command can be used 314 // even if e.g. the view object is broken. 315 let workspace = command.load_workspace()?; 316 let repo_loader = workspace.repo_loader(); 317 let op = op_walk::resolve_op_for_load(repo_loader, &args.operation)?; 318 if args.display == DebugOperationDisplay::Id { 319 writeln!(ui.stdout(), "{}", op.id().hex())?; 320 return Ok(()); 321 } 322 if args.display != DebugOperationDisplay::View { 323 writeln!(ui.stdout(), "{:#?}", op.store_operation())?; 324 } 325 if args.display != DebugOperationDisplay::Operation { 326 writeln!(ui.stdout(), "{:#?}", op.view()?.store_view())?; 327 } 328 Ok(()) 329} 330 331fn cmd_debug_tree( 332 ui: &mut Ui, 333 command: &CommandHelper, 334 args: &DebugTreeArgs, 335) -> Result<(), CommandError> { 336 let workspace_command = command.workspace_helper(ui)?; 337 let tree = if let Some(tree_id_hex) = &args.id { 338 let tree_id = 339 TreeId::try_from_hex(tree_id_hex).map_err(|_| user_error("Invalid tree id"))?; 340 let dir = if let Some(dir_str) = &args.dir { 341 workspace_command.parse_file_path(dir_str)? 342 } else { 343 RepoPathBuf::root() 344 }; 345 let store = workspace_command.repo().store(); 346 let tree = store.get_tree(&dir, &tree_id)?; 347 MergedTree::resolved(tree) 348 } else { 349 let commit = workspace_command 350 .resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; 351 commit.tree()? 352 }; 353 let matcher = workspace_command 354 .parse_file_patterns(&args.paths)? 355 .to_matcher(); 356 for (path, value) in tree.entries_matching(matcher.as_ref()) { 357 let ui_path = workspace_command.format_file_path(&path); 358 writeln!(ui.stdout(), "{ui_path}: {value:?}")?; 359 } 360 361 Ok(()) 362} 363 364#[cfg(feature = "watchman")] 365fn cmd_debug_watchman( 366 ui: &mut Ui, 367 command: &CommandHelper, 368 subcommand: &DebugWatchmanSubcommand, 369) -> Result<(), CommandError> { 370 use jj_lib::local_working_copy::LockedLocalWorkingCopy; 371 372 let mut workspace_command = command.workspace_helper(ui)?; 373 let repo = workspace_command.repo().clone(); 374 match subcommand { 375 DebugWatchmanSubcommand::Status => { 376 // TODO(ilyagr): It would be nice to add colors here 377 match command.settings().fsmonitor_kind()? { 378 jj_lib::fsmonitor::FsmonitorKind::Watchman => { 379 writeln!(ui.stdout(), "Watchman is enabled via `core.fsmonitor`.")? 380 } 381 jj_lib::fsmonitor::FsmonitorKind::None => writeln!( 382 ui.stdout(), 383 "Watchman is disabled. Set `core.fsmonitor=\"watchman\"` to \ 384 enable.\nAttempting to contact the `watchman` CLI regardless..." 385 )?, 386 other_fsmonitor => { 387 return Err(user_error(format!( 388 "This command does not support the currently enabled filesystem monitor: \ 389 {other_fsmonitor:?}." 390 ))) 391 } 392 }; 393 let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; 394 let _ = wc.query_watchman()?; 395 writeln!( 396 ui.stdout(), 397 "The watchman server seems to be installed and working correctly." 398 )?; 399 } 400 DebugWatchmanSubcommand::QueryClock => { 401 let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; 402 let (clock, _changed_files) = wc.query_watchman()?; 403 writeln!(ui.stdout(), "Clock: {clock:?}")?; 404 } 405 DebugWatchmanSubcommand::QueryChangedFiles => { 406 let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; 407 let (_clock, changed_files) = wc.query_watchman()?; 408 writeln!(ui.stdout(), "Changed files: {changed_files:?}")?; 409 } 410 DebugWatchmanSubcommand::ResetClock => { 411 let (mut locked_ws, _commit) = workspace_command.start_working_copy_mutation()?; 412 let Some(locked_local_wc): Option<&mut LockedLocalWorkingCopy> = 413 locked_ws.locked_wc().as_any_mut().downcast_mut() 414 else { 415 return Err(user_error( 416 "This command requires a standard local-disk working copy", 417 )); 418 }; 419 locked_local_wc.reset_watchman()?; 420 locked_ws.finish(repo.op_id().clone())?; 421 writeln!(ui.status(), "Reset Watchman clock")?; 422 } 423 } 424 Ok(()) 425} 426 427#[cfg(not(feature = "watchman"))] 428fn cmd_debug_watchman( 429 _ui: &mut Ui, 430 _command: &CommandHelper, 431 _subcommand: &DebugWatchmanSubcommand, 432) -> Result<(), CommandError> { 433 Err(user_error( 434 "Cannot query Watchman because jj was not compiled with the `watchman` feature", 435 )) 436} 437 438fn check_local_disk_wc(x: &dyn Any) -> Result<&LocalWorkingCopy, CommandError> { 439 x.downcast_ref() 440 .ok_or_else(|| user_error("This command requires a standard local-disk working copy")) 441}