Skip to content

Commit

Permalink
Add support for interactively checking out a branch
Browse files Browse the repository at this point in the history
  • Loading branch information
bcspragu authored and arxanas committed Oct 2, 2022
1 parent 0ac3f32 commit 7ce51cc
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 29 deletions.
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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"
];
}
)
Expand Down
52 changes: 47 additions & 5 deletions git-branchless-lib/src/core/check_out.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -54,6 +57,40 @@ impl Default for CheckOutCommitOptions {
}
}

fn maybe_get_branch_name(
current_target: Option<String>,
oid: Option<NonZeroOid>,
repo: &Repo,
) -> eyre::Result<Option<String>> {
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]
Expand All @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions git-branchless-lib/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ pub fn get_main_branch_name(repo: &Repo) -> eyre::Result<String> {
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<bool> {
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<char> {
Expand Down
29 changes: 13 additions & 16 deletions git-branchless/tests/command/test_move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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###"
Expand All @@ -3077,15 +3076,15 @@ 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: <git-executable> checkout 566e4341a4a9a930fc2bf7ccdfa168e9f266c34a
branchless: running command: <git-executable> checkout bar
:
O 62fc20d create test1.txt
|\
| o 4838e49 (foo) create test3.txt
| |
| 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.
Expand All @@ -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
"###);
Expand All @@ -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
"###);
Expand Down Expand Up @@ -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: <git-executable> checkout 91c5ce63686889388daec1120bf57bea8a744bc2
branchless: running command: <git-executable> 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.
Expand All @@ -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
"###);
Expand All @@ -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###"
Expand All @@ -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: <git-executable> checkout 91c5ce63686889388daec1120bf57bea8a744bc2
branchless: running command: <git-executable> checkout master
:
@ 91c5ce6 (master) create test2.txt
@ 91c5ce6 (> master) create test2.txt
|
o 012efd6 (more-work) create test3.txt
In-memory rebase succeeded.
Expand All @@ -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
"###);
Expand Down
82 changes: 81 additions & 1 deletion git-branchless/tests/command/test_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down Expand Up @@ -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(());
}
4 changes: 2 additions & 2 deletions git-branchless/tests/command/test_reword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"###);
Expand All @@ -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
"###);
Expand Down
6 changes: 3 additions & 3 deletions git-branchless/tests/command/test_undo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <git-executable> checkout 62fc20d2a290daea0d52bdc2ed2ad4be6491010e --detach
Confirm? [yN] branchless: running command: <git-executable> checkout master --detach
Applied 3 inverse events.
"###);
assert_eq!(exit_code, 0);
Expand Down Expand Up @@ -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: <git-executable> checkout f777ecc9b0db5ed372b2615695191a8a17f79f24 --detach
Confirm? [yN] branchless: running command: <git-executable> checkout master --detach
Applied 5 inverse events.
"###);
assert_eq!(exit_code, 0);
Expand Down Expand Up @@ -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: <git-executable> checkout 96d1c37a3d4363611c49f7e52186e189a04c531f --detach
Confirm? [yN] branchless: running command: <git-executable> checkout master --detach
:
@ 96d1c37 (master) create test2.txt
Applied 4 inverse events.
Expand Down
4 changes: 2 additions & 2 deletions git-branchless/tests/test_branchless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ fn test_commands() -> eyre::Result<()> {
{
let (stdout, _stderr) = git.run(&["next"])?;
insta::assert_snapshot!(stdout, @r###"
branchless: running command: <git-executable> checkout 3df4b9355b3b072aa6c50c6249bf32e289b3a661
branchless: running command: <git-executable> checkout master
:
@ 3df4b93 (master) create test.txt
@ 3df4b93 (> master) create test.txt
"###);
}

Expand Down

0 comments on commit 7ce51cc

Please sign in to comment.