just playing with tangled
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}