use std::{ collections::HashSet, ffi::{OsStr, OsString}, io::Write, path::PathBuf, sync::atomic::AtomicBool,}; use anyhow::{Context, Result};use clap::Parser;use ratatui::text::Line; mod enrich;mod new;mod rebase;mod reword;mod travel; /// Arguments and commands shared by the standalone `tix` binary and `gix tix`.#[derive(Debug, clap::Args)]#[command(args_conflicts_with_subcommands = true)]pub struct Platform { /// Draw on the normal screen so panic output remains visible. #[arg(long)] no_alt_screen: bool, /// Exit after the final frame, optionally replaying read-only INPUTS first. #[arg( long, value_name = "INPUTS", num_args = 0..=1, default_missing_value = "", require_equals = true )] quit_on_finish: Option<String>, /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, #[command(subcommand)] command: Option<Command>, /// Revisions whose reachable commits should be shown, or HEAD if omitted. revisions: Vec<OsString>,} #[derive(Debug, clap::Subcommand)]enum Command { /// Print the ref-tree of non-hidden references without opening the terminal UI. RefTree(RefTree), /// Print the complete history view without opening the terminal UI. #[command(visible_alias = "status")] Show(Show), /// Perform repository maintenance. #[command(subcommand)] Admin(Admin), /// Manage commit and tree enrichments. #[command(subcommand)] Enrich(enrich::Command), /// Add staged changes, or worktree changes when nothing is staged, to HEAD. Amend(Amend), /// Move changes introduced by HEAD into the worktree. Spill(Spill), /// Split HEAD by amending worktree changes into it and committing staged index changes on top. Split(Split), /// Save index and worktree changes in a gix stash associated with the HEAD commit. Stash, /// Pin one or more commits as persistent history tips. Pin(Pin), /// Copy the change introduced by one commit above another commit. CopyInsert(CopyInsert), /// Travel to a commit while preserving reachable history through tix pins. Travel(travel::Args), /// Edit a commit and lazily rebase every descendant retained by a tix pin. Reword(reword::Args), /// Create a new commit at HEAD. New(new::Args), /// Generate or apply a self-contained history-rebase todo. #[command(subcommand)] Rebase(rebase::Command),} #[derive(Debug, clap::Subcommand)]enum Admin { /// Clear this worktree's undo and redo history. ClearUndo,} #[derive(Debug, clap::Args)]struct RefTree { /// Omit tags as labels, traversal tips, and topology anchors. #[arg(long)] no_tags: bool, /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, /// Do not infer hidden local branches from remote HEADs. #[arg(long)] no_auto_hide: bool, /// Use the ref-tree view's Unicode line and node glyphs instead of ASCII. #[arg(long)] unicode: bool, /// Revisions to traverse instead of all normal references. #[arg(value_name = "REVSPEC")] revisions: Vec<OsString>,} #[derive(Debug, clap::Args)]struct Show { /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, /// Do not infer hidden local branches from remote HEADs. #[arg(long)] no_auto_hide: bool, /// Visible traversal tips, or HEAD if omitted. #[arg(value_name = "TIP")] revisions: Vec<OsString>,} #[derive(Debug, clap::Args)]struct Amend { /// Amend only staged index changes, without falling back to worktree changes. #[arg(long)] index: bool,} #[derive(Debug, clap::Args)]struct Spill { /// Paths to spill, or every path if omitted. #[arg(value_name = "PATH")] paths: Vec<OsString>,} #[derive(Debug, clap::Args)]struct Split { /// Mark the new upper commit as TODO. #[arg(long)] todo: bool,} #[derive(Debug, clap::Args)]struct Pin { /// Revisions resolving to commits to pin. #[arg(required = true, value_name = "REVSPEC")] revisions: Vec<OsString>,} #[derive(Debug, clap::Args)]#[command( after_long_help = "Conflicts change nothing by default. To materialize one and write a continuation todo:\n tix copy-insert --materialize-conflicts=todo.continue.md C I\nResolve the index, then run:\n tix rebase apply todo.continue.md\nUse --materialize-conflicts=- to write a continuation to non-terminal stdout.")]struct CopyInsert { /// On conflict, materialize it and write a continuation todo to FILE, or stdout if omitted or '-'. #[arg( long, value_name = "CONTINUE", num_args = 0..=1, default_missing_value = "-", require_equals = true )] materialize_conflicts: Option<PathBuf>, /// Revision resolving to the commit to copy. #[arg(value_name = "SOURCE")] source: OsString, /// Revision resolving to the commit above which to insert the copy. #[arg(value_name = "TARGET")] target: OsString,} #[derive(Debug, clap::Parser)]#[command( name = "tix", about = "Browse or edit commit history", after_long_help = "Commands which open an editor use Git's normal editor selection. Set GIT_EDITOR=<command> to override it.")]struct Cli { #[command(flatten)] platform: Platform,} /// Parse the standalone `tix` command line.pub fn parse() -> Platform { Cli::parse_from(gix::env::args_os()).platform} impl Platform { /// Run this command against `repository`. pub fn run(self, repository: gix::ThreadSafeRepository) -> Result<()> { let Platform { no_alt_screen, quit_on_finish, hide, command, revisions, } = self; let Some(command) = command else { return crate::run( repository, revisions, crate::Options { no_alt_screen, quit_on_finish, hide, }, ); }; let repository = repository.to_thread_local(); let command = match command { Command::RefTree(args) => return print_ref_tree(&repository, args), Command::Show(args) => return show(&repository, args), command => command, }; let _log_guard = crate::logging::init(); match command { Command::RefTree(_) | Command::Show(_) => unreachable!("display commands return before logging"), Command::Amend(args) => { let graph = crate::edit::loaded_view_graph(&repository)?; let output_repository = repository.clone(); let amended = if args.index { crate::edit::head::amend_index_reporting(repository, &graph)? } else { crate::edit::head::perform_reporting(repository, &graph, crate::edit::head::Kind::Amend)? }; match amended { Some(outcome) => { let selected = outcome.selected.context("amending did not produce a selection")?; println!("{}", crate::change_id::display(&output_repository, selected, 7)?); print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?; record_undo(&output_repository, "amend", Ok(outcome.ref_changes)); } None => println!("nothing to amend"), } } Command::Spill(args) => { let selected_paths = resolve_spill_paths(&repository, &args.paths)?; let graph = crate::edit::loaded_view_graph(&repository)?; edit_head( repository, &graph, crate::edit::head::Kind::Spill, "spill", selected_paths.as_deref(), )?; } Command::Split(args) => { let graph = crate::edit::loaded_view_graph(&repository)?; split(repository, &graph, args)?; } Command::Stash => { let id = repository .head_id() .context("stashing changes requires a born HEAD")? .detach(); let notice = crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)?; println!("{}", notice_with_change_id(&repository, ¬ice, id)?); } Command::Pin(args) => pin(&repository, args)?, Command::CopyInsert(args) => return copy_insert(repository, args), Command::Admin(Admin::ClearUndo) => crate::edit::undo::clear(&repository)?, Command::Travel(args) => return travel::run(repository, args), Command::Reword(args) => return reword::run(repository, args), Command::New(args) => return new::run(repository, args), Command::Enrich(command) => return enrich::run(repository, command), Command::Rebase(command) => return rebase::run(repository, command), } Ok(()) }} fn print_ref_tree(repository: &gix::Repository, args: RefTree) -> Result<()> { let rendered = render_ref_tree(repository, args)?; std::io::Write::write_all(&mut std::io::stdout().lock(), rendered.as_bytes()) .context("could not write ref-tree")?; Ok(())} fn render_ref_tree(repository: &gix::Repository, args: RefTree) -> Result<String> { let (hide, unavailable) = crate::history::available_hidden_revisions(repository, &args.hide, !args.no_auto_hide)?; for (revision, err) in unavailable { eprintln!( "warning: ignoring unavailable hidden revision {}: {err}", revision.to_string_lossy() ); } let revisions = if args.revisions.is_empty() { crate::history::ref_tree_revisions(repository, !args.no_tags)? } else { args.revisions }; crate::ref_tree::render_full(repository, &revisions, &hide, !args.no_tags, args.unicode)} fn show(repository: &gix::Repository, args: Show) -> Result<()> { let (hide, unavailable) = crate::history::available_hidden_revisions(repository, &args.hide, !args.no_auto_hide)?; if hide.is_empty() { anyhow::bail!("show requires at least one -x/--hide revision when no remote HEAD maps to a local branch"); } for (revision, err) in unavailable { eprintln!( "warning: ignoring unavailable hidden revision {}: {err}", revision.to_string_lossy() ); } write_history(repository, &args.revisions, &hide, std::io::stdout().lock())} fn write_history( repository: &gix::Repository, revisions: &[OsString], hide: &[OsString], mut out: impl Write,) -> Result<()> { let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( crate::history::Authors::default(), )); let refs = crate::history::snapshot(repository, revisions, hide, false)?; let mut app = crate::app::App::new(usize::MAX); app.id_mode = crate::app::IdMode::Commit; let mut decorations = crate::history::Decorations::default(); let mut history_graph = None; crate::history::load( repository, revisions, hide, false, &authors, &AtomicBool::new(false), |event| { match event { crate::history::Event::Decorations(value) => decorations = value, crate::history::Event::Commits(rows) => app.extend_commits(rows), crate::history::Event::HiddenCommits(rows) => app.extend_hidden_commits(rows), crate::history::Event::Complete(graph) => history_graph = Some(graph), crate::history::Event::VisibleComplete | crate::history::Event::Cancelled => {} } true }, )?; let graph = history_graph.context("history traversal did not complete")?; let rows = app .start_lane_computation() .context("history rows were unavailable for lane computation")?; let (rows, lanes, elapsed) = crate::app::compute_lanes(rows); app.finish_lane_computation(rows, lanes, elapsed); crate::update_hidden_branch_updates(&mut app, Some(&graph), &refs); for index in 0..app.rows.len() { if app.rows[index].metadata_loaded { continue; } let id = app.rows[index].id; let (metadata, attributions) = crate::history::load_metadata(repository, id, &authors).context("could not load displayed commit")?; app.set_metadata(index, metadata, attributions); } let mut note_ids = HashSet::new(); let mut notes = repository.notes().context("could not open Git notes")?; for row in &app.rows { if !notes .get(row.id) .context("could not load displayed commit notes")? .is_empty() { note_ids.insert(row.id); } } let mut todo_ids = HashSet::new(); let mut enrichment_note_ids = HashSet::new(); let mut enrichments = crate::enrich::open(repository)?; for row in &app.rows { let loaded = crate::change_id::for_commit(repository, row.id) .and_then(|change_id| crate::enrich::load(&mut enrichments, change_id)); match loaded { Ok(enrichment) => { if enrichment.todo { todo_ids.insert(row.id); } if enrichment.note.is_some() { enrichment_note_ids.insert(row.id); } } Err(err) => tracing::warn!(commit_id = %row.id, error = %err, "ignored malformed tix enrichment"), } } let mut checks_pass_ids = HashSet::new(); let mut tree_enrichments = crate::enrich::open_tree(repository)?; for row in &app.rows { let loaded = crate::enrich::tree_id(repository, row.id) .and_then(|tree_id| crate::enrich::load_tree(&mut tree_enrichments, tree_id)); match loaded { Ok(enrichment) if enrichment.checks_pass => { checks_pass_ids.insert(row.id); } Ok(_) => {} Err(err) => tracing::warn!(commit_id = %row.id, error = %err, "ignored malformed tix tree enrichment"), } } let change_ids = crate::change_id::abbreviations(repository, app.rows.iter().map(|row| row.id), 7)?; let mailmap = repository.open_mailmap(); let lanes = app.render_lanes(0..app.rows.len()); let enrichment_gutter = app .rows .iter() .map(|row| { Line::raw(crate::enrich::marker( todo_ids.contains(&row.id), enrichment_note_ids.contains(&row.id), checks_pass_ids.contains(&row.id), )) .width() }) .max() .unwrap_or_default(); let ambiguity_gutter = (!change_ids.ambiguous.is_empty()).then(|| Line::raw("💥").width()); let render_line = |index: usize, row: &crate::app::SharedCommitRow| { let metadata = crate::ui::plain_history_metadata( &app, row, &decorations, &mailmap, note_ids.contains(&row.id), change_ids.values.get(&row.id).copied(), ); let enrichment_marker = crate::enrich::marker( todo_ids.contains(&row.id), enrichment_note_ids.contains(&row.id), checks_pass_ids.contains(&row.id), ); let ambiguity_marker = if change_ids.ambiguous.contains(&row.id) { "💥" } else { "" }; let mut gutter = String::new(); if enrichment_gutter != 0 { gutter.push_str(enrichment_marker); gutter.push_str(&" ".repeat(enrichment_gutter.saturating_sub(Line::raw(enrichment_marker).width()))); } if let Some(width) = ambiguity_gutter { gutter.push_str(ambiguity_marker); gutter.push_str(&" ".repeat(width.saturating_sub(Line::raw(ambiguity_marker).width()))); } let behind = app .hidden_branch_behind(row.id) .map(|behind| format!(" ⇣{behind}")) .unwrap_or_default(); let line = format!("{gutter}{}{metadata}{behind}", lanes.lane(index)); let base = (app.visual_count(index) == Some(0)) .then(|| format!("base {enrichment_marker}{ambiguity_marker}{metadata}{behind}")); (line, base) }; let width = app .rows .iter() .enumerate() .map(|(index, row)| { let (line, base) = render_line(index, row); base.as_ref() .map_or_else(|| Line::raw(&line).width(), |base| Line::raw(base).width() + 10) }) .max() .unwrap_or_default(); for (index, row) in app.rows.iter().enumerate() { let (line, base) = render_line(index, row); if let Some(base) = base { let rails = width.saturating_sub(Line::raw(&base).width() + 2).max(8); let left = rails / 2; writeln!(out, "{} {base} {}", "─".repeat(left), "─".repeat(rails - left)) .context("could not write history base")?; } else { writeln!(out, "{line}").context("could not write history row")?; } } Ok(())} fn resolve_commit( repository: &gix::Repository, revision: &OsStr, description: &str,) -> Result<(gix::ObjectId, Option<crate::history::HistoryGraph>)> { let revision = gix::path::os_str_into_bstr(revision) .with_context(|| format!("revision {} is not valid UTF-8", revision.to_string_lossy()))?; match crate::history::resolve_revision(repository, revision) { Ok((id, _reference)) => Ok((id, None)), Err(revision_error) => { let graph = crate::edit::loaded_view_graph(repository)?; let resolved = std::str::from_utf8(revision) .ok() .map(|prefix| crate::change_id::resolve_prefix(repository, prefix, graph.stored_commit_ids())) .transpose()? .flatten(); match resolved { Some(id) => Ok((id, Some(graph))), None => Err(revision_error).with_context(|| format!("could not resolve {description} {revision:?}")), }Lines 1–500 of 2021. Load more
use std::{ collections::HashSet, ffi::{OsStr, OsString}, io::Write, path::PathBuf, sync::atomic::AtomicBool, }; use anyhow::{Context, Result}; use clap::Parser; use ratatui::text::Line; mod enrich; mod new; mod rebase; mod reword; mod travel; /// Arguments and commands shared by the standalone `tix` binary and `gix tix`. #[derive(Debug, clap::Args)] #[command(args_conflicts_with_subcommands = true)] pub struct Platform { /// Draw on the normal screen so panic output remains visible. #[arg(long)] no_alt_screen: bool, /// Exit after the final frame, optionally replaying read-only INPUTS first. #[arg( long, value_name = "INPUTS", num_args = 0..=1, default_missing_value = "", require_equals = true )] quit_on_finish: Option<String>, /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, #[command(subcommand)] command: Option<Command>, /// Revisions whose reachable commits should be shown, or HEAD if omitted. revisions: Vec<OsString>, } #[derive(Debug, clap::Subcommand)] enum Command { /// Print the ref-tree of non-hidden references without opening the terminal UI. RefTree(RefTree), /// Print the complete history view without opening the terminal UI. #[command(visible_alias = "status")] Show(Show), /// Perform repository maintenance. #[command(subcommand)] Admin(Admin), /// Manage commit and tree enrichments. #[command(subcommand)] Enrich(enrich::Command), /// Add staged changes, or worktree changes when nothing is staged, to HEAD. Amend(Amend), /// Move changes introduced by HEAD into the worktree. Spill(Spill), /// Split HEAD by amending worktree changes into it and committing staged index changes on top. Split(Split), /// Save index and worktree changes in a gix stash associated with the HEAD commit. Stash, /// Pin one or more commits as persistent history tips. Pin(Pin), /// Copy the change introduced by one commit above another commit. CopyInsert(CopyInsert), /// Travel to a commit while preserving reachable history through tix pins. Travel(travel::Args), /// Edit a commit and lazily rebase every descendant retained by a tix pin. Reword(reword::Args), /// Create a new commit at HEAD. New(new::Args), /// Generate or apply a self-contained history-rebase todo. #[command(subcommand)] Rebase(rebase::Command), } #[derive(Debug, clap::Subcommand)] enum Admin { /// Clear this worktree's undo and redo history. ClearUndo, } #[derive(Debug, clap::Args)] struct RefTree { /// Omit tags as labels, traversal tips, and topology anchors. #[arg(long)] no_tags: bool, /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, /// Do not infer hidden local branches from remote HEADs. #[arg(long)] no_auto_hide: bool, /// Use the ref-tree view's Unicode line and node glyphs instead of ASCII. #[arg(long)] unicode: bool, /// Revisions to traverse instead of all normal references. #[arg(value_name = "REVSPEC")] revisions: Vec<OsString>, } #[derive(Debug, clap::Args)] struct Show { /// Hide this revision and every commit reachable from it. #[arg(short = 'x', long, value_name = "REVSPEC")] hide: Vec<OsString>, /// Do not infer hidden local branches from remote HEADs. #[arg(long)] no_auto_hide: bool, /// Visible traversal tips, or HEAD if omitted. #[arg(value_name = "TIP")] revisions: Vec<OsString>, } #[derive(Debug, clap::Args)] struct Amend { /// Amend only staged index changes, without falling back to worktree changes. #[arg(long)] index: bool, } #[derive(Debug, clap::Args)] struct Spill { /// Paths to spill, or every path if omitted. #[arg(value_name = "PATH")] paths: Vec<OsString>, } #[derive(Debug, clap::Args)] struct Split { /// Mark the new upper commit as TODO. #[arg(long)] todo: bool, } #[derive(Debug, clap::Args)] struct Pin { /// Revisions resolving to commits to pin. #[arg(required = true, value_name = "REVSPEC")] revisions: Vec<OsString>, } #[derive(Debug, clap::Args)] #[command( after_long_help = "Conflicts change nothing by default. To materialize one and write a continuation todo:\n tix copy-insert --materialize-conflicts=todo.continue.md C I\nResolve the index, then run:\n tix rebase apply todo.continue.md\nUse --materialize-conflicts=- to write a continuation to non-terminal stdout." )] struct CopyInsert { /// On conflict, materialize it and write a continuation todo to FILE, or stdout if omitted or '-'. #[arg( long, value_name = "CONTINUE", num_args = 0..=1, default_missing_value = "-", require_equals = true )] materialize_conflicts: Option<PathBuf>, /// Revision resolving to the commit to copy. #[arg(value_name = "SOURCE")] source: OsString, /// Revision resolving to the commit above which to insert the copy. #[arg(value_name = "TARGET")] target: OsString, } #[derive(Debug, clap::Parser)] #[command( name = "tix", about = "Browse or edit commit history", after_long_help = "Commands which open an editor use Git's normal editor selection. Set GIT_EDITOR=<command> to override it." )] struct Cli { #[command(flatten)] platform: Platform, } /// Parse the standalone `tix` command line. pub fn parse() -> Platform { Cli::parse_from(gix::env::args_os()).platform } impl Platform { /// Run this command against `repository`. pub fn run(self, repository: gix::ThreadSafeRepository) -> Result<()> { let Platform { no_alt_screen, quit_on_finish, hide, command, revisions, } = self; let Some(command) = command else { return crate::run( repository, revisions, crate::Options { no_alt_screen, quit_on_finish, hide, }, ); }; let repository = repository.to_thread_local(); let command = match command { Command::RefTree(args) => return print_ref_tree(&repository, args), Command::Show(args) => return show(&repository, args), command => command, }; let _log_guard = crate::logging::init(); match command { Command::RefTree(_) | Command::Show(_) => unreachable!("display commands return before logging"), Command::Amend(args) => { let graph = crate::edit::loaded_view_graph(&repository)?; let output_repository = repository.clone(); let amended = if args.index { crate::edit::head::amend_index_reporting(repository, &graph)? } else { crate::edit::head::perform_reporting(repository, &graph, crate::edit::head::Kind::Amend)? }; match amended { Some(outcome) => { let selected = outcome.selected.context("amending did not produce a selection")?; println!("{}", crate::change_id::display(&output_repository, selected, 7)?); print_ref_rewrites(&output_repository, &outcome.ref_rewrites)?; record_undo(&output_repository, "amend", Ok(outcome.ref_changes)); } None => println!("nothing to amend"), } } Command::Spill(args) => { let selected_paths = resolve_spill_paths(&repository, &args.paths)?; let graph = crate::edit::loaded_view_graph(&repository)?; edit_head( repository, &graph, crate::edit::head::Kind::Spill, "spill", selected_paths.as_deref(), )?; } Command::Split(args) => { let graph = crate::edit::loaded_view_graph(&repository)?; split(repository, &graph, args)?; } Command::Stash => { let id = repository .head_id() .context("stashing changes requires a born HEAD")? .detach(); let notice = crate::edit::stash::save_manual(repository.git_dir(), repository.is_bare(), id)?; println!("{}", notice_with_change_id(&repository, ¬ice, id)?); } Command::Pin(args) => pin(&repository, args)?, Command::CopyInsert(args) => return copy_insert(repository, args), Command::Admin(Admin::ClearUndo) => crate::edit::undo::clear(&repository)?, Command::Travel(args) => return travel::run(repository, args), Command::Reword(args) => return reword::run(repository, args), Command::New(args) => return new::run(repository, args), Command::Enrich(command) => return enrich::run(repository, command), Command::Rebase(command) => return rebase::run(repository, command), } Ok(()) } } fn print_ref_tree(repository: &gix::Repository, args: RefTree) -> Result<()> { let rendered = render_ref_tree(repository, args)?; std::io::Write::write_all(&mut std::io::stdout().lock(), rendered.as_bytes()) .context("could not write ref-tree")?; Ok(()) } fn render_ref_tree(repository: &gix::Repository, args: RefTree) -> Result<String> { let (hide, unavailable) = crate::history::available_hidden_revisions(repository, &args.hide, !args.no_auto_hide)?; for (revision, err) in unavailable { eprintln!( "warning: ignoring unavailable hidden revision {}: {err}", revision.to_string_lossy() ); } let revisions = if args.revisions.is_empty() { crate::history::ref_tree_revisions(repository, !args.no_tags)? } else { args.revisions }; crate::ref_tree::render_full(repository, &revisions, &hide, !args.no_tags, args.unicode) } fn show(repository: &gix::Repository, args: Show) -> Result<()> { let (hide, unavailable) = crate::history::available_hidden_revisions(repository, &args.hide, !args.no_auto_hide)?; if hide.is_empty() { anyhow::bail!("show requires at least one -x/--hide revision when no remote HEAD maps to a local branch"); } for (revision, err) in unavailable { eprintln!( "warning: ignoring unavailable hidden revision {}: {err}", revision.to_string_lossy() ); } write_history(repository, &args.revisions, &hide, std::io::stdout().lock()) } fn write_history( repository: &gix::Repository, revisions: &[OsString], hide: &[OsString], mut out: impl Write, ) -> Result<()> { let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( crate::history::Authors::default(), )); let refs = crate::history::snapshot(repository, revisions, hide, false)?; let mut app = crate::app::App::new(usize::MAX); app.id_mode = crate::app::IdMode::Commit; let mut decorations = crate::history::Decorations::default(); let mut history_graph = None; crate::history::load( repository, revisions, hide, false, &authors, &AtomicBool::new(false), |event| { match event { crate::history::Event::Decorations(value) => decorations = value, crate::history::Event::Commits(rows) => app.extend_commits(rows), crate::history::Event::HiddenCommits(rows) => app.extend_hidden_commits(rows), crate::history::Event::Complete(graph) => history_graph = Some(graph), crate::history::Event::VisibleComplete | crate::history::Event::Cancelled => {} } true }, )?; let graph = history_graph.context("history traversal did not complete")?; let rows = app .start_lane_computation() .context("history rows were unavailable for lane computation")?; let (rows, lanes, elapsed) = crate::app::compute_lanes(rows); app.finish_lane_computation(rows, lanes, elapsed); crate::update_hidden_branch_updates(&mut app, Some(&graph), &refs); for index in 0..app.rows.len() { if app.rows[index].metadata_loaded { continue; } let id = app.rows[index].id; let (metadata, attributions) = crate::history::load_metadata(repository, id, &authors).context("could not load displayed commit")?; app.set_metadata(index, metadata, attributions); } let mut note_ids = HashSet::new(); let mut notes = repository.notes().context("could not open Git notes")?; for row in &app.rows { if !notes .get(row.id) .context("could not load displayed commit notes")? .is_empty() { note_ids.insert(row.id); } } let mut todo_ids = HashSet::new(); let mut enrichment_note_ids = HashSet::new(); let mut enrichments = crate::enrich::open(repository)?; for row in &app.rows { let loaded = crate::change_id::for_commit(repository, row.id) .and_then(|change_id| crate::enrich::load(&mut enrichments, change_id)); match loaded { Ok(enrichment) => { if enrichment.todo { todo_ids.insert(row.id); } if enrichment.note.is_some() { enrichment_note_ids.insert(row.id); } } Err(err) => tracing::warn!(commit_id = %row.id, error = %err, "ignored malformed tix enrichment"), } } let mut checks_pass_ids = HashSet::new(); let mut tree_enrichments = crate::enrich::open_tree(repository)?; for row in &app.rows { let loaded = crate::enrich::tree_id(repository, row.id) .and_then(|tree_id| crate::enrich::load_tree(&mut tree_enrichments, tree_id)); match loaded { Ok(enrichment) if enrichment.checks_pass => { checks_pass_ids.insert(row.id); } Ok(_) => {} Err(err) => tracing::warn!(commit_id = %row.id, error = %err, "ignored malformed tix tree enrichment"), } } let change_ids = crate::change_id::abbreviations(repository, app.rows.iter().map(|row| row.id), 7)?; let mailmap = repository.open_mailmap(); let lanes = app.render_lanes(0..app.rows.len()); let enrichment_gutter = app .rows .iter() .map(|row| { Line::raw(crate::enrich::marker( todo_ids.contains(&row.id), enrichment_note_ids.contains(&row.id), checks_pass_ids.contains(&row.id), )) .width() }) .max() .unwrap_or_default(); let ambiguity_gutter = (!change_ids.ambiguous.is_empty()).then(|| Line::raw("💥").width()); let render_line = |index: usize, row: &crate::app::SharedCommitRow| { let metadata = crate::ui::plain_history_metadata( &app, row, &decorations, &mailmap, note_ids.contains(&row.id), change_ids.values.get(&row.id).copied(), ); let enrichment_marker = crate::enrich::marker( todo_ids.contains(&row.id), enrichment_note_ids.contains(&row.id), checks_pass_ids.contains(&row.id), ); let ambiguity_marker = if change_ids.ambiguous.contains(&row.id) { "💥" } else { "" }; let mut gutter = String::new(); if enrichment_gutter != 0 { gutter.push_str(enrichment_marker); gutter.push_str(&" ".repeat(enrichment_gutter.saturating_sub(Line::raw(enrichment_marker).width()))); } if let Some(width) = ambiguity_gutter { gutter.push_str(ambiguity_marker); gutter.push_str(&" ".repeat(width.saturating_sub(Line::raw(ambiguity_marker).width()))); } let behind = app .hidden_branch_behind(row.id) .map(|behind| format!(" ⇣{behind}")) .unwrap_or_default(); let line = format!("{gutter}{}{metadata}{behind}", lanes.lane(index)); let base = (app.visual_count(index) == Some(0)) .then(|| format!("base {enrichment_marker}{ambiguity_marker}{metadata}{behind}")); (line, base) }; let width = app .rows .iter() .enumerate() .map(|(index, row)| { let (line, base) = render_line(index, row); base.as_ref() .map_or_else(|| Line::raw(&line).width(), |base| Line::raw(base).width() + 10) }) .max() .unwrap_or_default(); for (index, row) in app.rows.iter().enumerate() { let (line, base) = render_line(index, row); if let Some(base) = base { let rails = width.saturating_sub(Line::raw(&base).width() + 2).max(8); let left = rails / 2; writeln!(out, "{} {base} {}", "─".repeat(left), "─".repeat(rails - left)) .context("could not write history base")?; } else { writeln!(out, "{line}").context("could not write history row")?; } } Ok(()) } fn resolve_commit( repository: &gix::Repository, revision: &OsStr, description: &str, ) -> Result<(gix::ObjectId, Option<crate::history::HistoryGraph>)> { let revision = gix::path::os_str_into_bstr(revision) .with_context(|| format!("revision {} is not valid UTF-8", revision.to_string_lossy()))?; match crate::history::resolve_revision(repository, revision) { Ok((id, _reference)) => Ok((id, None)), Err(revision_error) => { let graph = crate::edit::loaded_view_graph(repository)?; let resolved = std::str::from_utf8(revision) .ok() .map(|prefix| crate::change_id::resolve_prefix(repository, prefix, graph.stored_commit_ids())) .transpose()? .flatten(); match resolved { Some(id) => Ok((id, Some(graph))), None => Err(revision_error).with_context(|| format!("could not resolve {description} {revision:?}")), }