2021 lines · 77.0 KB · Rust
RawDownload
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, &notice, 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.