Make primary command output identify commits by both their abbreviated object ID and equally sized reverse-hex change ID. This covers mutation results, rewritten refs, pins, notices, raw ref-tree tips, and CLI rebase documents while leaving diagnostics, serialized todo state, and TUI editor output unchanged.
Unified · Split
| @@ -67,6 +67,12 @@ without trading responsiveness for metadata that is not visible. · 66 unmodified lines | |||
| 67 | line. Editing that title preserves an existing message body byte-for-byte, | 67 | line. Editing that title preserves an existing message body byte-for-byte, |
| 68 | while commenting, deleting, or emptying the header removes the whole message. | 68 | while commenting, deleting, or emptying the header removes the whole message. |
| 69 | Explicit `-m` and `-f` messages preserve enrichments. | 69 | Explicit `-m` and `-f` messages preserve enrichments. |
| 70 | - Primary command output follows every displayed abbreviated commit hash with | ||
| 71 | its reverse-hex change ID. The two abbreviations have equal widths. This | ||
| 72 | applies to mutation results, rewritten-ref mappings, pins, stash and travel | ||
| 73 | notices, raw commit labels in `ref-tree`, and visible commit identifiers in | ||
| 74 | rebase todos. Diagnostics on stderr and the full object IDs in the hidden | ||
| 75 | `tix-rebase-state-v2` block remain unchanged. | ||
| 70 | - `-x/--hide REVSPEC` excludes the revision and its reachable ancestry. The | 76 | - `-x/--hide REVSPEC` excludes the revision and its reachable ancestry. The |
| 71 | option may be repeated. | 77 | option may be repeated. |
| 72 | - `-h/--help` prints Clap's standard help for `tix` and every subcommand. | 78 | - `-h/--help` prints Clap's standard help for `tix` and every subcommand. |
| @@ -9,6 +9,7 @@ use gix::{ · 8 unmodified lines | |||
| 9 | ObjectId, | 9 | ObjectId, |
| 10 | bstr::{BStr, BString}, | 10 | bstr::{BStr, BString}, |
| 11 | hash::ChangeId, | 11 | hash::ChangeId, |
| 12 | prelude::ObjectIdExt, | ||
| 12 | }; | 13 | }; |
| 13 | 14 | ||
| 14 | pub(crate) const HEADER: &str = "change-id"; | 15 | pub(crate) const HEADER: &str = "change-id"; |
| @@ -27,6 +28,26 @@ pub(crate) fn for_commit(repo: &gix::Repository, id: ObjectId) -> Result<ChangeI · 12 unmodified lines | |||
| 27 | Ok(effective(id, commit.extra_headers().find_all(HEADER))) | 28 | Ok(effective(id, commit.extra_headers().find_all(HEADER))) |
| 28 | } | 29 | } |
| 29 | 30 | ||
| 31 | pub(crate) fn display(repo: &gix::Repository, id: ObjectId, len: usize) -> Result<String> { | ||
| 32 | Ok(format!( | ||
| 33 | "{} {}", | ||
| 34 | id.to_hex_with_len(len), | ||
| 35 | for_commit(repo, id)?.to_reverse_hex_with_len(len) | ||
| 36 | )) | ||
| 37 | } | ||
| 38 | |||
| 39 | pub(crate) fn display_short(repo: &gix::Repository, id: ObjectId) -> Result<String> { | ||
| 40 | let hash = id | ||
| 41 | .attach(repo) | ||
| 42 | .shorten() | ||
| 43 | .context("could not shorten commit ID")? | ||
| 44 | .to_string(); | ||
| 45 | Ok(format!( | ||
| 46 | "{hash} {}", | ||
| 47 | for_commit(repo, id)?.to_reverse_hex_with_len(hash.len()) | ||
| 48 | )) | ||
| 49 | } | ||
| 50 | |||
| 30 | pub(crate) fn abbreviations( | 51 | pub(crate) fn abbreviations( |
| 31 | repo: &gix::Repository, | 52 | repo: &gix::Repository, |
| 32 | ids: impl IntoIterator<Item = ObjectId>, | 53 | ids: impl IntoIterator<Item = ObjectId>, |
| @@ -7,7 +7,7 @@ use std::{ · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | use anyhow::{Context, Result}; | 8 | use anyhow::{Context, Result}; |
| 9 | use clap::Parser; | 9 | use clap::Parser; |
| 10 | use gix::prelude::{ObjectIdExt, ReferenceExt}; | 10 | use gix::prelude::ReferenceExt; |
| 11 | use ratatui::text::Line; | 11 | use ratatui::text::Line; |
| 12 | 12 | ||
| 13 | mod new; | 13 | mod new; |
| @@ -148,6 +148,7 @@ impl Platform { · 134 unmodified lines | |||
| 148 | Command::RefTree(_) | Command::Show(_) => unreachable!("display commands return before logging"), | 148 | Command::RefTree(_) | Command::Show(_) => unreachable!("display commands return before logging"), |
| 149 | Command::Amend(args) => { | 149 | Command::Amend(args) => { |
| 150 | let graph = crate::edit::loaded_view_graph(&repository)?; | 150 | let graph = crate::edit::loaded_view_graph(&repository)?; |
| 151 | let output_repository = repository.clone(); | ||
| 151 | let amended = if args.index { | 152 | let amended = if args.index { |
| 152 | crate::edit::head::amend_index_reporting(repository, &graph)? | 153 | crate::edit::head::amend_index_reporting(repository, &graph)? |
| 153 | } else { | 154 | } else { |
| @@ -155,14 +156,9 @@ impl Platform { · 1 unmodified line | |||
| 155 | }; | 156 | }; |
| 156 | match amended { | 157 | match amended { |
| 157 | Some(outcome) => { | 158 | Some(outcome) => { |
| 158 | println!( | 159 | let selected = outcome.selected.context("amending did not produce a selection")?; |
| 159 | "{}", | 160 | println!("{}", crate::change_id::display(&output_repository, selected, 7)?); |
| 160 | outcome | 161 | print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?; |
| 161 | .selected | ||
| 162 | .context("amending did not produce a selection")? | ||
| 163 | .to_hex_with_len(7) | ||
| 164 | ); | ||
| 165 | print_ref_rewrites(&outcome.ref_rewrites); | ||
| 166 | } | 162 | } |
| 167 | None => println!("nothing to amend"), | 163 | None => println!("nothing to amend"), |
| 168 | } | 164 | } |
| @@ -180,10 +176,8 @@ impl Platform { · 11 unmodified lines | |||
| 180 | .head_id() | 176 | .head_id() |
| 181 | .context("stashing changes requires a born HEAD")? | 177 | .context("stashing changes requires a born HEAD")? |
| 182 | .detach(); | 178 | .detach(); |
| 183 | println!( | 179 | let notice = crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)?; |
| 184 | "{}", | 180 | println!("{}", notice_with_change_id(&repository, ¬ice, id)?); |
| 185 | crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)? | ||
| 186 | ); | ||
| 187 | } | 181 | } |
| 188 | Command::Pin(args) => pin(&repository, args)?, | 182 | Command::Pin(args) => pin(&repository, args)?, |
| 189 | Command::Travel(args) => return travel::run(repository, args), | 183 | Command::Travel(args) => return travel::run(repository, args), |
| @@ -458,10 +452,7 @@ fn display_pin(repository: &gix::Repository, pin: &crate::history::Pin) -> Resul · 268 unmodified lines | |||
| 458 | Ok(format!( | 452 | Ok(format!( |
| 459 | "{} {}", | 453 | "{} {}", |
| 460 | crate::edit::time_travel::pin_label(pin), | 454 | crate::edit::time_travel::pin_label(pin), |
| 461 | pin.id | 455 | crate::change_id::display_short(repository, pin.id)? |
| 462 | .attach(repository) | ||
| 463 | .shorten() | ||
| 464 | .context("could not shorten pinned commit ID")? | ||
| 465 | )) | 456 | )) |
| 466 | } | 457 | } |
| 467 | 458 | ||
| @@ -471,16 +462,12 @@ fn edit_head( · 3 unmodified lines | |||
| 471 | kind: crate::edit::head::Kind, | 462 | kind: crate::edit::head::Kind, |
| 472 | verb: &str, | 463 | verb: &str, |
| 473 | ) -> Result<()> { | 464 | ) -> Result<()> { |
| 465 | let output_repository = repository.clone(); | ||
| 474 | match crate::edit::head::perform_reporting(repository, graph, kind)? { | 466 | match crate::edit::head::perform_reporting(repository, graph, kind)? { |
| 475 | Some(outcome) => { | 467 | Some(outcome) => { |
| 476 | println!( | 468 | let selected = outcome.selected.context("editing HEAD did not produce a selection")?; |
| 477 | "{}", | 469 | println!("{}", crate::change_id::display(&output_repository, selected, 7)?); |
| 478 | outcome | 470 | print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?; |
| 479 | .selected | ||
| 480 | .context("editing HEAD did not produce a selection")? | ||
| 481 | .to_hex_with_len(7) | ||
| 482 | ); | ||
| 483 | print_ref_rewrites(&outcome.ref_rewrites); | ||
| 484 | } | 471 | } |
| 485 | None => println!("nothing to {verb}"), | 472 | None => println!("nothing to {verb}"), |
| 486 | } | 473 | } |
| @@ -504,40 +491,46 @@ fn split(repository: gix::Repository, graph: &crate::history::HistoryGraph, args · 17 unmodified lines | |||
| 504 | .context("could not reopen repository after editing split")?; | 491 | .context("could not reopen repository after editing split")?; |
| 505 | repository.object_cache_size(None); | 492 | repository.object_cache_size(None); |
| 506 | let outcome = crate::edit::split::apply_reporting(repository, graph, prepared, &edited)?; | 493 | let outcome = crate::edit::split::apply_reporting(repository, graph, prepared, &edited)?; |
| 507 | println!( | 494 | let output_repository = |
| 508 | "{}", | 495 | crate::open_repository(&repository_path, bare, false).context("could not reopen repository after splitting")?; |
| 509 | outcome | 496 | let selected = outcome.selected.context("splitting did not produce a selection")?; |
| 510 | .selected | 497 | println!("{}", crate::change_id::display(&output_repository, selected, 7)?); |
| 511 | .context("splitting did not produce a selection")? | 498 | print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?; |
| 512 | .to_hex_with_len(7) | ||
| 513 | ); | ||
| 514 | print_ref_rewrites(&outcome.ref_rewrites); | ||
| 515 | Ok(()) | 499 | Ok(()) |
| 516 | } | 500 | } |
| 517 | 501 | ||
| 518 | fn print_ref_rewrites(rewrites: &[crate::edit::rebase::RefRewrite]) { | 502 | fn print_ref_rewrites(repository: &gix::Repository, rewrites: &[crate::edit::rebase::RefRewrite]) -> Result<()> { |
| 519 | for line in ref_rewrite_lines(rewrites) { | 503 | for line in ref_rewrite_lines(repository, rewrites)? { |
| 520 | println!("{line}"); | 504 | println!("{line}"); |
| 521 | } | 505 | } |
| 506 | Ok(()) | ||
| 522 | } | 507 | } |
| 523 | 508 | ||
| 524 | fn ref_rewrite_lines(rewrites: &[crate::edit::rebase::RefRewrite]) -> Vec<String> { | 509 | fn ref_rewrite_lines( |
| 510 | repository: &gix::Repository, | ||
| 511 | rewrites: &[crate::edit::rebase::RefRewrite], | ||
| 512 | ) -> Result<Vec<String>> { | ||
| 525 | let mut rewrites = rewrites.to_vec(); | 513 | let mut rewrites = rewrites.to_vec(); |
| 526 | rewrites.sort_by(|a, b| a.name.cmp(&b.name)); | 514 | rewrites.sort_by(|a, b| a.name.cmp(&b.name)); |
| 527 | rewrites.dedup(); | 515 | rewrites.dedup(); |
| 528 | rewrites | 516 | rewrites |
| 529 | .into_iter() | 517 | .into_iter() |
| 530 | .map(|rewrite| { | 518 | .map(|rewrite| { |
| 531 | format!( | 519 | Ok(format!( |
| 532 | "{}: {} -> {}", | 520 | "{}: {} -> {}", |
| 533 | rewrite.name, | 521 | rewrite.name, |
| 534 | rewrite.old.to_hex_with_len(7), | 522 | crate::change_id::display(repository, rewrite.old, 7)?, |
| 535 | rewrite.new.to_hex_with_len(7) | 523 | crate::change_id::display(repository, rewrite.new, 7)? |
| 536 | ) | 524 | )) |
| 537 | }) | 525 | }) |
| 538 | .collect() | 526 | .collect() |
| 539 | } | 527 | } |
| 540 | 528 | ||
| 529 | fn notice_with_change_id(repository: &gix::Repository, notice: &str, id: gix::ObjectId) -> Result<String> { | ||
| 530 | let hash = id.to_hex_with_len(7).to_string(); | ||
| 531 | Ok(notice.replacen(&hash, &crate::change_id::display(repository, id, 7)?, 1)) | ||
| 532 | } | ||
| 533 | |||
| 541 | #[cfg(test)] | 534 | #[cfg(test)] |
| 542 | mod tests { | 535 | mod tests { |
| 543 | use std::{path::Path, process::Command as ProcessCommand}; | 536 | use std::{path::Path, process::Command as ProcessCommand}; |
| @@ -547,13 +540,11 @@ mod tests { · 3 unmodified lines | |||
| 547 | use super::*; | 540 | use super::*; |
| 548 | 541 | ||
| 549 | #[test] | 542 | #[test] |
| 550 | fn rewritten_ref_lines_are_sorted_and_show_the_commit_mapping() { | 543 | fn rewritten_ref_lines_are_sorted_and_show_the_commit_mapping() -> gix_testtools::Result { |
| 551 | let old: gix::ObjectId = "1111111111111111111111111111111111111111" | 544 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; |
| 552 | .parse() | 545 | let repository = crate::test_repository::open(fixture.path())?; |
| 553 | .expect("valid object ID"); | 546 | let old = repository.rev_parse_single("main~1")?.detach(); |
| 554 | let new: gix::ObjectId = "2222222222222222222222222222222222222222" | 547 | let new = repository.rev_parse_single("main")?.detach(); |
| 555 | .parse() | ||
| 556 | .expect("valid object ID"); | ||
| 557 | let branch = crate::edit::rebase::RefRewrite { | 548 | let branch = crate::edit::rebase::RefRewrite { |
| 558 | name: "refs/heads/z".try_into().expect("valid ref name"), | 549 | name: "refs/heads/z".try_into().expect("valid ref name"), |
| 559 | old, | 550 | old, |
| @@ -565,11 +556,44 @@ mod tests { · 5 unmodified lines | |||
| 565 | new, | 556 | new, |
| 566 | }; | 557 | }; |
| 567 | assert_eq!( | 558 | assert_eq!( |
| 568 | ref_rewrite_lines(&[branch.clone(), first, branch]), | 559 | ref_rewrite_lines(&repository, &[branch.clone(), first, branch])?, |
| 569 | ["refs/heads/a: 1111111 -> 2222222", "refs/heads/z: 1111111 -> 2222222"], | 560 | [ |
| 561 | format!( | ||
| 562 | "refs/heads/a: {} -> {}", | ||
| 563 | crate::change_id::display(&repository, old, 7)?, | ||
| 564 | crate::change_id::display(&repository, new, 7)? | ||
| 565 | ), | ||
| 566 | format!( | ||
| 567 | "refs/heads/z: {} -> {}", | ||
| 568 | crate::change_id::display(&repository, old, 7)?, | ||
| 569 | crate::change_id::display(&repository, new, 7)? | ||
| 570 | ) | ||
| 571 | ], | ||
| 570 | "ref mappings are stable and duplicate-free" | 572 | "ref mappings are stable and duplicate-free" |
| 571 | ); | 573 | ); |
| 572 | assert!(ref_rewrite_lines(&[]).is_empty(), "unchanged refs add no output"); | 574 | assert!( |
| 575 | ref_rewrite_lines(&repository, &[])?.is_empty(), | ||
| 576 | "unchanged refs add no output" | ||
| 577 | ); | ||
| 578 | Ok(()) | ||
| 579 | } | ||
| 580 | |||
| 581 | #[test] | ||
| 582 | fn commit_notices_pair_their_hash_with_the_change_id() -> gix_testtools::Result { | ||
| 583 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; | ||
| 584 | let repository = crate::test_repository::open(fixture.path())?; | ||
| 585 | let id = repository.head_id()?.detach(); | ||
| 586 | let notice = format!("stashed changes at {}; retained warning", id.to_hex_with_len(7)); | ||
| 587 | |||
| 588 | assert_eq!( | ||
| 589 | notice_with_change_id(&repository, ¬ice, id)?, | ||
| 590 | format!( | ||
| 591 | "stashed changes at {}; retained warning", | ||
| 592 | crate::change_id::display(&repository, id, 7)? | ||
| 593 | ), | ||
| 594 | "the change ID stays adjacent to the hash without disturbing later text" | ||
| 595 | ); | ||
| 596 | Ok(()) | ||
| 573 | } | 597 | } |
| 574 | 598 | ||
| 575 | #[test] | 599 | #[test] |
| @@ -951,12 +975,12 @@ mod tests { · 375 unmodified lines | |||
| 951 | assert_eq!(repeated[0].name, pins[0].name, "an existing symbolic pin is reused"); | 975 | assert_eq!(repeated[0].name, pins[0].name, "an existing symbolic pin is reused"); |
| 952 | assert_eq!(crate::history::all_pins(&repository)?.len(), 4); | 976 | assert_eq!(crate::history::all_pins(&repository)?.len(), 4); |
| 953 | let display = display_pin(&repository, &pins[0])?; | 977 | let display = display_pin(&repository, &pins[0])?; |
| 954 | let (label, short_id) = display.split_once(' ').context("pin output has a label and ID")?; | 978 | let (label, ids) = display.split_once(' ').context("pin output has a label and IDs")?; |
| 955 | assert!(label.starts_with("pin:"), "output names the pin"); | 979 | assert!(label.starts_with("pin:"), "output names the pin"); |
| 956 | assert_eq!( | 980 | assert_eq!( |
| 957 | short_id, | 981 | ids, |
| 958 | main.attach(&repository).shorten()?.to_string(), | 982 | crate::change_id::display_short(&repository, main)?, |
| 959 | "output uses the repository's abbreviated ID" | 983 | "output uses matching repository-abbreviated commit and change IDs" |
| 960 | ); | 984 | ); |
| 961 | repository.reference( | 985 | repository.reference( |
| 962 | "refs/heads/main", | 986 | "refs/heads/main", |
| @@ -72,14 +72,13 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 71 unmodified lines | |||
| 72 | repository.object_cache_size(None); | 72 | repository.object_cache_size(None); |
| 73 | crate::edit::create::apply_reporting(repository, &graph, prepared, &edited)? | 73 | crate::edit::create::apply_reporting(repository, &graph, prepared, &edited)? |
| 74 | }; | 74 | }; |
| 75 | println!( | 75 | let repository = crate::open_repository(&repository_path, bare, false) |
| 76 | "{}", | 76 | .context("could not reopen repository after creating commit")?; |
| 77 | outcome | 77 | let selected = outcome |
| 78 | .selected | 78 | .selected |
| 79 | .context("creating a commit did not produce a selection")? | 79 | .context("creating a commit did not produce a selection")?; |
| 80 | .to_hex_with_len(7) | 80 | println!("{}", crate::change_id::display(&repository, selected, 7)?); |
| 81 | ); | 81 | super::print_ref_rewrites(&repository, &outcome.ref_rewrites)?; |
| 82 | super::print_ref_rewrites(&outcome.ref_rewrites); | ||
| 83 | Ok(()) | 82 | Ok(()) |
| 84 | } | 83 | } |
| 85 | 84 | ||
| @@ -210,7 +210,7 @@ fn prepare(repo: &gix::Repository, args: &Todo) -> Result<todo::Prepared> { · 209 unmodified lines | |||
| 210 | }) | 210 | }) |
| 211 | }) | 211 | }) |
| 212 | .collect::<Result<Vec<_>>>()?; | 212 | .collect::<Result<Vec<_>>>()?; |
| 213 | todo::prepare(repo, base, onto, &commits, &refs.view_tips, onto_kind) | 213 | todo::prepare(repo, base, onto, &commits, &refs.view_tips, onto_kind, true) |
| 214 | } | 214 | } |
| 215 | 215 | ||
| 216 | fn resolve_commit(repo: &gix::Repository, revision: &OsStr, description: &str) -> Result<ObjectId> { | 216 | fn resolve_commit(repo: &gix::Repository, revision: &OsStr, description: &str) -> Result<ObjectId> { |
| @@ -259,13 +259,14 @@ fn apply_document(repo: gix::Repository, document: &[u8], materialize_conflicts: · 42 unmodified lines | |||
| 259 | match rebase::perform_plan(&repo, &graph, parsed.plan)? { | 259 | match rebase::perform_plan(&repo, &graph, parsed.plan)? { |
| 260 | rebase::PlanPerform::Complete(outcome) => { | 260 | rebase::PlanPerform::Complete(outcome) => { |
| 261 | let revisions = mapped_revisions(&tips, |id| outcome.map(id)); | 261 | let revisions = mapped_revisions(&tips, |id| outcome.map(id)); |
| 262 | if outcome.selected.is_some() { | 262 | if let Some(selected) = outcome.selected { |
| 263 | let notice = edit::time_travel::checkout_plan(&repository_path, bare, &outcome, &revisions, false)?; | 263 | let notice = edit::time_travel::checkout_plan(&repository_path, bare, &outcome, &revisions, false)?; |
| 264 | println!("{}", notice.unwrap_or_else(|| "rebased history".into())); | 264 | let notice = notice.unwrap_or_else(|| "rebased history".into()); |
| 265 | println!("{}", super::notice_with_change_id(&repo, ¬ice, selected)?); | ||
| 265 | } else { | 266 | } else { |
| 266 | println!("rebased history"); | 267 | println!("rebased history"); |
| 267 | } | 268 | } |
| 268 | super::print_ref_rewrites(&outcome.ref_rewrites); | 269 | super::print_ref_rewrites(&repo, &outcome.ref_rewrites)?; |
| 269 | Ok(()) | 270 | Ok(()) |
| 270 | } | 271 | } |
| 271 | rebase::PlanPerform::Conflict(mut conflict) => { | 272 | rebase::PlanPerform::Conflict(mut conflict) => { |
| @@ -283,7 +284,7 @@ fn apply_document(repo: gix::Repository, document: &[u8], materialize_conflicts: · 11 unmodified lines | |||
| 283 | conflict.persist_objects()?; | 284 | conflict.persist_objects()?; |
| 284 | let plan = conflict.continuation_plan(); | 285 | let plan = conflict.continuation_plan(); |
| 285 | let mapped_tips = tips.iter().filter_map(|id| conflict.map(*id)).collect(); | 286 | let mapped_tips = tips.iter().filter_map(|id| conflict.map(*id)).collect(); |
| 286 | let continuation = todo::prepare_continuation(conflict.repository(), &plan, mapped_tips)?.document; | 287 | let continuation = todo::prepare_continuation(conflict.repository(), &plan, mapped_tips, true)?.document; |
| 287 | let revisions = mapped_revisions(&tips, |id| conflict.map(id)); | 288 | let revisions = mapped_revisions(&tips, |id| conflict.map(id)); |
| 288 | if destination == Path::new("-") { | 289 | if destination == Path::new("-") { |
| 289 | let mut stdout = std::io::stdout().lock(); | 290 | let mut stdout = std::io::stdout().lock(); |
| @@ -314,7 +315,7 @@ fn apply_document(repo: gix::Repository, document: &[u8], materialize_conflicts: · 24 unmodified lines | |||
| 314 | return Err(err); | 315 | return Err(err); |
| 315 | } | 316 | } |
| 316 | }; | 317 | }; |
| 317 | super::print_ref_rewrites(&ref_rewrites); | 318 | super::print_ref_rewrites(&repo, &ref_rewrites)?; |
| 318 | eprintln!("{notice}; continue with `tix rebase apply {}`", destination.display()); | 319 | eprintln!("{notice}; continue with `tix rebase apply {}`", destination.display()); |
| 319 | anyhow::bail!("rebase stopped at a materialized conflict") | 320 | anyhow::bail!("rebase stopped at a materialized conflict") |
| 320 | } | 321 | } |
| @@ -483,7 +484,10 @@ mod tests { · 162 unmodified lines | |||
| 483 | "moving to the updated base makes an unchanged todo actionable" | 484 | "moving to the updated base makes an unchanged todo actionable" |
| 484 | ); | 485 | ); |
| 485 | assert!( | 486 | assert!( |
| 486 | document.contains(&format!("{} (updated-base)", updated.to_hex_with_len(7))), | 487 | document.contains(&format!( |
| 488 | "{} (updated-base)", | ||
| 489 | crate::change_id::display_short(&repo, updated)? | ||
| 490 | )), | ||
| 487 | "the TUI-selected hidden tip is labelled as the updated base: {document}" | 491 | "the TUI-selected hidden tip is labelled as the updated base: {document}" |
| 488 | ); | 492 | ); |
| 489 | 493 | ||
| @@ -556,6 +560,7 @@ mod tests { · 66 unmodified lines | |||
| 556 | ], | 560 | ], |
| 557 | &[after], | 561 | &[after], |
| 558 | todo::OntoKind::Onto, | 562 | todo::OntoKind::Onto, |
| 563 | true, | ||
| 559 | )?; | 564 | )?; |
| 560 | let generated = std::str::from_utf8(&prepared.document)?; | 565 | let generated = std::str::from_utf8(&prepared.document)?; |
| 561 | let state = &generated[generated | 566 | let state = &generated[generated |
| @@ -59,13 +59,17 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 58 unmodified lines | |||
| 59 | .context("author is not valid UTF-8")?; | 59 | .context("author is not valid UTF-8")?; |
| 60 | 60 | ||
| 61 | if let Some(message) = explicit_message(&args.edit, std::io::stdin())? { | 61 | if let Some(message) = explicit_message(&args.edit, std::io::stdin())? { |
| 62 | return finish(crate::edit::reword::apply_message_reporting( | 62 | let output_repository = repository.clone(); |
| 63 | repository, | 63 | return finish( |
| 64 | &graph, | 64 | &output_repository, |
| 65 | target, | 65 | crate::edit::reword::apply_message_reporting( |
| 66 | &message, | 66 | repository, |
| 67 | author.map(AsRef::as_ref), | 67 | &graph, |
| 68 | )?); | 68 | target, |
| 69 | &message, | ||
| 70 | author.map(AsRef::as_ref), | ||
| 71 | )?, | ||
| 72 | ); | ||
| 69 | } | 73 | } |
| 70 | 74 | ||
| 71 | let repository_path = repository.git_dir().to_owned(); | 75 | let repository_path = repository.git_dir().to_owned(); |
| @@ -89,7 +93,12 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 17 unmodified lines | |||
| 89 | let mut repository = crate::open_repository(&repository_path, bare, false) | 93 | let mut repository = crate::open_repository(&repository_path, bare, false) |
| 90 | .context("could not reopen repository after editing commit")?; | 94 | .context("could not reopen repository after editing commit")?; |
| 91 | repository.object_cache_size(None); | 95 | repository.object_cache_size(None); |
| 92 | finish_editor(crate::edit::reword::apply(repository, &graph, target, &edited)?, target) | 96 | let output_repository = repository.clone(); |
| 97 | finish_editor( | ||
| 98 | &output_repository, | ||
| 99 | crate::edit::reword::apply(repository, &graph, target, &edited)?, | ||
| 100 | target, | ||
| 101 | ) | ||
| 93 | } | 102 | } |
| 94 | 103 | ||
| 95 | pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Result<Option<Vec<u8>>> { | 104 | pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Result<Option<Vec<u8>>> { |
| @@ -122,22 +131,26 @@ pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Resu · 26 unmodified lines | |||
| 122 | } | 131 | } |
| 123 | } | 132 | } |
| 124 | 133 | ||
| 125 | fn finish(outcome: crate::edit::reword::Outcome) -> Result<()> { | 134 | fn finish(repository: &gix::Repository, outcome: crate::edit::reword::Outcome) -> Result<()> { |
| 126 | match outcome.commit { | 135 | match outcome.commit { |
| 127 | Some(id) => println!("{}", id.to_hex_with_len(7)), | 136 | Some(id) => println!("{}", crate::change_id::display(repository, id, 7)?), |
| 128 | None => println!("no reword performed: the edited commit was unchanged"), | 137 | None => println!("no reword performed: the edited commit was unchanged"), |
| 129 | } | 138 | } |
| 130 | super::print_ref_rewrites(&outcome.ref_rewrites); | 139 | super::print_ref_rewrites(repository, &outcome.ref_rewrites)?; |
| 131 | Ok(()) | 140 | Ok(()) |
| 132 | } | 141 | } |
| 133 | 142 | ||
| 134 | fn finish_editor(outcome: crate::edit::reword::Outcome, original: gix::ObjectId) -> Result<()> { | 143 | fn finish_editor( |
| 144 | repository: &gix::Repository, | ||
| 145 | outcome: crate::edit::reword::Outcome, | ||
| 146 | original: gix::ObjectId, | ||
| 147 | ) -> Result<()> { | ||
| 135 | match (outcome.commit, outcome.enrichment) { | 148 | match (outcome.commit, outcome.enrichment) { |
| 136 | (Some(id), _) => println!("{}", id.to_hex_with_len(7)), | 149 | (Some(id), _) => println!("{}", crate::change_id::display(repository, id, 7)?), |
| 137 | (None, Some(_)) => println!("{}", original.to_hex_with_len(7)), | 150 | (None, Some(_)) => println!("{}", crate::change_id::display(repository, original, 7)?), |
| 138 | (None, None) => println!("no reword performed: the edited commit was unchanged"), | 151 | (None, None) => println!("no reword performed: the edited commit was unchanged"), |
| 139 | } | 152 | } |
| 140 | super::print_ref_rewrites(&outcome.ref_rewrites); | 153 | super::print_ref_rewrites(repository, &outcome.ref_rewrites)?; |
| 141 | Ok(()) | 154 | Ok(()) |
| 142 | } | 155 | } |
| 143 | 156 | ||
| @@ -22,7 +22,7 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 21 unmodified lines | |||
| 22 | let detached = head.is_detached(); | 22 | let detached = head.is_detached(); |
| 23 | drop(head); | 23 | drop(head); |
| 24 | if selected == head_id { | 24 | if selected == head_id { |
| 25 | println!("already at {}", selected.to_hex_with_len(7)); | 25 | println!("already at {}", crate::change_id::display(&repository, selected, 7)?); |
| 26 | return Ok(()); | 26 | return Ok(()); |
| 27 | } | 27 | } |
| 28 | 28 | ||
| @@ -51,16 +51,28 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 22 unmodified lines | |||
| 51 | let bare = repository.is_bare(); | 51 | let bare = repository.is_bare(); |
| 52 | drop(repository); | 52 | drop(repository); |
| 53 | match crate::edit::time_travel::perform(&repository_path, bare, selected, &graph, &reviews, &[], false)? { | 53 | match crate::edit::time_travel::perform(&repository_path, bare, selected, &graph, &reviews, &[], false)? { |
| 54 | crate::edit::time_travel::Perform::Complete { notice, ref_rewrites } => { | 54 | crate::edit::time_travel::Perform::Complete { |
| 55 | notice, | ||
| 56 | selected, | ||
| 57 | ref_rewrites, | ||
| 58 | } => { | ||
| 59 | let repository = crate::open_repository(&repository_path, bare, false) | ||
| 60 | .context("could not reopen repository after time-travel")?; | ||
| 55 | println!( | 61 | println!( |
| 56 | "{}", | 62 | "{}", |
| 57 | notice.unwrap_or_else(|| format!("already at {}", selected.to_hex_with_len(7))) | 63 | super::notice_with_change_id( |
| 64 | &repository, | ||
| 65 | ¬ice.unwrap_or_else(|| format!("already at {}", selected.to_hex_with_len(7))), | ||
| 66 | selected, | ||
| 67 | )? | ||
| 58 | ); | 68 | ); |
| 59 | super::print_ref_rewrites(&ref_rewrites); | 69 | super::print_ref_rewrites(&repository, &ref_rewrites)?; |
| 60 | } | 70 | } |
| 61 | crate::edit::time_travel::Perform::Conflict(conflict) if args.materialize_conflicts => { | 71 | crate::edit::time_travel::Perform::Conflict(conflict) if args.materialize_conflicts => { |
| 62 | let (notice, _, ref_rewrites) = conflict.accept()?; | 72 | let (notice, _, ref_rewrites) = conflict.accept()?; |
| 63 | super::print_ref_rewrites(&ref_rewrites); | 73 | let repository = crate::open_repository(&repository_path, bare, false) |
| 74 | .context("could not reopen repository after materializing time-travel")?; | ||
| 75 | super::print_ref_rewrites(&repository, &ref_rewrites)?; | ||
| 64 | anyhow::bail!("{notice}"); | 76 | anyhow::bail!("{notice}"); |
| 65 | } | 77 | } |
| 66 | crate::edit::time_travel::Perform::Conflict(_) => { | 78 | crate::edit::time_travel::Perform::Conflict(_) => { |
| @@ -24,6 +24,7 @@ use super::stash::SavedStash; · 23 unmodified lines | |||
| 24 | pub(crate) enum Perform { | 24 | pub(crate) enum Perform { |
| 25 | Complete { | 25 | Complete { |
| 26 | notice: Option<String>, | 26 | notice: Option<String>, |
| 27 | selected: ObjectId, | ||
| 27 | ref_rewrites: Vec<super::rebase::RefRewrite>, | 28 | ref_rewrites: Vec<super::rebase::RefRewrite>, |
| 28 | }, | 29 | }, |
| 29 | Conflict(Conflict), | 30 | Conflict(Conflict), |
| @@ -477,7 +478,11 @@ pub(crate) fn perform_reporting_rebased( · 447 unmodified lines | |||
| 477 | super::stash::apply(repository_path, bare, &workdir, stash)?, | 478 | super::stash::apply(repository_path, bare, &workdir, stash)?, |
| 478 | ); | 479 | ); |
| 479 | } | 480 | } |
| 480 | Ok(Perform::Complete { notice, ref_rewrites }) | 481 | Ok(Perform::Complete { |
| 482 | notice, | ||
| 483 | selected, | ||
| 484 | ref_rewrites, | ||
| 485 | }) | ||
| 481 | } | 486 | } |
| 482 | 487 | ||
| 483 | #[derive(Clone, Debug, Eq, PartialEq)] | 488 | #[derive(Clone, Debug, Eq, PartialEq)] |
| @@ -84,6 +84,7 @@ pub(crate) fn prepare( · 83 unmodified lines | |||
| 84 | commits: &[Commit], | 84 | commits: &[Commit], |
| 85 | resolved_tips: &[ObjectId], | 85 | resolved_tips: &[ObjectId], |
| 86 | onto_kind: OntoKind, | 86 | onto_kind: OntoKind, |
| 87 | show_change_ids: bool, | ||
| 87 | ) -> Result<Prepared> { | 88 | ) -> Result<Prepared> { |
| 88 | repo.find_commit(base) | 89 | repo.find_commit(base) |
| 89 | .context("could not find the selected rebase base")?; | 90 | .context("could not find the selected rebase base")?; |
| @@ -160,11 +161,14 @@ pub(crate) fn prepare( · 70 unmodified lines | |||
| 160 | ref_points.dedup(); | 161 | ref_points.dedup(); |
| 161 | let expected_refs = rebase::capture_refs(repo, &ref_points, &tips)?; | 162 | let expected_refs = rebase::capture_refs(repo, &ref_points, &tips)?; |
| 162 | 163 | ||
| 163 | let source = short(repo, base)?; | 164 | let source = short(repo, base, show_change_ids)?; |
| 164 | let title = if base == onto { | 165 | let title = if base == onto { |
| 165 | format!("# Rebase from `{source}`") | 166 | format!("# Rebase from `{source}`") |
| 166 | } else { | 167 | } else { |
| 167 | format!("# Rebase from `{source}` onto `{}`", short(repo, onto)?) | 168 | format!( |
| 169 | "# Rebase from `{source}` onto `{}`", | ||
| 170 | short(repo, onto, show_change_ids)? | ||
| 171 | ) | ||
| 168 | }; | 172 | }; |
| 169 | let state = State { | 173 | let state = State { |
| 170 | base, | 174 | base, |
| @@ -206,6 +210,7 @@ pub(crate) fn prepare( · 35 unmodified lines | |||
| 206 | repo, | 210 | repo, |
| 207 | section.parent, | 211 | section.parent, |
| 208 | (section.parent == onto).then_some((anchor_kind, anchor_title.as_str())), | 212 | (section.parent == onto).then_some((anchor_kind, anchor_title.as_str())), |
| 213 | show_change_ids, | ||
| 209 | )?; | 214 | )?; |
| 210 | if !scope_set.contains(§ion.parent) { | 215 | if !scope_set.contains(§ion.parent) { |
| 211 | write_refs_at(&mut body, &state.expected_refs, section.parent)?; | 216 | write_refs_at(&mut body, &state.expected_refs, section.parent)?; |
| @@ -218,12 +223,25 @@ pub(crate) fn prepare( · 6 unmodified lines | |||
| 218 | "pick" | 223 | "pick" |
| 219 | }; | 224 | }; |
| 220 | let states = commit_states(repo, *id)?; | 225 | let states = commit_states(repo, *id)?; |
| 221 | body.extend_from_slice(format!("`{verb} {}` {states}{}\n", short(repo, *id)?, commit.info).as_bytes()); | 226 | body.extend_from_slice( |
| 227 | format!( | ||
| 228 | "`{verb} {}` {states}{}\n", | ||
| 229 | short(repo, *id, show_change_ids)?, | ||
| 230 | commit.info | ||
| 231 | ) | ||
| 232 | .as_bytes(), | ||
| 233 | ); | ||
| 222 | write_refs_at(&mut body, &state.expected_refs, *id)?; | 234 | write_refs_at(&mut body, &state.expected_refs, *id)?; |
| 223 | } | 235 | } |
| 224 | } | 236 | } |
| 225 | if sections.is_empty() { | 237 | if sections.is_empty() { |
| 226 | write_fork_heading(&mut body, repo, onto, Some((anchor_kind, anchor_title.as_str())))?; | 238 | write_fork_heading( |
| 239 | &mut body, | ||
| 240 | repo, | ||
| 241 | onto, | ||
| 242 | Some((anchor_kind, anchor_title.as_str())), | ||
| 243 | show_change_ids, | ||
| 244 | )?; | ||
| 227 | write_refs_at(&mut body, &state.expected_refs, onto)?; | 245 | write_refs_at(&mut body, &state.expected_refs, onto)?; |
| 228 | } | 246 | } |
| 229 | write_bottom_up(&mut document, &body)?; | 247 | write_bottom_up(&mut document, &body)?; |
| @@ -239,6 +257,7 @@ pub(crate) fn prepare_continuation( · 9 unmodified lines | |||
| 239 | repo: &gix::Repository, | 257 | repo: &gix::Repository, |
| 240 | plan: &rebase::Plan, | 258 | plan: &rebase::Plan, |
| 241 | tips: Vec<ObjectId>, | 259 | tips: Vec<ObjectId>, |
| 260 | show_change_ids: bool, | ||
| 242 | ) -> Result<Prepared> { | 261 | ) -> Result<Prepared> { |
| 243 | let resolved = plan.steps.iter().find_map(|step| match step.commit { | 262 | let resolved = plan.steps.iter().find_map(|step| match step.commit { |
| 244 | rebase::PlanCommit::Resolved(id) => Some(id), | 263 | rebase::PlanCommit::Resolved(id) => Some(id), |
| @@ -283,6 +302,7 @@ pub(crate) fn prepare_continuation( · 38 unmodified lines | |||
| 283 | repo, | 302 | repo, |
| 284 | parent, | 303 | parent, |
| 285 | (parent == plan.base).then_some(("base", base_title.as_str())), | 304 | (parent == plan.base).then_some(("base", base_title.as_str())), |
| 305 | show_change_ids, | ||
| 286 | )?; | 306 | )?; |
| 287 | if matches!(step.parent, rebase::PlanParent::Existing(_)) { | 307 | if matches!(step.parent, rebase::PlanParent::Existing(_)) { |
| 288 | write_plan_refs_at(&mut body, &plan.expected_refs, step.parent)?; | 308 | write_plan_refs_at(&mut body, &plan.expected_refs, step.parent)?; |
| @@ -300,9 +320,17 @@ pub(crate) fn prepare_continuation( · 11 unmodified lines | |||
| 300 | match step.commit { | 320 | match step.commit { |
| 301 | rebase::PlanCommit::Pick(id) | rebase::PlanCommit::Resolved(id) => { | 321 | rebase::PlanCommit::Pick(id) | rebase::PlanCommit::Resolved(id) => { |
| 302 | let value = if matches!(step.commit, rebase::PlanCommit::Resolved(_)) { | 322 | let value = if matches!(step.commit, rebase::PlanCommit::Resolved(_)) { |
| 303 | ObjectId::null(id.kind()).to_string() | 323 | let hash = ObjectId::null(id.kind()).to_string(); |
| 324 | if show_change_ids { | ||
| 325 | format!( | ||
| 326 | "{hash} {}", | ||
| 327 | crate::change_id::for_commit(repo, id)?.to_reverse_hex_with_len(hash.len()) | ||
| 328 | ) | ||
| 329 | } else { | ||
| 330 | hash | ||
| 331 | } | ||
| 304 | } else { | 332 | } else { |
| 305 | short(repo, id)? | 333 | short(repo, id, show_change_ids)? |
| 306 | }; | 334 | }; |
| 307 | let title = anchor_title(repo, id)?; | 335 | let title = anchor_title(repo, id)?; |
| 308 | body.extend_from_slice( | 336 | body.extend_from_slice( |
| @@ -318,7 +346,7 @@ pub(crate) fn prepare_continuation( · 9 unmodified lines | |||
| 318 | body.extend_from_slice( | 346 | body.extend_from_slice( |
| 319 | format!( | 347 | format!( |
| 320 | "`squash {}` {}{}\n", | 348 | "`squash {}` {}{}\n", |
| 321 | short(repo, *id)?, | 349 | short(repo, *id, show_change_ids)?, |
| 322 | commit_states(repo, *id)?, | 350 | commit_states(repo, *id)?, |
| 323 | title | 351 | title |
| 324 | ) | 352 | ) |
| @@ -521,8 +549,9 @@ fn write_fork_heading( · 196 unmodified lines | |||
| 521 | repo: &gix::Repository, | 549 | repo: &gix::Repository, |
| 522 | id: ObjectId, | 550 | id: ObjectId, |
| 523 | annotation: Option<(&str, &str)>, | 551 | annotation: Option<(&str, &str)>, |
| 552 | show_change_ids: bool, | ||
| 524 | ) -> Result<()> { | 553 | ) -> Result<()> { |
| 525 | out.extend_from_slice(format!("fork {}", short(repo, id)?).as_bytes()); | 554 | out.extend_from_slice(format!("fork {}", short(repo, id, show_change_ids)?).as_bytes()); |
| 526 | if let Some((kind, title)) = annotation { | 555 | if let Some((kind, title)) = annotation { |
| 527 | out.extend_from_slice(format!(" ({kind}) {title}").as_bytes()); | 556 | out.extend_from_slice(format!(" ({kind}) {title}").as_bytes()); |
| 528 | } | 557 | } |
| @@ -573,12 +602,16 @@ fn walk(id: ObjectId, children: &HashMap<ObjectId, Vec<ObjectId>>, section: &mut · 44 unmodified lines | |||
| 573 | } | 602 | } |
| 574 | } | 603 | } |
| 575 | 604 | ||
| 576 | fn short(repo: &gix::Repository, id: ObjectId) -> Result<String> { | 605 | fn short(repo: &gix::Repository, id: ObjectId, show_change_id: bool) -> Result<String> { |
| 577 | Ok(id | 606 | if show_change_id { |
| 578 | .attach(repo) | 607 | crate::change_id::display_short(repo, id).context("could not format a rebase todo ID") |
| 579 | .shorten() | 608 | } else { |
| 580 | .context("could not shorten a rebase todo ID")? | 609 | Ok(id |
| 581 | .to_string()) | 610 | .attach(repo) |
| 611 | .shorten() | ||
| 612 | .context("could not shorten a rebase todo ID")? | ||
| 613 | .to_string()) | ||
| 614 | } | ||
| 582 | } | 615 | } |
| 583 | 616 | ||
| 584 | fn parse_state(repo: &gix::Repository, input: &str) -> Result<Option<State>> { | 617 | fn parse_state(repo: &gix::Repository, input: &str) -> Result<Option<State>> { |
| @@ -1178,7 +1211,7 @@ mod tests { · 593 unmodified lines | |||
| 1178 | commits: &[Commit], | 1211 | commits: &[Commit], |
| 1179 | _head: Option<ObjectId>, | 1212 | _head: Option<ObjectId>, |
| 1180 | ) -> Result<Prepared> { | 1213 | ) -> Result<Prepared> { |
| 1181 | prepare(repo, base, onto, commits, &[], OntoKind::UpdatedBase) | 1214 | prepare(repo, base, onto, commits, &[], OntoKind::UpdatedBase, true) |
| 1182 | } | 1215 | } |
| 1183 | 1216 | ||
| 1184 | fn parse_plan(repo: &gix::Repository, document: &[u8]) -> Result<rebase::Plan> { | 1217 | fn parse_plan(repo: &gix::Repository, document: &[u8]) -> Result<rebase::Plan> { |
| @@ -1213,8 +1246,14 @@ mod tests { · 28 unmodified lines | |||
| 1213 | "the first line explains that saving unchanged is a no-op" | 1246 | "the first line explains that saving unchanged is a no-op" |
| 1214 | ); | 1247 | ); |
| 1215 | assert!(document.contains(STATE_START), "the todo carries its transaction state"); | 1248 | assert!(document.contains(STATE_START), "the todo carries its transaction state"); |
| 1216 | assert!(document.contains(&format!("# Rebase from `{}`", base.to_hex_with_len(7)))); | 1249 | assert!(document.contains(&format!( |
| 1217 | assert!(document.contains(&format!("fork {} (base) base", base.to_hex_with_len(7)))); | 1250 | "# Rebase from `{}`", |
| 1251 | crate::change_id::display_short(&repo, base)? | ||
| 1252 | ))); | ||
| 1253 | assert!(document.contains(&format!( | ||
| 1254 | "fork {} (base) base", | ||
| 1255 | crate::change_id::display_short(&repo, base)? | ||
| 1256 | ))); | ||
| 1218 | let middle = document.find("`pick ").expect("the oldest pick is shown"); | 1257 | let middle = document.find("`pick ").expect("the oldest pick is shown"); |
| 1219 | let tip = document.find("`@pick ").expect("HEAD is marked"); | 1258 | let tip = document.find("`@pick ").expect("HEAD is marked"); |
| 1220 | let base = document.find("fork ").expect("the base separator is shown"); | 1259 | let base = document.find("fork ").expect("the base separator is shown"); |
| @@ -1450,9 +1489,12 @@ mod tests { · 229 unmodified lines | |||
| 1450 | 1489 | ||
| 1451 | let prepared = prepare_test(&repo, base, base, &commits, Some(tip))?; | 1490 | let prepared = prepare_test(&repo, base, base, &commits, Some(tip))?; |
| 1452 | let document = String::from_utf8(prepared.document.clone())?; | 1491 | let document = String::from_utf8(prepared.document.clone())?; |
| 1453 | assert!(document.contains(&format!("fork {} (base) base", base.to_hex_with_len(7)))); | 1492 | assert!(document.contains(&format!( |
| 1493 | "fork {} (base) base", | ||
| 1494 | crate::change_id::display_short(&repo, base)? | ||
| 1495 | ))); | ||
| 1454 | assert!( | 1496 | assert!( |
| 1455 | document.contains(&format!("fork {} ", middle.to_hex_with_len(7))), | 1497 | document.contains(&format!("fork {} ", crate::change_id::display_short(&repo, middle)?)), |
| 1456 | "a fork within the editable tree has no external-anchor annotation" | 1498 | "a fork within the editable tree has no external-anchor annotation" |
| 1457 | ); | 1499 | ); |
| 1458 | let plan = parse_plan(&repo, document.as_bytes())?; | 1500 | let plan = parse_plan(&repo, document.as_bytes())?; |
| @@ -1483,15 +1525,15 @@ mod tests { · 24 unmodified lines | |||
| 1483 | assert!( | 1525 | assert!( |
| 1484 | document.contains(&format!( | 1526 | document.contains(&format!( |
| 1485 | "# Rebase from `{}` onto `{}`", | 1527 | "# Rebase from `{}` onto `{}`", |
| 1486 | base.to_hex_with_len(7), | 1528 | crate::change_id::display_short(&repo, base)?, |
| 1487 | onto.to_hex_with_len(7) | 1529 | crate::change_id::display_short(&repo, onto)? |
| 1488 | )), | 1530 | )), |
| 1489 | "the update target is explicit in the document title" | 1531 | "the update target is explicit in the document title" |
| 1490 | ); | 1532 | ); |
| 1491 | assert!( | 1533 | assert!( |
| 1492 | document.contains(&format!( | 1534 | document.contains(&format!( |
| 1493 | "fork {} (updated-base) [A] [N] updated * _ [hidden] <base> `raw` \\ base", | 1535 | "fork {} (updated-base) [A] [N] updated * _ [hidden] <base> `raw` \\ base", |
| 1494 | onto.to_hex_with_len(7) | 1536 | crate::change_id::display_short(&repo, onto)? |
| 1495 | )), | 1537 | )), |
| 1496 | "the unfamiliar fork target carries its raw UI title" | 1538 | "the unfamiliar fork target carries its raw UI title" |
| 1497 | ); | 1539 | ); |
| @@ -1627,6 +1669,7 @@ mod tests { · 129 unmodified lines | |||
| 1627 | }], | 1669 | }], |
| 1628 | }, | 1670 | }, |
| 1629 | vec![middle], | 1671 | vec![middle], |
| 1672 | true, | ||
| 1630 | )?; | 1673 | )?; |
| 1631 | assert!( | 1674 | assert!( |
| 1632 | prepared | 1675 | prepared |
| @@ -1642,7 +1685,7 @@ mod tests { · 9 unmodified lines | |||
| 1642 | ); | 1685 | ); |
| 1643 | assert!(document.contains(&"0".repeat(40)), "the conflict uses the full null ID"); | 1686 | assert!(document.contains(&"0".repeat(40)), "the conflict uses the full null ID"); |
| 1644 | assert!( | 1687 | assert!( |
| 1645 | document.contains(&format!("`squash {}`", tip.to_hex_with_len(7))), | 1688 | document.contains(&format!("`squash {}`", crate::change_id::display_short(&repo, tip)?)), |
| 1646 | "unapplied squash sources remain editable" | 1689 | "unapplied squash sources remain editable" |
| 1647 | ); | 1690 | ); |
| 1648 | let plan = parse_plan(&repo, &prepared.document)?; | 1691 | let plan = parse_plan(&repo, &prepared.document)?; |
| @@ -3881,6 +3881,7 @@ fn rebase_history( · 3880 unmodified lines | |||
| 3881 | &commits, | 3881 | &commits, |
| 3882 | &[], | 3882 | &[], |
| 3883 | edit::todo::OntoKind::UpdatedBase, | 3883 | edit::todo::OntoKind::UpdatedBase, |
| 3884 | false, | ||
| 3884 | )?; | 3885 | )?; |
| 3885 | (prepared, editor) | 3886 | (prepared, editor) |
| 3886 | }; | 3887 | }; |
| @@ -1512,13 +1512,17 @@ pub(crate) fn render_full( · 1511 unmodified lines | |||
| 1512 | )?; | 1512 | )?; |
| 1513 | let graph = graph.ok_or_else(|| anyhow::anyhow!("history traversal did not produce a graph"))?; | 1513 | let graph = graph.ok_or_else(|| anyhow::anyhow!("history traversal did not produce a graph"))?; |
| 1514 | let decorations = decorations.unwrap_or_default(); | 1514 | let decorations = decorations.unwrap_or_default(); |
| 1515 | Ok(render_overview( | 1515 | let overview = Overview::new(&graph, &refs, &decorations, show_tags); |
| 1516 | &Overview::new(&graph, &refs, &decorations, show_tags), | 1516 | let labels = overview |
| 1517 | unicode, | 1517 | .nodes |
| 1518 | )) | 1518 | .iter() |
| 1519 | .filter(|node| node.raw_tip && node.decorations.is_empty()) | ||
| 1520 | .map(|node| Ok((node.id, crate::change_id::display(repository, node.id, 7)?))) | ||
| 1521 | .collect::<anyhow::Result<HashMap<_, _>>>()?; | ||
| 1522 | Ok(render_overview(&overview, unicode, &labels)) | ||
| 1519 | } | 1523 | } |
| 1520 | 1524 | ||
| 1521 | fn render_overview(overview: &Overview, unicode: bool) -> String { | 1525 | fn render_overview(overview: &Overview, unicode: bool, commit_labels: &HashMap<ObjectId, String>) -> String { |
| 1522 | if overview.nodes.is_empty() { | 1526 | if overview.nodes.is_empty() { |
| 1523 | return String::new(); | 1527 | return String::new(); |
| 1524 | } | 1528 | } |
| @@ -1548,7 +1552,11 @@ fn render_overview(overview: &Overview, unicode: bool) -> String { · 23 unmodified lines | |||
| 1548 | out.push_str(&lane); | 1552 | out.push_str(&lane); |
| 1549 | out.extend(std::iter::repeat_n(' ', placed.rail_width.saturating_sub(width))); | 1553 | out.extend(std::iter::repeat_n(' ', placed.rail_width.saturating_sub(width))); |
| 1550 | if let Some(node) = node { | 1554 | if let Some(node) = node { |
| 1551 | let label = rail_label(&overview.nodes[node], None); | 1555 | let node = &overview.nodes[node]; |
| 1556 | let label = commit_labels | ||
| 1557 | .get(&node.id) | ||
| 1558 | .cloned() | ||
| 1559 | .unwrap_or_else(|| rail_label(node, None)); | ||
| 1552 | if unicode { | 1560 | if unicode { |
| 1553 | out.push_str(&label); | 1561 | out.push_str(&label); |
| 1554 | } else { | 1562 | } else { |
| @@ -2043,8 +2051,8 @@ mod tests { · 488 unmodified lines | |||
| 2043 | !overview.nodes.iter().any(|node| node.id == id(5)), | 2051 | !overview.nodes.iter().any(|node| node.id == id(5)), |
| 2044 | "ordinary pin-only tips are absent" | 2052 | "ordinary pin-only tips are absent" |
| 2045 | ); | 2053 | ); |
| 2046 | let unicode = render_overview(overview, true); | 2054 | let unicode = render_overview(overview, true, &HashMap::new()); |
| 2047 | let ascii = render_overview(overview, false); | 2055 | let ascii = render_overview(overview, false, &HashMap::new()); |
| 2048 | assert_eq!(unicode.matches('📌').count(), 1, "detached HEAD has one marker"); | 2056 | assert_eq!(unicode.matches('📌').count(), 1, "detached HEAD has one marker"); |
| 2049 | assert!( | 2057 | assert!( |
| 2050 | unicode.contains("★main"), | 2058 | unicode.contains("★main"), |
| @@ -2414,7 +2422,7 @@ mod tests { · 363 unmodified lines | |||
| 2414 | fn full_rendering_is_unstyled_ascii() { | 2422 | fn full_rendering_is_unstyled_ascii() { |
| 2415 | let (graph, refs, decorations) = fixture(); | 2423 | let (graph, refs, decorations) = fixture(); |
| 2416 | let overview = Overview::new(&graph, &refs, &decorations, true); | 2424 | let overview = Overview::new(&graph, &refs, &decorations, true); |
| 2417 | let rendered = render_overview(&overview, false); | 2425 | let rendered = render_overview(&overview, false, &HashMap::new()); |
| 2418 | 2426 | ||
| 2419 | assert!(rendered.contains("main"), "HEAD is rendered without selection state"); | 2427 | assert!(rendered.contains("main"), "HEAD is rendered without selection state"); |
| 2420 | assert!(rendered.contains("topic"), "ordinary nodes retain their label"); | 2428 | assert!(rendered.contains("topic"), "ordinary nodes retain their label"); |
| @@ -2424,12 +2432,28 @@ mod tests { · 3 unmodified lines | |||
| 2424 | assert!(!rendered.contains('\u{1b}'), "plain output has no terminal styles"); | 2432 | assert!(!rendered.contains('\u{1b}'), "plain output has no terminal styles"); |
| 2425 | } | 2433 | } |
| 2426 | 2434 | ||
| 2435 | #[test] | ||
| 2436 | fn full_rendering_pairs_raw_commit_tips_with_change_ids() -> gix_testtools::Result { | ||
| 2437 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; | ||
| 2438 | let repo = crate::test_repository::open(fixture.path())?; | ||
| 2439 | let mut commit = repo.head_commit()?.decode()?.into_owned()?; | ||
| 2440 | commit.message = "unreferenced raw tip".into(); | ||
| 2441 | let id = repo.write_object(&commit)?.detach(); | ||
| 2442 | let rendered = render_full(&repo, &[id.to_string().into()], &[], true, false)?; | ||
| 2443 | |||
| 2444 | assert!( | ||
| 2445 | rendered.contains(&crate::change_id::display(&repo, id, 7)?), | ||
| 2446 | "a displayed raw hash is immediately followed by its change ID: {rendered:?}" | ||
| 2447 | ); | ||
| 2448 | Ok(()) | ||
| 2449 | } | ||
| 2450 | |||
| 2427 | #[test] | 2451 | #[test] |
| 2428 | fn rail_layout_uses_rounded_unicode_connections_and_ascii_fallbacks() { | 2452 | fn rail_layout_uses_rounded_unicode_connections_and_ascii_fallbacks() { |
| 2429 | let (graph, refs, decorations) = fixture(); | 2453 | let (graph, refs, decorations) = fixture(); |
| 2430 | let overview = Overview::new(&graph, &refs, &decorations, true); | 2454 | let overview = Overview::new(&graph, &refs, &decorations, true); |
| 2431 | let unicode = render_overview(&overview, true); | 2455 | let unicode = render_overview(&overview, true, &HashMap::new()); |
| 2432 | let ascii = render_overview(&overview, false); | 2456 | let ascii = render_overview(&overview, false, &HashMap::new()); |
| 2433 | 2457 | ||
| 2434 | assert!( | 2458 | assert!( |
| 2435 | unicode.contains("│ ● topic"), | 2459 | unicode.contains("│ ● topic"), |