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
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. The76- `-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.
gix-tix/src/change_id.rs+21−0
@@ -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
14pub(crate) const HEADER: &str = "change-id";15pub(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
31pub(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
39pub(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
30pub(crate) fn abbreviations(51pub(crate) fn abbreviations(
31 repo: &gix::Repository,52 repo: &gix::Repository,
32 ids: impl IntoIterator<Item = ObjectId>,53 ids: impl IntoIterator<Item = ObjectId>,
gix-tix/src/command.rs+78−54
@@ -7,7 +7,7 @@ use std::{ · 6 unmodified lines
7 7
8use anyhow::{Context, Result};8use anyhow::{Context, Result};
9use clap::Parser;9use clap::Parser;
10use gix::prelude::{ObjectIdExt, ReferenceExt};10use gix::prelude::ReferenceExt;
11use ratatui::text::Line;11use ratatui::text::Line;
12 12
13mod new;13mod 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 outcome161 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, &notice, 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.id455 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 outcome470 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 outcome496 let selected = outcome.selected.context("splitting did not produce a selection")?;
510 .selected497 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
518fn print_ref_rewrites(rewrites: &[crate::edit::rebase::RefRewrite]) {502fn 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
524fn ref_rewrite_lines(rewrites: &[crate::edit::rebase::RefRewrite]) -> Vec<String> {509fn 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 rewrites516 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
529fn 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)]
542mod tests {535mod 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, &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(())
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",
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
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 outcome77 let selected = outcome
78 .selected78 .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
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
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
216fn resolve_commit(repo: &gix::Repository, revision: &OsStr, description: &str) -> Result<ObjectId> {216fn 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, &notice, 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[generated566 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
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
95pub(super) fn explicit_message(args: &MessageArgs, mut stdin: impl Read) -> Result<Option<Vec<u8>>> {104pub(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
125fn finish(outcome: crate::edit::reword::Outcome) -> Result<()> {134fn 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
134fn finish_editor(outcome: crate::edit::reword::Outcome, original: gix::ObjectId) -> Result<()> {143fn 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
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
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 &notice.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(_) => {
gix-tix/src/edit/time_travel.rs+6−1
@@ -24,6 +24,7 @@ use super::stash::SavedStash; · 23 unmodified lines
24pub(crate) enum Perform {24pub(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)]
gix-tix/src/edit/todo.rs+66−23
@@ -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(&section.parent) {215 if !scope_set.contains(&section.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 title351 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
576fn short(repo: &gix::Repository, id: ObjectId) -> Result<String> {605fn short(repo: &gix::Repository, id: ObjectId, show_change_id: bool) -> Result<String> {
577 Ok(id606 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
584fn parse_state(repo: &gix::Repository, input: &str) -> Result<Option<State>> {617fn 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 prepared1675 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)?;
gix-tix/src/lib.rs+1−0
@@ -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 };
gix-tix/src/ref_tree.rs+35−11
@@ -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
1521fn render_overview(overview: &Overview, unicode: bool) -> String {1525fn 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"),

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