History

feat: print change IDs with tix commit hashes

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.

Codex committed 2 months ago

Commit 482270f1 parent c7f9c6f11 files · +277 −124Browse files

Unified · Split

11 files
  1. gix-tix/spec.md +6 −0
  2. gix-tix/src/change_id.rs +21 −0
  3. gix-tix/src/command.rs +78 −54
  4. gix-tix/src/command/new.rs +7 −8
  5. gix-tix/src/command/rebase.rs +12 −7
  6. gix-tix/src/command/reword.rs +28 −15
  7. gix-tix/src/command/travel.rs +17 −5
  8. gix-tix/src/edit/time_travel.rs +6 −1
  9. gix-tix/src/edit/todo.rs +66 −23
  10. gix-tix/src/lib.rs +1 −0
  11. gix-tix/src/ref_tree.rs +35 −11
gix-tix/spec.md+6−0
@@ -67,6 +67,12 @@ without trading responsiveness for metadata that is not visible. · 66 unmodified lines
6767 line. Editing that title preserves an existing message body byte-for-byte,
6868 while commenting, deleting, or emptying the header removes the whole message.
6969 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.
7076 - `-x/--hide REVSPEC` excludes the revision and its reachable ancestry. The
7177 option may be repeated.
7278 - `-h/--help` prints Clap's standard help for `tix` and every subcommand.
gix-tix/src/change_id.rs+21−0
@@ -9,6 +9,7 @@ use gix::{ · 8 unmodified lines
99 ObjectId,
1010 bstr::{BStr, BString},
1111 hash::ChangeId,
12+ prelude::ObjectIdExt,
1213 };
1314
1415 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
2728 Ok(effective(id, commit.extra_headers().find_all(HEADER)))
2829 }
2930
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+
3051 pub(crate) fn abbreviations(
3152 repo: &gix::Repository,
3253 ids: impl IntoIterator<Item = ObjectId>,
gix-tix/src/command.rs+78−54
@@ -7,7 +7,7 @@ use std::{ · 6 unmodified lines
77
88 use anyhow::{Context, Result};
99 use clap::Parser;
10−use gix::prelude::{ObjectIdExt, ReferenceExt};
10+use gix::prelude::ReferenceExt;
1111 use ratatui::text::Line;
1212
1313 mod new;
@@ -148,6 +148,7 @@ impl Platform { · 134 unmodified lines
148148 Command::RefTree(_) | Command::Show(_) => unreachable!("display commands return before logging"),
149149 Command::Amend(args) => {
150150 let graph = crate::edit::loaded_view_graph(&repository)?;
151+ let output_repository = repository.clone();
151152 let amended = if args.index {
152153 crate::edit::head::amend_index_reporting(repository, &graph)?
153154 } else {
@@ -155,14 +156,9 @@ impl Platform { · 1 unmodified line
155156 };
156157 match amended {
157158 Some(outcome) => {
158− println!(
159− "{}",
160− outcome
161− .selected
162− .context("amending did not produce a selection")?
163− .to_hex_with_len(7)
164− );
165− print_ref_rewrites(&outcome.ref_rewrites);
159+ let selected = outcome.selected.context("amending did not produce a selection")?;
160+ println!("{}", crate::change_id::display(&output_repository, selected, 7)?);
161+ print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?;
166162 }
167163 None => println!("nothing to amend"),
168164 }
@@ -180,10 +176,8 @@ impl Platform { · 11 unmodified lines
180176 .head_id()
181177 .context("stashing changes requires a born HEAD")?
182178 .detach();
183− println!(
184− "{}",
185− crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)?
186− );
179+ let notice = crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)?;
180+ println!("{}", notice_with_change_id(&repository, &notice, id)?);
187181 }
188182 Command::Pin(args) => pin(&repository, args)?,
189183 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
458452 Ok(format!(
459453 "{} {}",
460454 crate::edit::time_travel::pin_label(pin),
461− pin.id
462− .attach(repository)
463− .shorten()
464− .context("could not shorten pinned commit ID")?
455+ crate::change_id::display_short(repository, pin.id)?
465456 ))
466457 }
467458
@@ -471,16 +462,12 @@ fn edit_head( · 3 unmodified lines
471462 kind: crate::edit::head::Kind,
472463 verb: &str,
473464 ) -> Result<()> {
465+ let output_repository = repository.clone();
474466 match crate::edit::head::perform_reporting(repository, graph, kind)? {
475467 Some(outcome) => {
476− println!(
477− "{}",
478− outcome
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);
468+ let selected = outcome.selected.context("editing HEAD did not produce a selection")?;
469+ println!("{}", crate::change_id::display(&output_repository, selected, 7)?);
470+ print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?;
484471 }
485472 None => println!("nothing to {verb}"),
486473 }
@@ -504,40 +491,46 @@ fn split(repository: gix::Repository, graph: &crate::history::HistoryGraph, args · 17 unmodified lines
504491 .context("could not reopen repository after editing split")?;
505492 repository.object_cache_size(None);
506493 let outcome = crate::edit::split::apply_reporting(repository, graph, prepared, &edited)?;
507− println!(
508− "{}",
509− outcome
510− .selected
511− .context("splitting did not produce a selection")?
512− .to_hex_with_len(7)
513− );
514− print_ref_rewrites(&outcome.ref_rewrites);
494+ let output_repository =
495+ crate::open_repository(&repository_path, bare, false).context("could not reopen repository after splitting")?;
496+ let selected = outcome.selected.context("splitting did not produce a selection")?;
497+ println!("{}", crate::change_id::display(&output_repository, selected, 7)?);
498+ print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?;
515499 Ok(())
516500 }
517501
518−fn print_ref_rewrites(rewrites: &[crate::edit::rebase::RefRewrite]) {
519− for line in ref_rewrite_lines(rewrites) {
502+fn print_ref_rewrites(repository: &gix::Repository, rewrites: &[crate::edit::rebase::RefRewrite]) -> Result<()> {
503+ for line in ref_rewrite_lines(repository, rewrites)? {
520504 println!("{line}");
521505 }
506+ Ok(())
522507 }
523508
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>> {
525513 let mut rewrites = rewrites.to_vec();
526514 rewrites.sort_by(|a, b| a.name.cmp(&b.name));
527515 rewrites.dedup();
528516 rewrites
529517 .into_iter()
530518 .map(|rewrite| {
531− format!(
519+ Ok(format!(
532520 "{}: {} -> {}",
533521 rewrite.name,
534− rewrite.old.to_hex_with_len(7),
535− rewrite.new.to_hex_with_len(7)
536− )
522+ crate::change_id::display(repository, rewrite.old, 7)?,
523+ crate::change_id::display(repository, rewrite.new, 7)?
524+ ))
537525 })
538526 .collect()
539527 }
540528
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+
541534 #[cfg(test)]
542535 mod tests {
543536 use std::{path::Path, process::Command as ProcessCommand};
@@ -547,13 +540,11 @@ mod tests { · 3 unmodified lines
547540 use super::*;
548541
549542 #[test]
550− fn rewritten_ref_lines_are_sorted_and_show_the_commit_mapping() {
551− let old: gix::ObjectId = "1111111111111111111111111111111111111111"
552− .parse()
553− .expect("valid object ID");
554− let new: gix::ObjectId = "2222222222222222222222222222222222222222"
555− .parse()
556− .expect("valid object ID");
543+ fn rewritten_ref_lines_are_sorted_and_show_the_commit_mapping() -> gix_testtools::Result {
544+ let fixture = gix_testtools::scripted_fixture_writable("history.sh")?;
545+ let repository = crate::test_repository::open(fixture.path())?;
546+ let old = repository.rev_parse_single("main~1")?.detach();
547+ let new = repository.rev_parse_single("main")?.detach();
557548 let branch = crate::edit::rebase::RefRewrite {
558549 name: "refs/heads/z".try_into().expect("valid ref name"),
559550 old,
@@ -565,11 +556,44 @@ mod tests { · 5 unmodified lines
565556 new,
566557 };
567558 assert_eq!(
568− ref_rewrite_lines(&[branch.clone(), first, branch]),
569− ["refs/heads/a: 1111111 -> 2222222", "refs/heads/z: 1111111 -> 2222222"],
559+ ref_rewrite_lines(&repository, &[branch.clone(), first, branch])?,
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+ ],
570572 "ref mappings are stable and duplicate-free"
571573 );
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, &notice, 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(())
573597 }
574598
575599 #[test]
@@ -951,12 +975,12 @@ mod tests { · 375 unmodified lines
951975 assert_eq!(repeated[0].name, pins[0].name, "an existing symbolic pin is reused");
952976 assert_eq!(crate::history::all_pins(&repository)?.len(), 4);
953977 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")?;
955979 assert!(label.starts_with("pin:"), "output names the pin");
956980 assert_eq!(
957− short_id,
958− main.attach(&repository).shorten()?.to_string(),
959− "output uses the repository's abbreviated ID"
981+ ids,
982+ crate::change_id::display_short(&repository, main)?,
983+ "output uses matching repository-abbreviated commit and change IDs"
960984 );
961985 repository.reference(
962986 "refs/heads/main",
gix-tix/src/command/new.rs+7−8
@@ -72,14 +72,13 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 71 unmodified lines
7272 repository.object_cache_size(None);
7373 crate::edit::create::apply_reporting(repository, &graph, prepared, &edited)?
7474 };
75− println!(
76− "{}",
77− outcome
78− .selected
79− .context("creating a commit did not produce a selection")?
80− .to_hex_with_len(7)
81− );
82− super::print_ref_rewrites(&outcome.ref_rewrites);
75+ let repository = crate::open_repository(&repository_path, bare, false)
76+ .context("could not reopen repository after creating commit")?;
77+ let selected = outcome
78+ .selected
79+ .context("creating a commit did not produce a selection")?;
80+ println!("{}", crate::change_id::display(&repository, selected, 7)?);
81+ super::print_ref_rewrites(&repository, &outcome.ref_rewrites)?;
8382 Ok(())
8483 }
8584
gix-tix/src/command/rebase.rs+12−7
@@ -210,7 +210,7 @@ fn prepare(repo: &gix::Repository, args: &Todo) -> Result<todo::Prepared> { · 209 unmodified lines
210210 })
211211 })
212212 .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)
214214 }
215215
216216 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
259259 match rebase::perform_plan(&repo, &graph, parsed.plan)? {
260260 rebase::PlanPerform::Complete(outcome) => {
261261 let revisions = mapped_revisions(&tips, |id| outcome.map(id));
262− if outcome.selected.is_some() {
262+ if let Some(selected) = outcome.selected {
263263 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, &notice, selected)?);
265266 } else {
266267 println!("rebased history");
267268 }
268− super::print_ref_rewrites(&outcome.ref_rewrites);
269+ super::print_ref_rewrites(&repo, &outcome.ref_rewrites)?;
269270 Ok(())
270271 }
271272 rebase::PlanPerform::Conflict(mut conflict) => {
@@ -283,7 +284,7 @@ fn apply_document(repo: gix::Repository, document: &[u8], materialize_conflicts: · 11 unmodified lines
283284 conflict.persist_objects()?;
284285 let plan = conflict.continuation_plan();
285286 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;
287288 let revisions = mapped_revisions(&tips, |id| conflict.map(id));
288289 if destination == Path::new("-") {
289290 let mut stdout = std::io::stdout().lock();
@@ -314,7 +315,7 @@ fn apply_document(repo: gix::Repository, document: &[u8], materialize_conflicts: · 24 unmodified lines
314315 return Err(err);
315316 }
316317 };
317− super::print_ref_rewrites(&ref_rewrites);
318+ super::print_ref_rewrites(&repo, &ref_rewrites)?;
318319 eprintln!("{notice}; continue with `tix rebase apply {}`", destination.display());
319320 anyhow::bail!("rebase stopped at a materialized conflict")
320321 }
@@ -483,7 +484,10 @@ mod tests { · 162 unmodified lines
483484 "moving to the updated base makes an unchanged todo actionable"
484485 );
485486 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+ )),
487491 "the TUI-selected hidden tip is labelled as the updated base: {document}"
488492 );
489493
@@ -556,6 +560,7 @@ mod tests { · 66 unmodified lines
556560 ],
557561 &[after],
558562 todo::OntoKind::Onto,
563+ true,
559564 )?;
560565 let generated = std::str::from_utf8(&prepared.document)?;
561566 let state = &generated[generated
gix-tix/src/command/reword.rs+28−15
@@ -59,13 +59,17 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 58 unmodified lines
5959 .context("author is not valid UTF-8")?;
6060
6161 if let Some(message) = explicit_message(&args.edit, std::io::stdin())? {
62− return finish(crate::edit::reword::apply_message_reporting(
63− repository,
64− &graph,
65− target,
66− &message,
67− author.map(AsRef::as_ref),
68− )?);
62+ let output_repository = repository.clone();
63+ return finish(
64+ &output_repository,
65+ crate::edit::reword::apply_message_reporting(
66+ repository,
67+ &graph,
68+ target,
69+ &message,
70+ author.map(AsRef::as_ref),
71+ )?,
72+ );
6973 }
7074
7175 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
8993 let mut repository = crate::open_repository(&repository_path, bare, false)
9094 .context("could not reopen repository after editing commit")?;
9195 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+ )
93102 }
94103
95104 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
122131 }
123132 }
124133
125−fn finish(outcome: crate::edit::reword::Outcome) -> Result<()> {
134+fn finish(repository: &gix::Repository, outcome: crate::edit::reword::Outcome) -> Result<()> {
126135 match outcome.commit {
127− Some(id) => println!("{}", id.to_hex_with_len(7)),
136+ Some(id) => println!("{}", crate::change_id::display(repository, id, 7)?),
128137 None => println!("no reword performed: the edited commit was unchanged"),
129138 }
130− super::print_ref_rewrites(&outcome.ref_rewrites);
139+ super::print_ref_rewrites(repository, &outcome.ref_rewrites)?;
131140 Ok(())
132141 }
133142
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<()> {
135148 match (outcome.commit, outcome.enrichment) {
136− (Some(id), _) => println!("{}", id.to_hex_with_len(7)),
137− (None, Some(_)) => println!("{}", original.to_hex_with_len(7)),
149+ (Some(id), _) => println!("{}", crate::change_id::display(repository, id, 7)?),
150+ (None, Some(_)) => println!("{}", crate::change_id::display(repository, original, 7)?),
138151 (None, None) => println!("no reword performed: the edited commit was unchanged"),
139152 }
140− super::print_ref_rewrites(&outcome.ref_rewrites);
153+ super::print_ref_rewrites(repository, &outcome.ref_rewrites)?;
141154 Ok(())
142155 }
143156
gix-tix/src/command/travel.rs+17−5
@@ -22,7 +22,7 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 21 unmodified lines
2222 let detached = head.is_detached();
2323 drop(head);
2424 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)?);
2626 return Ok(());
2727 }
2828
@@ -51,16 +51,28 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { · 22 unmodified lines
5151 let bare = repository.is_bare();
5252 drop(repository);
5353 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")?;
5561 println!(
5662 "{}",
57− notice.unwrap_or_else(|| format!("already at {}", selected.to_hex_with_len(7)))
63+ super::notice_with_change_id(
64+ &repository,
65+ &notice.unwrap_or_else(|| format!("already at {}", selected.to_hex_with_len(7))),
66+ selected,
67+ )?
5868 );
59− super::print_ref_rewrites(&ref_rewrites);
69+ super::print_ref_rewrites(&repository, &ref_rewrites)?;
6070 }
6171 crate::edit::time_travel::Perform::Conflict(conflict) if args.materialize_conflicts => {
6272 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)?;
6476 anyhow::bail!("{notice}");
6577 }
6678 crate::edit::time_travel::Perform::Conflict(_) => {
gix-tix/src/edit/time_travel.rs+6−1
@@ -24,6 +24,7 @@ use super::stash::SavedStash; · 23 unmodified lines
2424 pub(crate) enum Perform {
2525 Complete {
2626 notice: Option<String>,
27+ selected: ObjectId,
2728 ref_rewrites: Vec<super::rebase::RefRewrite>,
2829 },
2930 Conflict(Conflict),
@@ -477,7 +478,11 @@ pub(crate) fn perform_reporting_rebased( · 447 unmodified lines
477478 super::stash::apply(repository_path, bare, &workdir, stash)?,
478479 );
479480 }
480− Ok(Perform::Complete { notice, ref_rewrites })
481+ Ok(Perform::Complete {
482+ notice,
483+ selected,
484+ ref_rewrites,
485+ })
481486 }
482487
483488 #[derive(Clone, Debug, Eq, PartialEq)]
gix-tix/src/edit/todo.rs+66−23
@@ -84,6 +84,7 @@ pub(crate) fn prepare( · 83 unmodified lines
8484 commits: &[Commit],
8585 resolved_tips: &[ObjectId],
8686 onto_kind: OntoKind,
87+ show_change_ids: bool,
8788 ) -> Result<Prepared> {
8889 repo.find_commit(base)
8990 .context("could not find the selected rebase base")?;
@@ -160,11 +161,14 @@ pub(crate) fn prepare( · 70 unmodified lines
160161 ref_points.dedup();
161162 let expected_refs = rebase::capture_refs(repo, &ref_points, &tips)?;
162163
163− let source = short(repo, base)?;
164+ let source = short(repo, base, show_change_ids)?;
164165 let title = if base == onto {
165166 format!("# Rebase from `{source}`")
166167 } 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+ )
168172 };
169173 let state = State {
170174 base,
@@ -206,6 +210,7 @@ pub(crate) fn prepare( · 35 unmodified lines
206210 repo,
207211 section.parent,
208212 (section.parent == onto).then_some((anchor_kind, anchor_title.as_str())),
213+ show_change_ids,
209214 )?;
210215 if !scope_set.contains(&section.parent) {
211216 write_refs_at(&mut body, &state.expected_refs, section.parent)?;
@@ -218,12 +223,25 @@ pub(crate) fn prepare( · 6 unmodified lines
218223 "pick"
219224 };
220225 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+ );
222234 write_refs_at(&mut body, &state.expected_refs, *id)?;
223235 }
224236 }
225237 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+ )?;
227245 write_refs_at(&mut body, &state.expected_refs, onto)?;
228246 }
229247 write_bottom_up(&mut document, &body)?;
@@ -239,6 +257,7 @@ pub(crate) fn prepare_continuation( · 9 unmodified lines
239257 repo: &gix::Repository,
240258 plan: &rebase::Plan,
241259 tips: Vec<ObjectId>,
260+ show_change_ids: bool,
242261 ) -> Result<Prepared> {
243262 let resolved = plan.steps.iter().find_map(|step| match step.commit {
244263 rebase::PlanCommit::Resolved(id) => Some(id),
@@ -283,6 +302,7 @@ pub(crate) fn prepare_continuation( · 38 unmodified lines
283302 repo,
284303 parent,
285304 (parent == plan.base).then_some(("base", base_title.as_str())),
305+ show_change_ids,
286306 )?;
287307 if matches!(step.parent, rebase::PlanParent::Existing(_)) {
288308 write_plan_refs_at(&mut body, &plan.expected_refs, step.parent)?;
@@ -300,9 +320,17 @@ pub(crate) fn prepare_continuation( · 11 unmodified lines
300320 match step.commit {
301321 rebase::PlanCommit::Pick(id) | rebase::PlanCommit::Resolved(id) => {
302322 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+ }
304332 } else {
305− short(repo, id)?
333+ short(repo, id, show_change_ids)?
306334 };
307335 let title = anchor_title(repo, id)?;
308336 body.extend_from_slice(
@@ -318,7 +346,7 @@ pub(crate) fn prepare_continuation( · 9 unmodified lines
318346 body.extend_from_slice(
319347 format!(
320348 "`squash {}` {}{}\n",
321− short(repo, *id)?,
349+ short(repo, *id, show_change_ids)?,
322350 commit_states(repo, *id)?,
323351 title
324352 )
@@ -521,8 +549,9 @@ fn write_fork_heading( · 196 unmodified lines
521549 repo: &gix::Repository,
522550 id: ObjectId,
523551 annotation: Option<(&str, &str)>,
552+ show_change_ids: bool,
524553 ) -> 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());
526555 if let Some((kind, title)) = annotation {
527556 out.extend_from_slice(format!(" ({kind}) {title}").as_bytes());
528557 }
@@ -573,12 +602,16 @@ fn walk(id: ObjectId, children: &HashMap<ObjectId, Vec<ObjectId>>, section: &mut · 44 unmodified lines
573602 }
574603 }
575604
576−fn short(repo: &gix::Repository, id: ObjectId) -> Result<String> {
577− Ok(id
578− .attach(repo)
579− .shorten()
580− .context("could not shorten a rebase todo ID")?
581− .to_string())
605+fn short(repo: &gix::Repository, id: ObjectId, show_change_id: bool) -> Result<String> {
606+ if show_change_id {
607+ crate::change_id::display_short(repo, id).context("could not format a rebase todo ID")
608+ } else {
609+ Ok(id
610+ .attach(repo)
611+ .shorten()
612+ .context("could not shorten a rebase todo ID")?
613+ .to_string())
614+ }
582615 }
583616
584617 fn parse_state(repo: &gix::Repository, input: &str) -> Result<Option<State>> {
@@ -1178,7 +1211,7 @@ mod tests { · 593 unmodified lines
11781211 commits: &[Commit],
11791212 _head: Option<ObjectId>,
11801213 ) -> Result<Prepared> {
1181− prepare(repo, base, onto, commits, &[], OntoKind::UpdatedBase)
1214+ prepare(repo, base, onto, commits, &[], OntoKind::UpdatedBase, true)
11821215 }
11831216
11841217 fn parse_plan(repo: &gix::Repository, document: &[u8]) -> Result<rebase::Plan> {
@@ -1213,8 +1246,14 @@ mod tests { · 28 unmodified lines
12131246 "the first line explains that saving unchanged is a no-op"
12141247 );
12151248 assert!(document.contains(STATE_START), "the todo carries its transaction state");
1216− assert!(document.contains(&format!("# Rebase from `{}`", base.to_hex_with_len(7))));
1217− assert!(document.contains(&format!("fork {} (base) base", base.to_hex_with_len(7))));
1249+ assert!(document.contains(&format!(
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+ )));
12181257 let middle = document.find("`pick ").expect("the oldest pick is shown");
12191258 let tip = document.find("`@pick ").expect("HEAD is marked");
12201259 let base = document.find("fork ").expect("the base separator is shown");
@@ -1450,9 +1489,12 @@ mod tests { · 229 unmodified lines
14501489
14511490 let prepared = prepare_test(&repo, base, base, &commits, Some(tip))?;
14521491 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+ )));
14541496 assert!(
1455− document.contains(&format!("fork {} ", middle.to_hex_with_len(7))),
1497+ document.contains(&format!("fork {} ", crate::change_id::display_short(&repo, middle)?)),
14561498 "a fork within the editable tree has no external-anchor annotation"
14571499 );
14581500 let plan = parse_plan(&repo, document.as_bytes())?;
@@ -1483,15 +1525,15 @@ mod tests { · 24 unmodified lines
14831525 assert!(
14841526 document.contains(&format!(
14851527 "# Rebase from `{}` onto `{}`",
1486− base.to_hex_with_len(7),
1487− onto.to_hex_with_len(7)
1528+ crate::change_id::display_short(&repo, base)?,
1529+ crate::change_id::display_short(&repo, onto)?
14881530 )),
14891531 "the update target is explicit in the document title"
14901532 );
14911533 assert!(
14921534 document.contains(&format!(
14931535 "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)?
14951537 )),
14961538 "the unfamiliar fork target carries its raw UI title"
14971539 );
@@ -1627,6 +1669,7 @@ mod tests { · 129 unmodified lines
16271669 }],
16281670 },
16291671 vec![middle],
1672+ true,
16301673 )?;
16311674 assert!(
16321675 prepared
@@ -1642,7 +1685,7 @@ mod tests { · 9 unmodified lines
16421685 );
16431686 assert!(document.contains(&"0".repeat(40)), "the conflict uses the full null ID");
16441687 assert!(
1645− document.contains(&format!("`squash {}`", tip.to_hex_with_len(7))),
1688+ document.contains(&format!("`squash {}`", crate::change_id::display_short(&repo, tip)?)),
16461689 "unapplied squash sources remain editable"
16471690 );
16481691 let plan = parse_plan(&repo, &prepared.document)?;
gix-tix/src/lib.rs+1−0
@@ -3881,6 +3881,7 @@ fn rebase_history( · 3880 unmodified lines
38813881 &commits,
38823882 &[],
38833883 edit::todo::OntoKind::UpdatedBase,
3884+ false,
38843885 )?;
38853886 (prepared, editor)
38863887 };
gix-tix/src/ref_tree.rs+35−11
@@ -1512,13 +1512,17 @@ pub(crate) fn render_full( · 1511 unmodified lines
15121512 )?;
15131513 let graph = graph.ok_or_else(|| anyhow::anyhow!("history traversal did not produce a graph"))?;
15141514 let decorations = decorations.unwrap_or_default();
1515− Ok(render_overview(
1516− &Overview::new(&graph, &refs, &decorations, show_tags),
1517− unicode,
1518− ))
1515+ let overview = Overview::new(&graph, &refs, &decorations, show_tags);
1516+ let labels = overview
1517+ .nodes
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))
15191523 }
15201524
1521−fn render_overview(overview: &Overview, unicode: bool) -> String {
1525+fn render_overview(overview: &Overview, unicode: bool, commit_labels: &HashMap<ObjectId, String>) -> String {
15221526 if overview.nodes.is_empty() {
15231527 return String::new();
15241528 }
@@ -1548,7 +1552,11 @@ fn render_overview(overview: &Overview, unicode: bool) -> String { · 23 unmodified lines
15481552 out.push_str(&lane);
15491553 out.extend(std::iter::repeat_n(' ', placed.rail_width.saturating_sub(width)));
15501554 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));
15521560 if unicode {
15531561 out.push_str(&label);
15541562 } else {
@@ -2043,8 +2051,8 @@ mod tests { · 488 unmodified lines
20432051 !overview.nodes.iter().any(|node| node.id == id(5)),
20442052 "ordinary pin-only tips are absent"
20452053 );
2046− let unicode = render_overview(overview, true);
2047− let ascii = render_overview(overview, false);
2054+ let unicode = render_overview(overview, true, &HashMap::new());
2055+ let ascii = render_overview(overview, false, &HashMap::new());
20482056 assert_eq!(unicode.matches('📌').count(), 1, "detached HEAD has one marker");
20492057 assert!(
20502058 unicode.contains("★main"),
@@ -2414,7 +2422,7 @@ mod tests { · 363 unmodified lines
24142422 fn full_rendering_is_unstyled_ascii() {
24152423 let (graph, refs, decorations) = fixture();
24162424 let overview = Overview::new(&graph, &refs, &decorations, true);
2417− let rendered = render_overview(&overview, false);
2425+ let rendered = render_overview(&overview, false, &HashMap::new());
24182426
24192427 assert!(rendered.contains("main"), "HEAD is rendered without selection state");
24202428 assert!(rendered.contains("topic"), "ordinary nodes retain their label");
@@ -2424,12 +2432,28 @@ mod tests { · 3 unmodified lines
24242432 assert!(!rendered.contains('\u{1b}'), "plain output has no terminal styles");
24252433 }
24262434
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+
24272451 #[test]
24282452 fn rail_layout_uses_rounded_unicode_connections_and_ascii_fallbacks() {
24292453 let (graph, refs, decorations) = fixture();
24302454 let overview = Overview::new(&graph, &refs, &decorations, true);
2431− let unicode = render_overview(&overview, true);
2432− let ascii = render_overview(&overview, false);
2455+ let unicode = render_overview(&overview, true, &HashMap::new());
2456+ let ascii = render_overview(&overview, false, &HashMap::new());
24332457
24342458 assert!(
24352459 unicode.contains("│ ● topic"),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t