git-branchless 0.3.4

Branchless workflow for Git
Documentation
use std::convert::TryInto;
use std::ffi::OsString;
use std::path::PathBuf;

use branchless::commands::wrap;
use branchless::core::formatting::Glyphs;
use branchless::git::{GitRunInfo, NonZeroOid};
use branchless::tui::Effects;
use structopt::StructOpt;

#[derive(StructOpt)]
enum WrappedCommand {
    #[structopt(external_subcommand)]
    WrappedCommand(Vec<String>),
}

/// Branchless workflow for Git.
///
/// See the documentation at https://github.com/arxanas/git-branchless/wiki.
#[derive(StructOpt)]
#[structopt(version = env!("CARGO_PKG_VERSION"), author = "Waleed Khan <me@waleedkhan.name>")]
enum Opts {
    /// Initialize the branchless workflow for this repository.
    Init {
        /// Uninstall the branchless workflow instead of initializing it.
        #[structopt(long = "--uninstall")]
        uninstall: bool,
    },

    /// Display a nice graph of the commits you've recently worked on.
    Smartlog,

    /// Hide the provided commits from the smartlog.
    Hide {
        /// Zero or more commits to hide.
        ///
        /// Can either be hashes, like `abc123`, or ref-specs, like `HEAD^`.
        commits: Vec<String>,

        /// Also recursively hide all children commits of the provided commits.
        #[structopt(short = "-r", long = "--recursive")]
        recursive: bool,
    },

    /// Unhide previously-hidden commits from the smartlog.
    Unhide {
        /// Zero or more commits to unhide.
        ///
        /// Can either be hashes, like `abc123`, or ref-specs, like `HEAD^`.
        commits: Vec<String>,

        /// Also recursively unhide all children commits of the provided commits.
        #[structopt(short = "-r", long = "--recursive")]
        recursive: bool,
    },

    /// Move to an earlier commit in the current stack.
    Prev {
        /// The number of commits backward to go.
        num_commits: Option<isize>,
    },

    /// Move to a later commit in the current stack.
    Next {
        /// The number of commits forward to go.
        ///
        /// If not provided, defaults to 1.
        num_commits: Option<isize>,

        /// When encountering multiple next commits, choose the oldest.
        #[structopt(short = "-o", long = "--oldest")]
        oldest: bool,

        /// When encountering multiple next commits, choose the newest.
        #[structopt(short = "-n", long = "--newest", conflicts_with("oldest"))]
        newest: bool,
    },

    /// Move a subtree of commits from one location to another.
    ///
    /// By default, `git move` tries to move the entire current stack if you
    /// don't pass a `--source` or `--base` option (equivalent to writing
    /// `--base HEAD`).
    ///
    /// By default, `git move` attempts to rebase all commits in-memory. If you
    /// want to force an on-disk rebase, pass the `--on-disk` flag. Note that
    /// `post-commit` hooks are not called during in-memory rebases.
    Move {
        /// The source commit to move. This commit, and all of its descendants,
        /// will be moved.
        #[structopt(short = "-s", long = "--source")]
        source: Option<String>,

        /// A commit inside a subtree to move. The entire subtree, starting from
        /// the main branch, will be moved, not just the commits descending from
        /// this commit.
        #[structopt(short = "-b", long = "--base", conflicts_with = "source")]
        base: Option<String>,

        /// The destination commit to move all source commits onto. If not
        /// provided, defaults to the current commit.
        #[structopt(short = "-d", long = "--dest")]
        dest: Option<String>,

        /// Only attempt to perform an in-memory rebase. If it fails, do not
        /// attempt an on-disk rebase.
        #[structopt(long = "--in-memory", conflicts_with = "force_on_disk")]
        force_in_memory: bool,

        /// Skip attempting to use an in-memory rebase, and try an
        /// on-disk rebase directly.
        #[structopt(long = "--on-disk")]
        force_on_disk: bool,

        /// Debugging option. Print the constraints used to create the rebase
        /// plan before executing it.
        #[structopt(long = "--debug-dump-rebase-constraints")]
        dump_rebase_constraints: bool,

        /// Debugging option. Print the rebase plan that will be executed before
        /// executing it.
        #[structopt(long = "--debug-dump-rebase-plan")]
        dump_rebase_plan: bool,
    },

    /// Fix up commits abandoned by a previous rewrite operation.
    Restack {
        /// The IDs of the abandoned commits whose descendants should be
        /// restacked. If not provided, all abandoned commits are restacked.
        commits: Vec<String>,

        /// Debugging option. Print the constraints used to create the rebase
        /// plan before executing it.
        #[structopt(long = "--debug-dump-rebase-constraints")]
        dump_rebase_constraints: bool,

        /// Debugging option. Print the rebase plan that will be executed before
        /// executing it.
        #[structopt(long = "--debug-dump-rebase-plan")]
        dump_rebase_plan: bool,
    },

    /// Browse or return to a previous state of the repository.
    Undo,

    /// Run internal garbage collection.
    Gc,

    /// Wrap a Git command inside a branchless transaction.
    Wrap {
        #[structopt(long = "--git-executable")]
        git_executable: Option<PathBuf>,

        #[structopt(subcommand)]
        command: WrappedCommand,
    },

    /// Internal use.
    HookPreAutoGc,

    /// Internal use.
    HookPostRewrite { rewrite_type: String },

    /// Internal use.
    HookRegisterExtraPostRewriteHook,

    /// Internal use.
    HookDetectEmptyCommit { old_commit_oid: NonZeroOid },

