just playing with tangled
at tmp-tutorial 740 lines 27 kB view raw
1// Copyright 2020-2022 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::collections::VecDeque; 16use std::io; 17use std::ops::Range; 18use std::sync::Arc; 19 20use itertools::Itertools; 21use jj_lib::backend::{ObjectId, TreeValue}; 22use jj_lib::commit::Commit; 23use jj_lib::diff::{Diff, DiffHunk}; 24use jj_lib::files::DiffLine; 25use jj_lib::matchers::Matcher; 26use jj_lib::repo::{ReadonlyRepo, Repo}; 27use jj_lib::repo_path::RepoPath; 28use jj_lib::settings::UserSettings; 29use jj_lib::tree::{Tree, TreeDiffIterator}; 30use jj_lib::{diff, files, rewrite, tree}; 31use tracing::instrument; 32 33use crate::cli_util::{CommandError, WorkspaceCommandHelper}; 34use crate::formatter::Formatter; 35 36#[derive(clap::Args, Clone, Debug)] 37#[command(group(clap::ArgGroup::new("short-format").args(&["summary", "types"])))] 38#[command(group(clap::ArgGroup::new("long-format").args(&["git", "color_words"])))] 39pub struct DiffFormatArgs { 40 /// For each path, show only whether it was modified, added, or removed 41 #[arg(long, short)] 42 pub summary: bool, 43 /// For each path, show only its type before and after 44 /// 45 /// The diff is shown as two letters. The first letter indicates the type 46 /// before and the second letter indicates the type after. '-' indicates 47 /// that the path was not present, 'F' represents a regular file, `L' 48 /// represents a symlink, 'C' represents a conflict, and 'G' represents a 49 /// Git submodule. 50 #[arg(long)] 51 pub types: bool, 52 /// Show a Git-format diff 53 #[arg(long)] 54 pub git: bool, 55 /// Show a word-level diff with changes indicated only by color 56 #[arg(long)] 57 pub color_words: bool, 58} 59 60#[derive(Clone, Copy, Debug, Eq, PartialEq)] 61pub enum DiffFormat { 62 Summary, 63 Types, 64 Git, 65 ColorWords, 66} 67 68/// Returns a list of requested diff formats, which will never be empty. 69pub fn diff_formats_for(settings: &UserSettings, args: &DiffFormatArgs) -> Vec<DiffFormat> { 70 let formats = diff_formats_from_args(args); 71 if formats.is_empty() { 72 vec![default_diff_format(settings)] 73 } else { 74 formats 75 } 76} 77 78/// Returns a list of requested diff formats for log-like commands, which may be 79/// empty. 80pub fn diff_formats_for_log( 81 settings: &UserSettings, 82 args: &DiffFormatArgs, 83 patch: bool, 84) -> Vec<DiffFormat> { 85 let mut formats = diff_formats_from_args(args); 86 // --patch implies default if no format other than --summary is specified 87 if patch && matches!(formats.as_slice(), [] | [DiffFormat::Summary]) { 88 formats.push(default_diff_format(settings)); 89 formats.dedup(); 90 } 91 formats 92} 93 94fn diff_formats_from_args(args: &DiffFormatArgs) -> Vec<DiffFormat> { 95 [ 96 (args.summary, DiffFormat::Summary), 97 (args.types, DiffFormat::Types), 98 (args.git, DiffFormat::Git), 99 (args.color_words, DiffFormat::ColorWords), 100 ] 101 .into_iter() 102 .filter_map(|(arg, format)| arg.then_some(format)) 103 .collect() 104} 105 106fn default_diff_format(settings: &UserSettings) -> DiffFormat { 107 match settings 108 .config() 109 .get_string("ui.diff.format") 110 // old config name 111 .or_else(|_| settings.config().get_string("diff.format")) 112 .as_deref() 113 { 114 Ok("summary") => DiffFormat::Summary, 115 Ok("types") => DiffFormat::Types, 116 Ok("git") => DiffFormat::Git, 117 Ok("color-words") => DiffFormat::ColorWords, 118 _ => DiffFormat::ColorWords, 119 } 120} 121 122pub fn show_diff( 123 formatter: &mut dyn Formatter, 124 workspace_command: &WorkspaceCommandHelper, 125 from_tree: &Tree, 126 to_tree: &Tree, 127 matcher: &dyn Matcher, 128 formats: &[DiffFormat], 129) -> Result<(), CommandError> { 130 for format in formats { 131 let tree_diff = from_tree.diff(to_tree, matcher); 132 match format { 133 DiffFormat::Summary => { 134 show_diff_summary(formatter, workspace_command, tree_diff)?; 135 } 136 DiffFormat::Types => { 137 show_types(formatter, workspace_command, tree_diff)?; 138 } 139 DiffFormat::Git => { 140 show_git_diff(formatter, workspace_command, tree_diff)?; 141 } 142 DiffFormat::ColorWords => { 143 show_color_words_diff(formatter, workspace_command, tree_diff)?; 144 } 145 } 146 } 147 Ok(()) 148} 149 150pub fn show_patch( 151 formatter: &mut dyn Formatter, 152 workspace_command: &WorkspaceCommandHelper, 153 commit: &Commit, 154 matcher: &dyn Matcher, 155 formats: &[DiffFormat], 156) -> Result<(), CommandError> { 157 let parents = commit.parents(); 158 let from_tree = rewrite::merge_commit_trees(workspace_command.repo().as_ref(), &parents)?; 159 let to_tree = commit.tree(); 160 show_diff( 161 formatter, 162 workspace_command, 163 &from_tree, 164 &to_tree, 165 matcher, 166 formats, 167 ) 168} 169 170fn show_color_words_diff_hunks( 171 left: &[u8], 172 right: &[u8], 173 formatter: &mut dyn Formatter, 174) -> io::Result<()> { 175 const SKIPPED_CONTEXT_LINE: &str = " ...\n"; 176 let num_context_lines = 3; 177 let mut context = VecDeque::new(); 178 // Have we printed "..." for any skipped context? 179 let mut skipped_context = false; 180 // Are the lines in `context` to be printed before the next modified line? 181 let mut context_before = true; 182 for diff_line in files::diff(left, right) { 183 if diff_line.is_unmodified() { 184 context.push_back(diff_line.clone()); 185 let mut start_skipping_context = false; 186 if context_before { 187 if skipped_context && context.len() > num_context_lines { 188 context.pop_front(); 189 } else if !skipped_context && context.len() > num_context_lines + 1 { 190 start_skipping_context = true; 191 } 192 } else if context.len() > num_context_lines * 2 + 1 { 193 for line in context.drain(..num_context_lines) { 194 show_color_words_diff_line(formatter, &line)?; 195 } 196 start_skipping_context = true; 197 } 198 if start_skipping_context { 199 context.drain(..2); 200 formatter.write_str(SKIPPED_CONTEXT_LINE)?; 201 skipped_context = true; 202 context_before = true; 203 } 204 } else { 205 for line in &context { 206 show_color_words_diff_line(formatter, line)?; 207 } 208 context.clear(); 209 show_color_words_diff_line(formatter, &diff_line)?; 210 context_before = false; 211 skipped_context = false; 212 } 213 } 214 if !context_before { 215 if context.len() > num_context_lines + 1 { 216 context.truncate(num_context_lines); 217 skipped_context = true; 218 context_before = true; 219 } 220 for line in &context { 221 show_color_words_diff_line(formatter, line)?; 222 } 223 if context_before { 224 formatter.write_str(SKIPPED_CONTEXT_LINE)?; 225 } 226 } 227 228 // If the last diff line doesn't end with newline, add it. 229 let no_hunk = left.is_empty() && right.is_empty(); 230 let any_last_newline = left.ends_with(b"\n") || right.ends_with(b"\n"); 231 if !skipped_context && !no_hunk && !any_last_newline { 232 formatter.write_str("\n")?; 233 } 234 235 Ok(()) 236} 237 238fn show_color_words_diff_line( 239 formatter: &mut dyn Formatter, 240 diff_line: &DiffLine, 241) -> io::Result<()> { 242 if diff_line.has_left_content { 243 write!( 244 formatter.labeled("removed"), 245 "{:>4}", 246 diff_line.left_line_number 247 )?; 248 formatter.write_str(" ")?; 249 } else { 250 formatter.write_str(" ")?; 251 } 252 if diff_line.has_right_content { 253 write!( 254 formatter.labeled("added"), 255 "{:>4}", 256 diff_line.right_line_number 257 )?; 258 formatter.write_str(": ")?; 259 } else { 260 formatter.write_str(" : ")?; 261 } 262 for hunk in &diff_line.hunks { 263 match hunk { 264 DiffHunk::Matching(data) => { 265 formatter.write_all(data)?; 266 } 267 DiffHunk::Different(data) => { 268 let before = data[0]; 269 let after = data[1]; 270 if !before.is_empty() { 271 formatter.with_label("removed", |formatter| formatter.write_all(before))?; 272 } 273 if !after.is_empty() { 274 formatter.with_label("added", |formatter| formatter.write_all(after))?; 275 } 276 } 277 } 278 } 279 280 Ok(()) 281} 282 283fn diff_content( 284 repo: &Arc<ReadonlyRepo>, 285 path: &RepoPath, 286 value: &TreeValue, 287) -> Result<Vec<u8>, CommandError> { 288 match value { 289 TreeValue::File { id, .. } => { 290 let mut file_reader = repo.store().read_file(path, id).unwrap(); 291 let mut content = vec![]; 292 file_reader.read_to_end(&mut content)?; 293 Ok(content) 294 } 295 TreeValue::Symlink(id) => { 296 let target = repo.store().read_symlink(path, id)?; 297 Ok(target.into_bytes()) 298 } 299 TreeValue::Tree(_) => { 300 panic!( 301 "Got an unexpected tree in a diff of path {}", 302 path.to_internal_file_string() 303 ); 304 } 305 TreeValue::GitSubmodule(id) => { 306 Ok(format!("Git submodule checked out at {}", id.hex()).into_bytes()) 307 } 308 TreeValue::Conflict(id) => { 309 let conflict = repo.store().read_conflict(path, id).unwrap(); 310 let mut content = vec![]; 311 conflict 312 .materialize(repo.store(), path, &mut content) 313 .unwrap(); 314 Ok(content) 315 } 316 } 317} 318 319fn basic_diff_file_type(value: &TreeValue) -> String { 320 match value { 321 TreeValue::File { executable, .. } => { 322 if *executable { 323 "executable file".to_string() 324 } else { 325 "regular file".to_string() 326 } 327 } 328 TreeValue::Symlink(_) => "symlink".to_string(), 329 TreeValue::Tree(_) => "tree".to_string(), 330 TreeValue::GitSubmodule(_) => "Git submodule".to_string(), 331 TreeValue::Conflict(_) => "conflict".to_string(), 332 } 333} 334 335pub fn show_color_words_diff( 336 formatter: &mut dyn Formatter, 337 workspace_command: &WorkspaceCommandHelper, 338 tree_diff: TreeDiffIterator, 339) -> Result<(), CommandError> { 340 let repo = workspace_command.repo(); 341 formatter.push_label("diff")?; 342 for (path, diff) in tree_diff { 343 let ui_path = workspace_command.format_file_path(&path); 344 match diff { 345 tree::Diff::Added(right_value) => { 346 let right_content = diff_content(repo, &path, &right_value)?; 347 let description = basic_diff_file_type(&right_value); 348 writeln!( 349 formatter.labeled("header"), 350 "Added {description} {ui_path}:" 351 )?; 352 show_color_words_diff_hunks(&[], &right_content, formatter)?; 353 } 354 tree::Diff::Modified(left_value, right_value) => { 355 let left_content = diff_content(repo, &path, &left_value)?; 356 let right_content = diff_content(repo, &path, &right_value)?; 357 let description = match (left_value, right_value) { 358 ( 359 TreeValue::File { 360 executable: left_executable, 361 .. 362 }, 363 TreeValue::File { 364 executable: right_executable, 365 .. 366 }, 367 ) => { 368 if left_executable && right_executable { 369 "Modified executable file".to_string() 370 } else if left_executable { 371 "Executable file became non-executable at".to_string() 372 } else if right_executable { 373 "Non-executable file became executable at".to_string() 374 } else { 375 "Modified regular file".to_string() 376 } 377 } 378 (TreeValue::Conflict(_), TreeValue::Conflict(_)) => { 379 "Modified conflict in".to_string() 380 } 381 (TreeValue::Conflict(_), _) => "Resolved conflict in".to_string(), 382 (_, TreeValue::Conflict(_)) => "Created conflict in".to_string(), 383 (TreeValue::Symlink(_), TreeValue::Symlink(_)) => { 384 "Symlink target changed at".to_string() 385 } 386 (left_value, right_value) => { 387 let left_type = basic_diff_file_type(&left_value); 388 let right_type = basic_diff_file_type(&right_value); 389 let (first, rest) = left_type.split_at(1); 390 format!( 391 "{}{} became {} at", 392 first.to_ascii_uppercase(), 393 rest, 394 right_type 395 ) 396 } 397 }; 398 writeln!(formatter.labeled("header"), "{description} {ui_path}:")?; 399 show_color_words_diff_hunks(&left_content, &right_content, formatter)?; 400 } 401 tree::Diff::Removed(left_value) => { 402 let left_content = diff_content(repo, &path, &left_value)?; 403 let description = basic_diff_file_type(&left_value); 404 writeln!( 405 formatter.labeled("header"), 406 "Removed {description} {ui_path}:" 407 )?; 408 show_color_words_diff_hunks(&left_content, &[], formatter)?; 409 } 410 } 411 } 412 formatter.pop_label()?; 413 Ok(()) 414} 415 416struct GitDiffPart { 417 mode: String, 418 hash: String, 419 content: Vec<u8>, 420} 421 422fn git_diff_part( 423 repo: &Arc<ReadonlyRepo>, 424 path: &RepoPath, 425 value: &TreeValue, 426) -> Result<GitDiffPart, CommandError> { 427 let mode; 428 let hash; 429 let mut content = vec![]; 430 match value { 431 TreeValue::File { id, executable } => { 432 mode = if *executable { 433 "100755".to_string() 434 } else { 435 "100644".to_string() 436 }; 437 hash = id.hex(); 438 let mut file_reader = repo.store().read_file(path, id).unwrap(); 439 file_reader.read_to_end(&mut content)?; 440 } 441 TreeValue::Symlink(id) => { 442 mode = "120000".to_string(); 443 hash = id.hex(); 444 let target = repo.store().read_symlink(path, id)?; 445 content = target.into_bytes(); 446 } 447 TreeValue::Tree(_) => { 448 panic!( 449 "Got an unexpected tree in a diff of path {}", 450 path.to_internal_file_string() 451 ); 452 } 453 TreeValue::GitSubmodule(id) => { 454 // TODO: What should we actually do here? 455 mode = "040000".to_string(); 456 hash = id.hex(); 457 } 458 TreeValue::Conflict(id) => { 459 mode = "100644".to_string(); 460 hash = id.hex(); 461 let conflict = repo.store().read_conflict(path, id).unwrap(); 462 conflict 463 .materialize(repo.store(), path, &mut content) 464 .unwrap(); 465 } 466 } 467 let hash = hash[0..10].to_string(); 468 Ok(GitDiffPart { 469 mode, 470 hash, 471 content, 472 }) 473} 474 475#[derive(PartialEq)] 476enum DiffLineType { 477 Context, 478 Removed, 479 Added, 480} 481 482struct UnifiedDiffHunk<'content> { 483 left_line_range: Range<usize>, 484 right_line_range: Range<usize>, 485 lines: Vec<(DiffLineType, &'content [u8])>, 486} 487 488fn unified_diff_hunks<'content>( 489 left_content: &'content [u8], 490 right_content: &'content [u8], 491 num_context_lines: usize, 492) -> Vec<UnifiedDiffHunk<'content>> { 493 let mut hunks = vec![]; 494 let mut current_hunk = UnifiedDiffHunk { 495 left_line_range: 1..1, 496 right_line_range: 1..1, 497 lines: vec![], 498 }; 499 let mut show_context_after = false; 500 let diff = Diff::for_tokenizer(&[left_content, right_content], &diff::find_line_ranges); 501 for hunk in diff.hunks() { 502 match hunk { 503 DiffHunk::Matching(content) => { 504 let lines = content.split_inclusive(|b| *b == b'\n').collect_vec(); 505 // Number of context lines to print after the previous non-matching hunk. 506 let num_after_lines = lines.len().min(if show_context_after { 507 num_context_lines 508 } else { 509 0 510 }); 511 current_hunk.left_line_range.end += num_after_lines; 512 current_hunk.right_line_range.end += num_after_lines; 513 for line in lines.iter().take(num_after_lines) { 514 current_hunk.lines.push((DiffLineType::Context, line)); 515 } 516 let num_skip_lines = lines 517 .len() 518 .saturating_sub(num_after_lines) 519 .saturating_sub(num_context_lines); 520 if num_skip_lines > 0 { 521 let left_start = current_hunk.left_line_range.end + num_skip_lines; 522 let right_start = current_hunk.right_line_range.end + num_skip_lines; 523 if !current_hunk.lines.is_empty() { 524 hunks.push(current_hunk); 525 } 526 current_hunk = UnifiedDiffHunk { 527 left_line_range: left_start..left_start, 528 right_line_range: right_start..right_start, 529 lines: vec![], 530 }; 531 } 532 let num_before_lines = lines.len() - num_after_lines - num_skip_lines; 533 current_hunk.left_line_range.end += num_before_lines; 534 current_hunk.right_line_range.end += num_before_lines; 535 for line in lines.iter().skip(num_after_lines + num_skip_lines) { 536 current_hunk.lines.push((DiffLineType::Context, line)); 537 } 538 } 539 DiffHunk::Different(content) => { 540 show_context_after = true; 541 let left_lines = content[0].split_inclusive(|b| *b == b'\n').collect_vec(); 542 let right_lines = content[1].split_inclusive(|b| *b == b'\n').collect_vec(); 543 if !left_lines.is_empty() { 544 current_hunk.left_line_range.end += left_lines.len(); 545 for line in left_lines { 546 current_hunk.lines.push((DiffLineType::Removed, line)); 547 } 548 } 549 if !right_lines.is_empty() { 550 current_hunk.right_line_range.end += right_lines.len(); 551 for line in right_lines { 552 current_hunk.lines.push((DiffLineType::Added, line)); 553 } 554 } 555 } 556 } 557 } 558 if !current_hunk 559 .lines 560 .iter() 561 .all(|(diff_type, _line)| *diff_type == DiffLineType::Context) 562 { 563 hunks.push(current_hunk); 564 } 565 hunks 566} 567 568fn show_unified_diff_hunks( 569 formatter: &mut dyn Formatter, 570 left_content: &[u8], 571 right_content: &[u8], 572) -> Result<(), CommandError> { 573 for hunk in unified_diff_hunks(left_content, right_content, 3) { 574 writeln!( 575 formatter.labeled("hunk_header"), 576 "@@ -{},{} +{},{} @@", 577 hunk.left_line_range.start, 578 hunk.left_line_range.len(), 579 hunk.right_line_range.start, 580 hunk.right_line_range.len() 581 )?; 582 for (line_type, content) in hunk.lines { 583 match line_type { 584 DiffLineType::Context => { 585 formatter.with_label("context", |formatter| { 586 formatter.write_str(" ")?; 587 formatter.write_all(content) 588 })?; 589 } 590 DiffLineType::Removed => { 591 formatter.with_label("removed", |formatter| { 592 formatter.write_str("-")?; 593 formatter.write_all(content) 594 })?; 595 } 596 DiffLineType::Added => { 597 formatter.with_label("added", |formatter| { 598 formatter.write_str("+")?; 599 formatter.write_all(content) 600 })?; 601 } 602 } 603 if !content.ends_with(b"\n") { 604 formatter.write_str("\n\\ No newline at end of file\n")?; 605 } 606 } 607 } 608 Ok(()) 609} 610 611pub fn show_git_diff( 612 formatter: &mut dyn Formatter, 613 workspace_command: &WorkspaceCommandHelper, 614 tree_diff: TreeDiffIterator, 615) -> Result<(), CommandError> { 616 let repo = workspace_command.repo(); 617 formatter.push_label("diff")?; 618 for (path, diff) in tree_diff { 619 let path_string = path.to_internal_file_string(); 620 match diff { 621 tree::Diff::Added(right_value) => { 622 let right_part = git_diff_part(repo, &path, &right_value)?; 623 formatter.with_label("file_header", |formatter| { 624 writeln!(formatter, "diff --git a/{path_string} b/{path_string}")?; 625 writeln!(formatter, "new file mode {}", &right_part.mode)?; 626 writeln!(formatter, "index 0000000000..{}", &right_part.hash)?; 627 writeln!(formatter, "--- /dev/null")?; 628 writeln!(formatter, "+++ b/{path_string}") 629 })?; 630 show_unified_diff_hunks(formatter, &[], &right_part.content)?; 631 } 632 tree::Diff::Modified(left_value, right_value) => { 633 let left_part = git_diff_part(repo, &path, &left_value)?; 634 let right_part = git_diff_part(repo, &path, &right_value)?; 635 formatter.with_label("file_header", |formatter| { 636 writeln!(formatter, "diff --git a/{path_string} b/{path_string}")?; 637 if left_part.mode != right_part.mode { 638 writeln!(formatter, "old mode {}", &left_part.mode)?; 639 writeln!(formatter, "new mode {}", &right_part.mode)?; 640 if left_part.hash != right_part.hash { 641 writeln!(formatter, "index {}...{}", &left_part.hash, right_part.hash)?; 642 } 643 } else if left_part.hash != right_part.hash { 644 writeln!( 645 formatter, 646 "index {}...{} {}", 647 &left_part.hash, right_part.hash, left_part.mode 648 )?; 649 } 650 if left_part.content != right_part.content { 651 writeln!(formatter, "--- a/{path_string}")?; 652 writeln!(formatter, "+++ b/{path_string}")?; 653 } 654 Ok(()) 655 })?; 656 show_unified_diff_hunks(formatter, &left_part.content, &right_part.content)?; 657 } 658 tree::Diff::Removed(left_value) => { 659 let left_part = git_diff_part(repo, &path, &left_value)?; 660 formatter.with_label("file_header", |formatter| { 661 writeln!(formatter, "diff --git a/{path_string} b/{path_string}")?; 662 writeln!(formatter, "deleted file mode {}", &left_part.mode)?; 663 writeln!(formatter, "index {}..0000000000", &left_part.hash)?; 664 writeln!(formatter, "--- a/{path_string}")?; 665 writeln!(formatter, "+++ /dev/null") 666 })?; 667 show_unified_diff_hunks(formatter, &left_part.content, &[])?; 668 } 669 } 670 } 671 formatter.pop_label()?; 672 Ok(()) 673} 674 675#[instrument(skip_all)] 676pub fn show_diff_summary( 677 formatter: &mut dyn Formatter, 678 workspace_command: &WorkspaceCommandHelper, 679 tree_diff: TreeDiffIterator, 680) -> io::Result<()> { 681 formatter.with_label("diff", |formatter| { 682 for (repo_path, diff) in tree_diff { 683 match diff { 684 tree::Diff::Modified(_, _) => { 685 writeln!( 686 formatter.labeled("modified"), 687 "M {}", 688 workspace_command.format_file_path(&repo_path) 689 )?; 690 } 691 tree::Diff::Added(_) => { 692 writeln!( 693 formatter.labeled("added"), 694 "A {}", 695 workspace_command.format_file_path(&repo_path) 696 )?; 697 } 698 tree::Diff::Removed(_) => { 699 writeln!( 700 formatter.labeled("removed"), 701 "R {}", 702 workspace_command.format_file_path(&repo_path) 703 )?; 704 } 705 } 706 } 707 Ok(()) 708 }) 709} 710 711pub fn show_types( 712 formatter: &mut dyn Formatter, 713 workspace_command: &WorkspaceCommandHelper, 714 tree_diff: TreeDiffIterator, 715) -> io::Result<()> { 716 formatter.with_label("diff", |formatter| { 717 for (repo_path, diff) in tree_diff { 718 let (before, after) = diff.into_options(); 719 writeln!( 720 formatter.labeled("modified"), 721 "{}{} {}", 722 diff_summary_char(before.as_ref()), 723 diff_summary_char(after.as_ref()), 724 workspace_command.format_file_path(&repo_path) 725 )?; 726 } 727 Ok(()) 728 }) 729} 730 731fn diff_summary_char(value: Option<&TreeValue>) -> char { 732 match value { 733 None => '-', 734 Some(TreeValue::File { .. }) => 'F', 735 Some(TreeValue::Symlink(_)) => 'L', 736 Some(TreeValue::GitSubmodule(_)) => 'G', 737 Some(TreeValue::Conflict(_)) => 'C', 738 Some(TreeValue::Tree(_)) => panic!("unexpected tree entry in diff"), 739 } 740}