History

feat: add checks-pass tree enrichments

Store checks-pass metadata as worktree-local Git notes keyed by tree ID. Show the marker in the TUI and plain status output, and expose it through the enrich shortcut group.

Codex committed 2 months ago

Commit b32ffbc1 parent 254f9356 files · +278 −28Browse files

Unified · Split

6 files
  1. gix-tix/spec.md +10 −3
  2. gix-tix/src/app.rs +40 −2
  3. gix-tix/src/command.rs +23 −4
  4. gix-tix/src/enrich.rs +135 −13
  5. gix-tix/src/lib.rs +51 −0
  6. gix-tix/src/ui.rs +19 −6
gix-tix/spec.md+10−3
@@ -247,7 +247,12 @@ without trading responsiveness for metadata that is not visible. · 246 unmodified lines
247247 Consequently, rewritten commits retain their metadata and commits sharing a
248248 change ID share it as well. Malformed enrichments are ignored for display and
249249 diagnosed, while mutation refuses to overwrite them.
250−- Todo and note enrichments receive a leading `🚧` and `📝` respectively before
250+- Tree enrichments use the same human-readable Git config format in notes headed
251+ at `refs/worktree/tix/enrich-tree`. They are keyed directly by tree object ID;
252+ `[tree] checks-pass = true` therefore applies to every commit with that exact
253+ tree and naturally disappears when a rewrite changes the tree.
254+- Todo, note, and checks-pass enrichments receive a leading `🚧`, `📝`, and `✔️`
255+ respectively before
251256 the graph, with no gap between them or the following status field. The dedicated
252257 field remains visible alongside selection, dirty-worktree, and conflict markers.
253258 `tix show` emits the same field and aligns unmarked rows when any displayed
@@ -1014,8 +1019,10 @@ space first; changes blocks adapt within the remaining history width. · 760 unmodified lines
10141019 - The `n` in `enrich` toggles its shortcut group. On any commit eligible for rewording,
10151020 `n t` toggles `[commit] todo`, preserving a saved note, and `n o` opens
10161021 `[commit] note` in Git's editor as Markdown. Saving or removing a note preserves
1017− the todo flag, and toggling todo preserves the note. `n g` edits the real Git
1018− note and remains available when the Tix-specific actions are not. The group is mutually
1022+ the todo flag, and toggling todo preserves the note. `n c` toggles
1023+ `[tree] checks-pass` for any selected commit, including immutable boundaries.
1024+ `n g` edits the real Git note and remains available when the commit-specific
1025+ Tix actions are not. The group is mutually
10191026 exclusive with the view, commit, actions, and information groups and otherwise follows
10201027 their closing behavior.
10211028
gix-tix/src/app.rs+40−2
@@ -356,6 +356,7 @@ pub(crate) enum Action { · 355 unmodified lines
356356 ToggleActions,
357357 ToggleEnrich,
358358 ToggleTodo,
359+ ToggleChecksPass,
359360 EditNote,
360361 EditGitNote,
361362 ToggleInformation,
@@ -435,6 +436,7 @@ pub(crate) enum Effect { · 73 unmodified lines
435436 TimeTravel(ObjectId),
436437 TogglePin(ObjectId),
437438 ToggleTodo(ObjectId),
439+ ToggleChecksPass(ObjectId),
438440 EditNote(ObjectId),
439441 EditGitNote(ObjectId),
440442 VerifySignatures(Vec<ObjectId>),
@@ -475,6 +477,7 @@ pub(crate) struct App { · 34 unmodified lines
475477 titles: Vec<u8>,
476478 notes: HashMap<ObjectId, Vec<BString>>,
477479 enrichments: HashMap<ObjectId, crate::enrich::Enrichment>,
480+ tree_enrichments: HashMap<ObjectId, crate::enrich::TreeEnrichment>,
478481 graph: Option<Graph>,
479482 attributions: Vec<Attribution>,
480483 #[cfg(test)]
@@ -575,6 +578,7 @@ impl App { · 94 unmodified lines
575578 titles: Vec::new(),
576579 notes: HashMap::new(),
577580 enrichments: HashMap::new(),
581+ tree_enrichments: HashMap::new(),
578582 graph: None,
579583 attributions: Vec::new(),
580584 #[cfg(test)]
@@ -994,8 +998,23 @@ impl App { · 413 unmodified lines
994998 .and_then(|enrichment| enrichment.note.as_ref().map(|note| note.as_bstr()))
995999 }
9961000
1001+ pub(crate) fn tree_enrichment_loaded(&self, id: ObjectId) -> bool {
1002+ self.tree_enrichments.contains_key(&id)
1003+ }
1004+
1005+ pub(crate) fn set_tree_enrichment(&mut self, id: ObjectId, enrichment: crate::enrich::TreeEnrichment) {
1006+ self.tree_enrichments.insert(id, enrichment);
1007+ }
1008+
1009+ pub(crate) fn checks_pass(&self, id: ObjectId) -> bool {
1010+ self.tree_enrichments
1011+ .get(&id)
1012+ .is_some_and(|enrichment| enrichment.checks_pass)
1013+ }
1014+
9971015 pub(crate) fn clear_enrichments(&mut self) {
9981016 self.enrichments.clear();
1017+ self.tree_enrichments.clear();
9991018 }
10001019
10011020 pub(crate) fn render_lanes(&self, range: Range<usize>) -> RenderedLanes {
@@ -1068,7 +1087,11 @@ impl App { · 66 unmodified lines
10681087 }
10691088 if !matches!(
10701089 &action,
1071− Action::ToggleEnrich | Action::ToggleTodo | Action::EditNote | Action::EditGitNote
1090+ Action::ToggleEnrich
1091+ | Action::ToggleTodo
1092+ | Action::ToggleChecksPass
1093+ | Action::EditNote
1094+ | Action::EditGitNote
10721095 ) {
10731096 self.enrich_expanded = false;
10741097 }
@@ -1443,6 +1466,11 @@ impl App { · 368 unmodified lines
14431466 self.rows[self.selected.expect("todo requires a selection")].id,
14441467 )];
14451468 }
1469+ Action::ToggleChecksPass => {
1470+ if let Some(id) = self.selected.and_then(|index| self.rows.get(index)).map(|row| row.id) {
1471+ return vec![Effect::ToggleChecksPass(id)];
1472+ }
1473+ }
14461474 Action::EditNote if self.can_reword() => {
14471475 return vec![Effect::EditNote(
14481476 self.rows[self.selected.expect("note requires a selection")].id,
@@ -1783,7 +1811,7 @@ impl App { · 334 unmodified lines
17831811 self.pending_hidden_rows = None;
17841812 self.titles = Vec::new();
17851813 self.notes.clear();
1786− self.enrichments.clear();
1814+ self.clear_enrichments();
17871815 self.graph = None;
17881816 self.attributions = Vec::new();
17891817 #[cfg(test)]
@@ -4271,6 +4299,11 @@ mod tests { · 2481 unmodified lines
42714299 vec![Effect::EditGitNote(id(1))],
42724300 "Git notes are available on every selection"
42734301 );
4302+ assert_eq!(
4303+ app.update(Action::ToggleChecksPass),
4304+ vec![Effect::ToggleChecksPass(id(1))],
4305+ "tree enrichments are available on every selection"
4306+ );
42744307 assert!(app.enrich_expanded, "the grouped actions keep enrich open");
42754308
42764309 app.update(Action::MoveDown);
@@ -4291,6 +4324,11 @@ mod tests { · 14 unmodified lines
42914324 vec![Effect::EditGitNote(id(1))],
42924325 "immutable boundaries still accept Git notes"
42934326 );
4327+ assert_eq!(
4328+ app.update(Action::ToggleChecksPass),
4329+ vec![Effect::ToggleChecksPass(id(1))],
4330+ "immutable boundaries still accept tree enrichments"
4331+ );
42944332 }
42954333
42964334 #[test]
gix-tix/src/command.rs+23−4
@@ -311,6 +311,20 @@ fn write_history( · 310 unmodified lines
311311 }
312312 }
313313
314+ let mut checks_pass_ids = HashSet::new();
315+ let mut tree_enrichments = crate::enrich::open_tree(repository)?;
316+ for row in &app.rows {
317+ let loaded = crate::enrich::tree_id(repository, row.id)
318+ .and_then(|tree_id| crate::enrich::load_tree(&mut tree_enrichments, tree_id));
319+ match loaded {
320+ Ok(enrichment) if enrichment.checks_pass => {
321+ checks_pass_ids.insert(row.id);
322+ }
323+ Ok(_) => {}
324+ Err(err) => tracing::warn!(commit_id = %row.id, error = %err, "ignored malformed tix tree enrichment"),
325+ }
326+ }
327+
314328 let change_ids = crate::change_id::abbreviations(repository, app.rows.iter().map(|row| row.id), 7)?;
315329
316330 let mailmap = repository.open_mailmap();
@@ -322,6 +336,7 @@ fn write_history( · 5 unmodified lines
322336 Line::raw(crate::enrich::marker(
323337 todo_ids.contains(&row.id),
324338 enrichment_note_ids.contains(&row.id),
339+ checks_pass_ids.contains(&row.id),
325340 ))
326341 .width()
327342 })
@@ -337,8 +352,11 @@ fn write_history( · 9 unmodified lines
337352 note_ids.contains(&row.id),
338353 change_ids.values.get(&row.id).copied(),
339354 );
340− let enrichment_marker =
341− crate::enrich::marker(todo_ids.contains(&row.id), enrichment_note_ids.contains(&row.id));
355+ let enrichment_marker = crate::enrich::marker(
356+ todo_ids.contains(&row.id),
357+ enrichment_note_ids.contains(&row.id),
358+ checks_pass_ids.contains(&row.id),
359+ );
342360 let ambiguity_marker = if change_ids.ambiguous.contains(&row.id) {
343361 "💥"
344362 } else {
@@ -1150,6 +1168,7 @@ mod tests { · 805 unmodified lines
11501168 create_pins(&repository, &[OsString::from(orphan.to_string())])?;
11511169 let head_change_id = crate::change_id::for_commit(&repository, head)?;
11521170 assert!(crate::enrich::toggle(&repository, head)?.todo);
1171+ assert!(crate::enrich::toggle_checks_pass(&repository, head)?.checks_pass);
11531172
11541173 let mut output = Vec::new();
11551174 write_history(&repository, &[], &[OsString::from("v1")], &mut output)?;
@@ -1202,8 +1221,8 @@ mod tests { · 46 unmodified lines
12021221 }
12031222 assert!(output.contains('●'), "history graph lanes are rendered");
12041223 assert!(
1205− output.lines().any(|line| line.starts_with("🚧💥├")),
1206− "todos directly lead their rows: {output:?}"
1224+ output.lines().any(|line| line.starts_with("🚧✔️💥├")),
1225+ "commit and tree enrichments directly lead their rows: {output:?}"
12071226 );
12081227 assert!(output.contains("📌"), "applicable pins are decorated and traversed");
12091228 assert!(
gix-tix/src/enrich.rs+135−13
@@ -8,6 +8,7 @@ use gix::{ · 7 unmodified lines
88 };
99
1010 pub(crate) const REF_NAME: &str = "refs/worktree/tix/enrich";
11+pub(crate) const TREE_REF_NAME: &str = "refs/worktree/tix/enrich-tree";
1112
1213 #[derive(Clone, Debug, Default, Eq, PartialEq)]
1314 pub(crate) struct Enrichment {
@@ -15,30 +16,47 @@ pub(crate) struct Enrichment { · 1 unmodified line
1516 pub note: Option<BString>,
1617 }
1718
19+#[derive(Clone, Debug, Default, Eq, PartialEq)]
20+pub(crate) struct TreeEnrichment {
21+ pub checks_pass: bool,
22+}
23+
1824 #[derive(Clone, Debug, Default, Eq, PartialEq)]
1925 pub(crate) struct Headers {
2026 pub todo: bool,
2127 pub message: Option<BString>,
2228 }
2329
24−pub(crate) fn marker(todo: bool, note: bool) -> &'static str {
25− match (todo, note) {
26− (true, true) => "🚧📝",
27− (true, false) => "🚧",
28− (false, true) => "📝",
29− (false, false) => "",
30+pub(crate) fn marker(todo: bool, note: bool, checks_pass: bool) -> &'static str {
31+ match (todo, note, checks_pass) {
32+ (true, true, true) => "🚧📝✔️",
33+ (true, true, false) => "🚧📝",
34+ (true, false, true) => "🚧✔️",
35+ (true, false, false) => "🚧",
36+ (false, true, true) => "📝✔️",
37+ (false, true, false) => "📝",
38+ (false, false, true) => "✔️",
39+ (false, false, false) => "",
3040 }
3141 }
3242
3343 pub(crate) fn open(repo: &gix::Repository) -> Result<gix::note::Platform<'_>> {
44+ open_at(repo, REF_NAME)
45+}
46+
47+pub(crate) fn open_tree(repo: &gix::Repository) -> Result<gix::note::Platform<'_>> {
48+ open_at(repo, TREE_REF_NAME)
49+}
50+
51+fn open_at<'repo>(repo: &'repo gix::Repository, reference: &str) -> Result<gix::note::Platform<'repo>> {
3452 repo.notes()
3553 .context("could not open tix enrichments")?
36− .with_refs([REF_NAME])
54+ .with_refs([reference])
3755 .context("could not select the tix enrich reference")
3856 }
3957
4058 pub(crate) fn load(notes: &mut gix::note::Platform, change_id: ChangeId) -> Result<Enrichment> {
41− let config = load_config(notes, change_id)?;
59+ let config = load_config(notes, ObjectId::from(change_id))?;
4260 let Some(config) = config else {
4361 return Ok(Enrichment::default());
4462 };
@@ -51,10 +69,20 @@ pub(crate) fn load(notes: &mut gix::note::Platform, change_id: ChangeId) -> Resu · 6 unmodified lines
5169 })
5270 }
5371
54−fn load_config(notes: &mut gix::note::Platform, change_id: ChangeId) -> Result<Option<File>> {
55− let found = notes
56− .get(ObjectId::from(change_id))
57− .context("could not load the tix enrichment")?;
72+pub(crate) fn load_tree(notes: &mut gix::note::Platform, tree_id: ObjectId) -> Result<TreeEnrichment> {
73+ let Some(config) = load_config(notes, tree_id)? else {
74+ return Ok(TreeEnrichment::default());
75+ };
76+ Ok(TreeEnrichment {
77+ checks_pass: config
78+ .boolean("tree.checks-pass")
79+ .context("tree.checks-pass is not a boolean")?
80+ .unwrap_or(false),
81+ })
82+}
83+
84+fn load_config(notes: &mut gix::note::Platform, object_id: ObjectId) -> Result<Option<File>> {
85+ let found = notes.get(object_id).context("could not load the tix enrichment")?;
5886 found
5987 .first()
6088 .map(|note| {
@@ -63,6 +91,14 @@ fn load_config(notes: &mut gix::note::Platform, change_id: ChangeId) -> Result<O · 2 unmodified lines
6391 .transpose()
6492 }
6593
94+pub(crate) fn tree_id(repo: &gix::Repository, commit_id: ObjectId) -> Result<ObjectId> {
95+ repo.find_commit(commit_id)
96+ .context("could not find the enriched commit")?
97+ .tree_id()
98+ .context("could not read the enriched commit tree")
99+ .map(gix::Id::detach)
100+}
101+
66102 pub(crate) fn toggle(repo: &gix::Repository, commit_id: ObjectId) -> Result<Enrichment> {
67103 update(repo, commit_id, |config| {
68104 let enabled = !config
@@ -95,6 +131,22 @@ pub(crate) fn set_note(repo: &gix::Repository, commit_id: ObjectId, note: Option · 26 unmodified lines
95131 })
96132 }
97133
134+pub(crate) fn toggle_checks_pass(repo: &gix::Repository, commit_id: ObjectId) -> Result<TreeEnrichment> {
135+ let tree_id = tree_id(repo, commit_id)?;
136+ update_tree(repo, tree_id, |config| {
137+ let enabled = !config
138+ .boolean("tree.checks-pass")
139+ .context("tree.checks-pass is not a boolean")?
140+ .unwrap_or(false);
141+ config
142+ .section_mut_or_create_new("tree", None)
143+ .context("could not create the tree enrichment section")?
144+ .set("checks-pass", if enabled { "true" } else { "false" })
145+ .context("could not update tree.checks-pass")?;
146+ Ok(())
147+ })
148+}
149+
98150 pub(crate) fn apply_headers(
99151 repo: &gix::Repository,
100152 commit_id: ObjectId,
@@ -155,7 +207,7 @@ fn update( · 54 unmodified lines
155207 ) -> Result<Enrichment> {
156208 let change_id = crate::change_id::for_commit(repo, commit_id)?;
157209 let mut notes = open(repo)?;
158− let mut config = load_config(&mut notes, change_id)?.unwrap_or_default();
210+ let mut config = load_config(&mut notes, ObjectId::from(change_id))?.unwrap_or_default();
159211 edit(&mut config)?;
160212 let reference: FullName = REF_NAME.try_into().expect("the tix enrich reference is valid");
161213 notes
@@ -164,6 +216,23 @@ fn update( · 2 unmodified lines
164216 load(&mut notes, change_id)
165217 }
166218
219+fn update_tree(
220+ repo: &gix::Repository,
221+ tree_id: ObjectId,
222+ edit: impl FnOnce(&mut File) -> Result<()>,
223+) -> Result<TreeEnrichment> {
224+ let mut notes = open_tree(repo)?;
225+ let mut config = load_config(&mut notes, tree_id)?.unwrap_or_default();
226+ edit(&mut config)?;
227+ let reference: FullName = TREE_REF_NAME
228+ .try_into()
229+ .expect("the tix tree enrich reference is valid");
230+ notes
231+ .add_to_ref(reference.as_ref(), tree_id, config.to_bstring())
232+ .context("could not write the tix tree enrichment")?;
233+ load_tree(&mut notes, tree_id)
234+}
235+
167236 #[cfg(test)]
168237 mod tests {
169238 use std::process::Command;
@@ -224,6 +293,46 @@ mod tests { · 54 unmodified lines
224293 Ok(())
225294 }
226295
296+ #[test]
297+ fn checks_pass_follows_only_the_exact_tree_and_preserves_other_fields() -> gix_testtools::Result {
298+ let fixture = gix_testtools::scripted_fixture_writable("history.sh")?;
299+ let repo = crate::test_repository::open_with(
300+ fixture.path(),
301+ ["user.name=checks author", "user.email=checks@example.com"],
302+ )?;
303+ let original = repo.head_id()?.detach();
304+ let tree = tree_id(&repo, original)?;
305+ let reference: FullName = TREE_REF_NAME.try_into()?;
306+ repo.notes()?.add_to_ref(
307+ reference.as_ref(),
308+ tree,
309+ b"[tree]\n\tchecks-pass = false\n\towner = me\n",
310+ )?;
311+
312+ assert!(toggle_checks_pass(&repo, original)?.checks_pass);
313+ let mut notes = open_tree(&repo)?;
314+ let note = notes.get(tree)?.into_iter().next().expect("the tree enrichment exists");
315+ let config = File::try_from(note.blob.data.as_bstr())?;
316+ assert_eq!(
317+ config.string("tree.owner").as_ref().map(|value| value.as_bstr()),
318+ Some(b"me".as_bstr())
319+ );
320+
321+ let mut rewritten = repo.find_commit(original)?.decode()?.into_owned()?;
322+ rewritten.message = "same tree".into();
323+ let rewritten = repo.write_object(&rewritten)?.detach();
324+ assert!(
325+ load_tree(&mut open_tree(&repo)?, tree_id(&repo, rewritten)?)?.checks_pass,
326+ "a message-only rewrite retains the tree marker"
327+ );
328+ let changed_tree = tree_id(&repo, repo.rev_parse_single("HEAD~1")?.detach())?;
329+ assert!(
330+ !load_tree(&mut open_tree(&repo)?, changed_tree)?.checks_pass,
331+ "a different tree has no marker"
332+ );
333+ Ok(())
334+ }
335+
227336 #[test]
228337 fn notes_and_todos_are_independent() -> gix_testtools::Result {
229338 let fixture = gix_testtools::scripted_fixture_writable("history.sh")?;
@@ -316,6 +425,13 @@ mod tests { · 86 unmodified lines
316425 toggle(&repo, id).is_err(),
317426 "mutation does not replace malformed enrichments"
318427 );
428+ let tree = tree_id(&repo, id)?;
429+ let reference: FullName = TREE_REF_NAME.try_into()?;
430+ repo.notes()?.add_to_ref(reference.as_ref(), tree, b"[tree")?;
431+ assert!(
432+ toggle_checks_pass(&repo, id).is_err(),
433+ "tree mutation does not replace malformed enrichments"
434+ );
319435 Ok(())
320436 }
321437
@@ -347,6 +463,12 @@ mod tests { · 25 unmodified lines
347463 load(&mut open(&linked)?, change_id)?.todo,
348464 "linked enrichments survive main-worktree changes"
349465 );
466+ let tree = tree_id(&main, id)?;
467+ assert!(toggle_checks_pass(&main, id)?.checks_pass);
468+ assert!(
469+ !load_tree(&mut open_tree(&linked)?, tree)?.checks_pass,
470+ "tree enrichments are also worktree-local"
471+ );
350472 Ok(())
351473 }
352474 }
gix-tix/src/lib.rs+51−0
@@ -2702,6 +2702,26 @@ fn event_loop( · 2701 unmodified lines
27022702 Err(err) => app.leave_error(format!("todo: {err:#}")),
27032703 }
27042704 }
2705+ Effect::ToggleChecksPass(id) => {
2706+ fill_repository.retain = false;
2707+ fill_repository.retained = None;
2708+ let result = open_repository(&repository_path, repository_is_bare, false)
2709+ .context("could not open repository to update the tree enrichment")
2710+ .and_then(|repo| enrich::toggle_checks_pass(&repo, id));
2711+ match result {
2712+ Ok(enrichment) => {
2713+ let enabled = enrichment.checks_pass;
2714+ app.clear_enrichments();
2715+ app.set_tree_enrichment(id, enrichment);
2716+ app.leave_success(if enabled {
2717+ "marked tree checks-pass"
2718+ } else {
2719+ "cleared tree checks-pass"
2720+ });
2721+ }
2722+ Err(err) => app.leave_error(format!("checks-pass: {err:#}")),
2723+ }
2724+ }
27052725 Effect::EditNote(id) => {
27062726 fill_repository.retain = false;
27072727 fill_repository.retained = None;
@@ -3119,6 +3139,11 @@ fn draw( · 411 unmodified lines
31193139 .map(|row| row.id)
31203140 .filter(|id| repository_fill_allowed && !app.enrichment_loaded(*id))
31213141 .collect();
3142+ let tree_enrichments_to_load: Vec<_> = app.rows[start..end]
3143+ .iter()
3144+ .map(|row| row.id)
3145+ .filter(|id| repository_fill_allowed && !app.tree_enrichment_loaded(*id))
3146+ .collect();
31223147 let changes_visible = app.changes_visible();
31233148 let selected_id = app.selected.and_then(|index| app.rows.get(index)).map(|row| row.id);
31243149 app.selection_relation = selection_cache
@@ -3193,6 +3218,7 @@ fn draw( · 68 unmodified lines
31933218 }
31943219 if !notes_to_load.is_empty()
31953220 || !enrichments_to_load.is_empty()
3221+ || !tree_enrichments_to_load.is_empty()
31963222 || app.rows[start..end].iter().any(|row| !row.metadata_loaded)
31973223 || message_to_load.is_some()
31983224 || tree_changes_to_load.is_some()
@@ -3236,6 +3262,19 @@ fn draw( · 37 unmodified lines
32363262 }
32373263 }
32383264 }
3265+ if !tree_enrichments_to_load.is_empty() {
3266+ let mut notes = enrich::open_tree(repository)?;
3267+ for id in tree_enrichments_to_load {
3268+ let loaded = enrich::tree_id(repository, id).and_then(|tree_id| enrich::load_tree(&mut notes, tree_id));
3269+ match loaded {
3270+ Ok(enrichment) => app.set_tree_enrichment(id, enrichment),
3271+ Err(err) => {
3272+ tracing::warn!(commit_id = %id, error = %err, "ignored malformed tix tree enrichment");
3273+ app.set_tree_enrichment(id, enrich::TreeEnrichment::default());
3274+ }
3275+ }
3276+ }
3277+ }
32393278 for index in start..end {
32403279 if app.rows[index].metadata_loaded {
32413280 continue;
@@ -5085,6 +5124,7 @@ fn action_with_shortcut_groups( · 1843 unmodified lines
50855124 KeyCode::Char('s') if commit_expanded => Some(Action::Spill),
50865125 KeyCode::Char('d') if commit_expanded => Some(Action::Forget),
50875126 KeyCode::Char('i') if commit_expanded => Some(Action::TogglePin),
5127+ KeyCode::Char('c') if enrich_expanded => Some(Action::ToggleChecksPass),
50885128 KeyCode::Char('t') if enrich_expanded => Some(Action::ToggleTodo),
50895129 KeyCode::Char('o') if enrich_expanded => Some(Action::EditNote),
50905130 KeyCode::Char('@') => Some(Action::TimeTravel),
@@ -6179,6 +6219,17 @@ mod tests { · 1088 unmodified lines
61796219 action(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)),
61806220 Some(Action::ToggleEnrich)
61816221 );
6222+ assert_eq!(
6223+ action_with_shortcut_groups(
6224+ KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
6225+ false,
6226+ false,
6227+ false,
6228+ true,
6229+ false
6230+ ),
6231+ Some(Action::ToggleChecksPass)
6232+ );
61826233 assert_eq!(
61836234 action_with_shortcut_groups(
61846235 KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE),
gix-tix/src/ui.rs+19−6
@@ -479,7 +479,14 @@ pub(crate) fn draw_with_worktree( · 478 unmodified lines
479479 let lanes = app.render_lanes(start..render_end);
480480 let enrichment_gutter = visible_rows
481481 .iter()
482− .map(|row| Line::raw(crate::enrich::marker(app.todo(row.id), app.note(row.id).is_some())).width())
482+ .map(|row| {
483+ Line::raw(crate::enrich::marker(
484+ app.todo(row.id),
485+ app.note(row.id).is_some(),
486+ app.checks_pass(row.id),
487+ ))
488+ .width()
489+ })
483490 .max()
484491 .unwrap_or_default() as u16;
485492 let change_id_gutter = if visible_rows.iter().any(|row| app.has_duplicate_change_id(row.id)) {
@@ -692,6 +699,7 @@ pub(crate) fn draw_with_worktree( · 206 unmodified lines
692699 let enrichment_marker = crate::enrich::marker(
693700 app.todo(visible_rows[index].id),
694701 app.note(visible_rows[index].id).is_some(),
702+ app.checks_pass(visible_rows[index].id),
695703 );
696704 if !enrichment_marker.is_empty() {
697705 frame.render_widget(
@@ -1154,6 +1162,8 @@ pub(crate) fn draw_with_worktree( · 456 unmodified lines
11541162 ordered.extend(shortcut("note", 'o', app.note(row.id).is_some()));
11551163 ordered.push(Span::raw(" · "));
11561164 }
1165+ ordered.extend(shortcut("checks-pass", 'c', app.checks_pass(row.id)));
1166+ ordered.push(Span::raw(" · "));
11571167 ordered.extend(shortcut("git note", 'g', !app.notes(row.id).is_empty()));
11581168 } else {
11591169 ordered.push(Span::raw("no actions"));
@@ -2656,14 +2666,16 @@ mod tests { · 1496 unmodified lines
26562666 note: Some("follow-up *title*\n\nwhy it matters\n".into()),
26572667 },
26582668 );
2669+ app.set_tree_enrichment(id, crate::enrich::TreeEnrichment { checks_pass: true });
26592670 let mut terminal = Terminal::new(TestBackend::new(80, 2))?;
26602671 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
26612672
26622673 let row = terminal.backend().buffer();
26632674 assert_eq!(row[(0, 0)].symbol(), "🚧", "todo leads the row");
26642675 assert_eq!(row[(2, 0)].symbol(), "📝", "note directly follows todo");
2665− assert_eq!(row[(4, 0)].symbol(), ">", "selection directly follows enrichments");
2666− assert_eq!(row[(6, 0)].symbol(), "●", "the graph remains separate");
2676+ assert_eq!(row[(4, 0)].symbol(), "✔️", "tree status follows commit enrichments");
2677+ assert_eq!(row[(6, 0)].symbol(), ">", "selection directly follows enrichments");
2678+ assert_eq!(row[(8, 0)].symbol(), "●", "the graph remains separate");
26672679 assert!(
26682680 rendered_line(&terminal, 0).contains("follow-up title"),
26692681 "the selected todo displays its note title"
@@ -2680,7 +2692,7 @@ mod tests { · 10 unmodified lines
26802692 app.enrich_expanded = true;
26812693 terminal.draw(|frame| draw(frame, &mut app, &Decorations::new()))?;
26822694 assert!(
2683− rendered_line(&terminal, 1).contains("enrich (todo · note · git note)"),
2695+ rendered_line(&terminal, 1).contains("enrich (todo · note · checks-pass · git note)"),
26842696 "the enrich group advertises all note actions"
26852697 );
26862698 app.enrich_expanded = false;
@@ -2705,8 +2717,9 @@ mod tests { · 18 unmodified lines
27052717 "a note replaces the selected title independently of todo"
27062718 );
27072719 assert_eq!(terminal.backend().buffer()[(0, 0)].symbol(), "📝");
2708− assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), ">");
2709− assert_eq!(terminal.backend().buffer()[(4, 0)].symbol(), "●");
2720+ assert_eq!(terminal.backend().buffer()[(2, 0)].symbol(), "✔️");
2721+ assert_eq!(terminal.backend().buffer()[(4, 0)].symbol(), ">");
2722+ assert_eq!(terminal.backend().buffer()[(6, 0)].symbol(), "●");
27102723 Ok(())
27112724 }
27122725

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