    /// Internal use.
    HookSkipUpstreamAppliedCommit { commit_oid: NonZeroOid },

    /// Internal use.
    HookPostCheckout {
        previous_commit: String,
        current_commit: String,
        is_branch_checkout: isize,
    },

    /// Internal use.
    HookPostCommit,

    /// Internal use.
    HookReferenceTransaction { transaction_state: String },
}

fn main() -> eyre::Result<()> {
    color_eyre::install()?;
    install_tracing();

    let opts = Opts::from_args();
    let path_to_git = std::env::var_os("PATH_TO_GIT").unwrap_or_else(|| OsString::from("git"));
    let path_to_git = PathBuf::from(&path_to_git);
    let git_run_info = GitRunInfo {
        path_to_git,
        working_directory: std::env::current_dir()?,
        env: std::env::vars_os().collect(),
    };
    let effects = Effects::new(Glyphs::detect());

    let exit_code = match opts {
        Opts::Init { uninstall: false } => {
            branchless::commands::init::init(&effects, &git_run_info)?;
            0
        }

        Opts::Init { uninstall: true } => {
            branchless::commands::init::uninstall(&effects)?;
            0
        }

        Opts::Smartlog => {
            branchless::commands::smartlog::smartlog(&effects)?;
            0
        }

        Opts::Hide { commits, recursive } => {
            branchless::commands::hide::hide(&effects, commits, recursive)?
        }

        Opts::Unhide { commits, recursive } => {
            branchless::commands::hide::unhide(&effects, commits, recursive)?
        }

        Opts::Prev { num_commits } => {
            branchless::commands::navigation::prev(&effects, &git_run_info, num_commits)?
        }

        Opts::Next {
            num_commits,
            oldest,
            newest,
        } => {
            let towards = match (oldest, newest) {
                (false, false) => None,
                (true, false) => Some(branchless::commands::navigation::Towards::Oldest),
                (false, true) => Some(branchless::commands::navigation::Towards::Newest),
                (true, true) => eyre::bail!("Both --oldest and --newest were set"),
            };
            branchless::commands::navigation::next(&effects, &git_run_info, num_commits, towards)?
        }

        Opts::Move {
            source,
            dest,
            base,
            force_in_memory,
            force_on_disk,
            dump_rebase_constraints,
            dump_rebase_plan,
        } => branchless::commands::r#move::r#move(
            &effects,
            &git_run_info,
            source,
            dest,
            base,
            force_in_memory,
            force_on_disk,
            dump_rebase_constraints,
            dump_rebase_plan,
        )?,

        Opts::Restack {
            commits,
            dump_rebase_constraints,
            dump_rebase_plan,
        } => branchless::commands::restack::restack(
            &effects,
            &git_run_info,
            commits,
            dump_rebase_constraints,
            dump_rebase_plan,
        )?,

        Opts::Undo => branchless::commands::undo::undo(&effects, &git_run_info)?,

        Opts::Gc | Opts::HookPreAutoGc => {
            branchless::commands::gc::gc(&effects)?;
            0
        }

        Opts::Wrap {
            git_executable: explicit_git_executable,
            command: WrappedCommand::WrappedCommand(args),
        } => {
            let git_run_info = match explicit_git_executable {
                Some(path_to_git) => GitRunInfo {
                    path_to_git,
                    ..git_run_info
                },
                None => git_run_info,
            };
            let exit_code = wrap::wrap(&git_run_info, args.as_slice())?;
            exit_code
        }

        Opts::HookPostRewrite { rewrite_type } => {
            branchless::commands::hooks::hook_post_rewrite(&effects, &git_run_info, &rewrite_type)?;
            0
        }

        Opts::HookRegisterExtraPostRewriteHook => {
            branchless::commands::hooks::hook_register_extra_post_rewrite_hook()?;
            0
        }

        Opts::HookDetectEmptyCommit { old_commit_oid } => {
            branchless::commands::hooks::hook_drop_commit_if_empty(&effects, old_commit_oid)?;
            0
        }

        Opts::HookSkipUpstreamAppliedCommit { commit_oid } => {
            branchless::commands::hooks::hook_skip_upstream_applied_commit(&effects, commit_oid)?;
            0
        }

        Opts::HookPostCheckout {
            previous_commit,
            current_commit,
            is_branch_checkout,
        } => {
            branchless::commands::hooks::hook_post_checkout(
                &effects,
                &previous_commit,
                &current_commit,
                is_branch_checkout,
            )?;
            0
        }

        Opts::HookPostCommit => {
            branchless::commands::hooks::hook_post_commit(&effects)?;
            0
        }

        Opts::HookReferenceTransaction { transaction_state } => {
            branchless::commands::hooks::hook_reference_transaction(&effects, &transaction_state)?;
            0
        }
    };

    let exit_code: i32 = exit_code.try_into()?;
    std::process::exit(exit_code)
}

fn install_tracing() {
    // From https://github.com/yaahc/color-eyre/blob/07b9f0351544e2b07fcd173dc1fc602a7fc8bb6b/examples/usage.rs
    // Licensed under MIT.
    use tracing_error::ErrorLayer;
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{fmt, EnvFilter};

    let filter_layer = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new("warn"))
        .unwrap();
    let fmt_layer = fmt::layer()
        .with_span_events(fmt::format::FmtSpan::CLOSE)
        .with_target(false);

    tracing_subscriber::registry()
        .with(filter_layer)
        .with(fmt_layer)
        .with(ErrorLayer::default())
        .init();
}