From 7ce51cc13ec1f554ec921792c1b33f64f3cf7327 Mon Sep 17 00:00:00 2001 From: Brandon Sprague Date: Thu, 29 Sep 2022 20:57:22 -0700 Subject: [PATCH] Add support for interactively checking out a branch --- flake.nix | 2 + git-branchless-lib/src/core/check_out.rs | 52 ++++++++++-- git-branchless-lib/src/core/config.rs | 10 +++ git-branchless/tests/command/test_move.rs | 29 +++---- .../tests/command/test_navigation.rs | 82 ++++++++++++++++++- git-branchless/tests/command/test_reword.rs | 4 +- git-branchless/tests/command/test_undo.rs | 6 +- git-branchless/tests/test_branchless.rs | 4 +- 8 files changed, 160 insertions(+), 29 deletions(-) diff --git a/flake.nix b/flake.nix index 359b3a646..94eca3457 100644 --- a/flake.nix +++ b/flake.nix @@ -59,6 +59,8 @@ checkFlags = [ "--skip=test_checkout_pty" "--skip=test_next_ambiguous_interactive" + "--skip=test_checkout_auto_switch_interactive" + "--skip=test_checkout_auto_switch_interactive_disabled" ]; } ) diff --git a/git-branchless-lib/src/core/check_out.rs b/git-branchless-lib/src/core/check_out.rs index 53c691c8f..99c204af9 100644 --- a/git-branchless-lib/src/core/check_out.rs +++ b/git-branchless-lib/src/core/check_out.rs @@ -7,8 +7,10 @@ use std::time::{SystemTime, UNIX_EPOCH}; use cursive::theme::BaseColor; use cursive::utils::markup::StyledString; use eyre::Context; +use itertools::Itertools; use tracing::instrument; +use crate::core::config::get_auto_switch_branches; use crate::git::{ update_index, CategorizedReferenceName, GitRunInfo, MaybeZeroOid, NonZeroOid, ReferenceName, Repo, Stage, UpdateIndexCommand, WorkingCopySnapshot, @@ -19,6 +21,7 @@ use super::config::get_undo_create_snapshots; use super::effects::Effects; use super::eventlog::{Event, EventLogDb, EventTransactionId}; use super::formatting::printable_styled_string; +use super::repo_ext::{RepoExt, RepoReferencesSnapshot}; /// An entity to check out. #[derive(Clone, Debug)] @@ -54,6 +57,40 @@ impl Default for CheckOutCommitOptions { } } +fn maybe_get_branch_name( + current_target: Option, + oid: Option, + repo: &Repo, +) -> eyre::Result> { + let RepoReferencesSnapshot { + head_oid, + branch_oid_to_names, + .. + } = repo.get_references_snapshot()?; + if (head_oid.is_some() && head_oid == oid) || current_target == head_oid.map(|o| o.to_string()) + { + // Don't try to checkout the branch if we aren't actually checking anything new out. + return Ok(current_target); + } + + // Determine if the oid corresponds to exactly a single branch. If so, + // check that out directly. + match oid { + Some(oid) => match branch_oid_to_names.get(&oid) { + Some(branch_names) => match branch_names.iter().exactly_one() { + Ok(branch_name) => { + // To remove the `refs/heads/` prefix + let name = CategorizedReferenceName::new(branch_name); + Ok(Some(name.remove_prefix()?)) + } + Err(_) => Ok(current_target), + }, + None => Ok(current_target), + }, + None => Ok(current_target), + } +} + /// Checks out the requested commit. If the operation succeeds, then displays /// the new smartlog. Otherwise displays a warning message. #[instrument] @@ -71,20 +108,25 @@ pub fn check_out_commit( render_smartlog, } = options; - let target = match target { - None => None, + let (target, oid) = match target { + None => (None, None), Some(CheckoutTarget::Reference(reference_name)) => { let categorized_target = CategorizedReferenceName::new(&reference_name); - Some(categorized_target.remove_prefix()?) + (Some(categorized_target.remove_prefix()?), None) } - Some(CheckoutTarget::Oid(oid)) => Some(oid.to_string()), - Some(CheckoutTarget::Unknown(target)) => Some(target), + Some(CheckoutTarget::Oid(oid)) => (Some(oid.to_string()), Some(oid)), + Some(CheckoutTarget::Unknown(target)) => (Some(target), None), }; if get_undo_create_snapshots(repo)? { create_snapshot(effects, git_run_info, repo, event_log_db, event_tx_id)?; } + let target = if get_auto_switch_branches(repo)? { + maybe_get_branch_name(target, oid, repo)? + } else { + target + }; let args = { let mut args = vec![OsStr::new("checkout")]; if let Some(target) = &target { diff --git a/git-branchless-lib/src/core/config.rs b/git-branchless-lib/src/core/config.rs index cbdc64e3e..b84e28244 100644 --- a/git-branchless-lib/src/core/config.rs +++ b/git-branchless-lib/src/core/config.rs @@ -45,6 +45,16 @@ pub fn get_main_branch_name(repo: &Repo) -> eyre::Result { Ok("master".to_string()) } +/// If `true`, switch to the branch associated with a target commit instead of +/// the commit directly. +/// +/// The switch will only occur if it is the only branch on the target commit. +#[instrument] +pub fn get_auto_switch_branches(repo: &Repo) -> eyre::Result { + repo.get_readonly_config()? + .get_or("branchless.navigation.autoSwitchBranches", true) +} + /// Get the default comment character. #[instrument] pub fn get_comment_char(repo: &Repo) -> eyre::Result { diff --git a/git-branchless/tests/command/test_move.rs b/git-branchless/tests/command/test_move.rs index 2f0bb615d..97c8825fa 100644 --- a/git-branchless/tests/command/test_move.rs +++ b/git-branchless/tests/command/test_move.rs @@ -3066,8 +3066,7 @@ fn test_move_branches_after_move() -> eyre::Result<()> { insta::assert_snapshot!(stderr, @r###" branchless: creating working copy snapshot Previous HEAD position was f81d55c create test5.txt - branchless: processing 1 update: ref HEAD - HEAD is now at 566e434 create test5.txt + Switched to branch 'bar' branchless: processing checkout "###); insta::assert_snapshot!(stdout, @r###" @@ -3077,7 +3076,7 @@ fn test_move_branches_after_move() -> eyre::Result<()> { [3/3] Committed as: 566e434 create test5.txt branchless: processing 2 updates: branch bar, branch foo branchless: processing 3 rewritten commits - branchless: running command: checkout 566e4341a4a9a930fc2bf7ccdfa168e9f266c34a + branchless: running command: checkout bar : O 62fc20d create test1.txt |\ @@ -3085,7 +3084,7 @@ fn test_move_branches_after_move() -> eyre::Result<()> { | | | o a248207 create test4.txt | | - | @ 566e434 (bar) create test5.txt + | @ 566e434 (> bar) create test5.txt | O 96d1c37 (master) create test2.txt In-memory rebase succeeded. @@ -3102,7 +3101,7 @@ fn test_move_branches_after_move() -> eyre::Result<()> { | | | o a248207 create test4.txt | | - | @ 566e434 (bar) create test5.txt + | @ 566e434 (> bar) create test5.txt | O 96d1c37 (master) create test2.txt "###); @@ -3121,7 +3120,7 @@ fn test_move_branches_after_move() -> eyre::Result<()> { | | | o a248207 create test4.txt | | - | @ 566e434 (bar) create test5.txt + | @ 566e434 (> bar) create test5.txt | O 96d1c37 (master) create test2.txt "###); @@ -3427,13 +3426,12 @@ fn test_move_delete_checked_out_branch() -> eyre::Result<()> { branchless: processing 3 rewritten commits branchless: processing 2 updates: branch more-work, branch work branchless: creating working copy snapshot - branchless: running command: checkout 91c5ce63686889388daec1120bf57bea8a744bc2 + branchless: running command: checkout master Previous HEAD position was 012efd6 create test3.txt - branchless: processing 1 update: ref HEAD - HEAD is now at 91c5ce6 create test2.txt + Switched to branch 'master' branchless: processing checkout : - @ 91c5ce6 (master) create test2.txt + @ 91c5ce6 (> master) create test2.txt | o 012efd6 (more-work) create test3.txt Successfully rebased and updated detached HEAD. @@ -3451,7 +3449,7 @@ fn test_move_delete_checked_out_branch() -> eyre::Result<()> { let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ 91c5ce6 (master) create test2.txt + @ 91c5ce6 (> master) create test2.txt | o 012efd6 (more-work) create test3.txt "###); @@ -3466,8 +3464,7 @@ fn test_move_delete_checked_out_branch() -> eyre::Result<()> { insta::assert_snapshot!(stderr, @r###" branchless: creating working copy snapshot Previous HEAD position was 96d1c37 create test2.txt - branchless: processing 1 update: ref HEAD - HEAD is now at 91c5ce6 create test2.txt + Switched to branch 'master' branchless: processing checkout "###); insta::assert_snapshot!(stdout, @r###" @@ -3477,9 +3474,9 @@ fn test_move_delete_checked_out_branch() -> eyre::Result<()> { [3/3] Committed as: 012efd6 create test3.txt branchless: processing 2 updates: branch more-work, branch work branchless: processing 3 rewritten commits - branchless: running command: checkout 91c5ce63686889388daec1120bf57bea8a744bc2 + branchless: running command: checkout master : - @ 91c5ce6 (master) create test2.txt + @ 91c5ce6 (> master) create test2.txt | o 012efd6 (more-work) create test3.txt In-memory rebase succeeded. @@ -3490,7 +3487,7 @@ fn test_move_delete_checked_out_branch() -> eyre::Result<()> { let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ 91c5ce6 (master) create test2.txt + @ 91c5ce6 (> master) create test2.txt | o 012efd6 (more-work) create test3.txt "###); diff --git a/git-branchless/tests/command/test_navigation.rs b/git-branchless/tests/command/test_navigation.rs index c479e8155..4f2edc3e6 100644 --- a/git-branchless/tests/command/test_navigation.rs +++ b/git-branchless/tests/command/test_navigation.rs @@ -600,7 +600,7 @@ fn test_checkout_pty_branch() -> eyre::Result<()> { let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ 62fc20d (master) create test1.txt + @ 62fc20d (> master) create test1.txt |\ | o 96d1c37 create test2.txt | @@ -839,3 +839,83 @@ fn test_navigation_checkout_target_only() -> eyre::Result<()> { Ok(()) } + +#[test] +#[cfg(unix)] +fn test_checkout_auto_switch_interactive() -> eyre::Result<()> { + let git = make_git()?; + + git.init_repo()?; + git.detach_head()?; + git.commit_file("test1", 1)?; + git.run(&["branch", "test1"])?; + git.commit_file("test2", 2)?; + git.run(&["branch", "test2"])?; + + run_in_pty( + &git, + &["co", "--interactive"], + &[ + PtyAction::WaitUntilContains("> "), + PtyAction::Write("test1"), + PtyAction::WaitUntilContains("> 62fc20d"), + PtyAction::Write(CARRIAGE_RETURN), + ], + )?; + + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + @ 62fc20d (> test1) create test1.txt + | + o 96d1c37 (test2) create test2.txt + "###); + } + + return Ok(()); +} + +#[test] +#[cfg(unix)] +fn test_checkout_auto_switch_interactive_disabled() -> eyre::Result<()> { + let git = make_git()?; + + git.init_repo()?; + git.run(&[ + "config", + "branchless.navigation.autoSwitchBranches", + "false", + ])?; + + git.detach_head()?; + git.commit_file("test1", 1)?; + git.run(&["branch", "test1"])?; + git.commit_file("test2", 2)?; + git.run(&["branch", "test2"])?; + + run_in_pty( + &git, + &["co", "--interactive"], + &[ + PtyAction::WaitUntilContains("> "), + PtyAction::Write("test1"), + PtyAction::WaitUntilContains("> 62fc20d"), + PtyAction::Write(CARRIAGE_RETURN), + ], + )?; + + { + let (stdout, _stderr) = git.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + O f777ecc (master) create initial.txt + | + @ 62fc20d (test1) create test1.txt + | + o 96d1c37 (test2) create test2.txt + "###); + } + + return Ok(()); +} diff --git a/git-branchless/tests/command/test_reword.rs b/git-branchless/tests/command/test_reword.rs index 992988e57..4ed82a4c4 100644 --- a/git-branchless/tests/command/test_reword.rs +++ b/git-branchless/tests/command/test_reword.rs @@ -49,7 +49,7 @@ fn test_reword_current_commit_not_head() -> eyre::Result<()> { let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ 62fc20d (test1) create test1.txt + @ 62fc20d (> test1) create test1.txt | O 96d1c37 (master) create test2.txt "###); @@ -59,7 +59,7 @@ fn test_reword_current_commit_not_head() -> eyre::Result<()> { let (stdout, _stderr) = git.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ a6f8868 (test1) foo + @ a6f8868 (> test1) foo | O 5207ad5 (master) create test2.txt "###); diff --git a/git-branchless/tests/command/test_undo.rs b/git-branchless/tests/command/test_undo.rs index c5bdd7b42..bd9b94d33 100644 --- a/git-branchless/tests/command/test_undo.rs +++ b/git-branchless/tests/command/test_undo.rs @@ -412,7 +412,7 @@ fn test_undo_move_refs() -> eyre::Result<()> { to 62fc20d create test1.txt 3. Check out from 96d1c37 create test2.txt to 62fc20d create test1.txt - Confirm? [yN] branchless: running command: checkout 62fc20d2a290daea0d52bdc2ed2ad4be6491010e --detach + Confirm? [yN] branchless: running command: checkout master --detach Applied 3 inverse events. "###); assert_eq!(exit_code, 0); @@ -637,7 +637,7 @@ fn test_undo_doesnt_make_working_dir_dirty() -> eyre::Result<()> { to f777ecc create initial.txt 5. Delete branch foo at f777ecc create initial.txt - Confirm? [yN] branchless: running command: checkout f777ecc9b0db5ed372b2615695191a8a17f79f24 --detach + Confirm? [yN] branchless: running command: checkout master --detach Applied 5 inverse events. "###); assert_eq!(exit_code, 0); @@ -876,7 +876,7 @@ fn test_undo_noninteractive() -> eyre::Result<()> { to 96d1c37 create test2.txt 4. Check out from 9ed8f9a bad message to 96d1c37 create test2.txt - Confirm? [yN] branchless: running command: checkout 96d1c37a3d4363611c49f7e52186e189a04c531f --detach + Confirm? [yN] branchless: running command: checkout master --detach : @ 96d1c37 (master) create test2.txt Applied 4 inverse events. diff --git a/git-branchless/tests/test_branchless.rs b/git-branchless/tests/test_branchless.rs index cb8f3a2ad..499a0e786 100644 --- a/git-branchless/tests/test_branchless.rs +++ b/git-branchless/tests/test_branchless.rs @@ -48,9 +48,9 @@ fn test_commands() -> eyre::Result<()> { { let (stdout, _stderr) = git.run(&["next"])?; insta::assert_snapshot!(stdout, @r###" - branchless: running command: checkout 3df4b9355b3b072aa6c50c6249bf32e289b3a661 + branchless: running command: checkout master : - @ 3df4b93 (master) create test.txt + @ 3df4b93 (> master) create test.txt "###); }