Skip to content

Commit 34584a0

Browse files
feat(switch): accept a revset as target
1 parent 7ee189b commit 34584a0

File tree

3 files changed

+109
-6
lines changed

3 files changed

+109
-6
lines changed

git-branchless-navigation/src/lib.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ use lib::core::repo_ext::RepoExt;
2525
use lib::util::{ExitCode, EyreExitOr};
2626
use tracing::{instrument, warn};
2727

28-
use git_branchless_opts::{SwitchOptions, TraverseCommitsOptions};
29-
use git_branchless_revset::resolve_default_smartlog_commits;
28+
use git_branchless_opts::{ResolveRevsetOptions, Revset, SwitchOptions, TraverseCommitsOptions};
29+
use git_branchless_revset::{resolve_commits, resolve_default_smartlog_commits};
3030
use git_branchless_smartlog::make_smartlog_graph;
3131
use lib::core::config::get_next_interactive;
32-
use lib::core::dag::{sorted_commit_set, CommitSet, Dag};
32+
use lib::core::dag::{sorted_commit_set, union_all, CommitSet, Dag};
3333
use lib::core::effects::Effects;
3434
use lib::core::eventlog::{EventLogDb, EventReplayer};
3535
use lib::core::formatting::Pluralize;
@@ -515,17 +515,28 @@ pub fn switch(
515515
/// query in the commit selector.
516516
Interactive(String),
517517

518+
/// The target expression is probably a git revision or reference and
519+
/// should be passed directly to git for resolution.
520+
Passthrough(String),
521+
522+
/// The target expression should be interpreted as a revset.
523+
Revset(Revset),
524+
518525
/// No target expression was specified.
519526
None,
520527
}
521528
let initial_query = match (interactive, target) {
522-
(true, Some(target)) => Target::Interactive(target.clone()),
529+
(true, Some(target)) => Target::Interactive(target.to_string()),
523530
(true, None) => Target::Interactive(String::new()),
524-
(false, Some(_)) => Target::None,
531+
(false, Some(target)) => match repo.revparse_single_commit(target.to_string().as_ref()) {
532+
Ok(Some(_)) => Target::Passthrough(target.to_string()),
533+
Ok(None) | Err(_) => Target::Revset(target.clone()),
534+
},
525535
(false, None) => Target::None,
526536
};
527537
let target: Option<CheckoutTarget> = match initial_query {
528538
Target::None => None,
539+
Target::Passthrough(target) => Some(CheckoutTarget::Unknown(target)),
529540
Target::Interactive(initial_query) => {
530541
match prompt_select_commit(
531542
None,
@@ -548,6 +559,35 @@ pub fn switch(
548559
None => return Ok(Err(ExitCode(1))),
549560
}
550561
}
562+
Target::Revset(target) => {
563+
let commit_sets = resolve_commits(
564+
effects,
565+
&repo,
566+
&mut dag,
567+
&[target.clone()],
568+
&ResolveRevsetOptions::default(),
569+
)?;
570+
571+
let commit_set = union_all(&commit_sets);
572+
let commit_set = dag.query_heads(commit_set)?;
573+
let commits = sorted_commit_set(&repo, &dag, &commit_set)?;
574+
575+
match commits.as_slice() {
576+
[commit] => Some(CheckoutTarget::Unknown(commit.get_oid().to_string())),
577+
[] | [..] => {
578+
writeln!(
579+
effects.get_error_stream(),
580+
"Cannot switch to target: expected '{target}' to contain 1 head, but found {}.",
581+
commits.len()
582+
)?;
583+
writeln!(
584+
effects.get_error_stream(),
585+
"Target should be a commit or a set of commits with exactly 1 head. Aborting."
586+
)?;
587+
return Ok(Err(ExitCode(1)));
588+
}
589+
}
590+
}
551591
};
552592

553593
let additional_args = {

git-branchless-opts/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,15 @@ pub struct SwitchOptions {
172172

173173
/// The commit or branch to check out.
174174
///
175+
/// If a revset is provided, it must evaluate to set with exactly 1 head.
176+
///
175177
/// If this is not provided, then interactive commit selection starts as
176178
/// if `--interactive` were passed.
177179
///
178180
/// If this is provided and the `--interactive` flag is passed, this
179181
/// text is used to pre-fill the interactive commit selector.
180182
#[clap(value_parser)]
181-
pub target: Option<String>,
183+
pub target: Option<Revset>,
182184
}
183185

184186
/// Internal use.

git-branchless/tests/test_navigation.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,67 @@ fn test_navigation_switch_target_only() -> eyre::Result<()> {
851851
Ok(())
852852
}
853853

854+
#[test]
855+
fn test_navigation_switch_revset() -> eyre::Result<()> {
856+
let git = make_git()?;
857+
git.init_repo()?;
858+
859+
git.detach_head()?;
860+
git.commit_file("test1", 1)?;
861+
git.run(&["checkout", "HEAD~"])?;
862+
863+
{
864+
{
865+
let (stdout, _stderr) = git.branchless("smartlog", &[])?;
866+
insta::assert_snapshot!(stdout, @r###"
867+
@ f777ecc (master) create initial.txt
868+
|
869+
o 62fc20d create test1.txt
870+
"###);
871+
}
872+
873+
let (stdout, _stderr) = git.branchless("switch", &["descendants(master)"])?;
874+
insta::assert_snapshot!(stdout, @r###"
875+
branchless: running command: <git-executable> checkout 62fc20d2a290daea0d52bdc2ed2ad4be6491010e
876+
O f777ecc (master) create initial.txt
877+
|
878+
@ 62fc20d create test1.txt
879+
"###);
880+
}
881+
882+
git.run(&["checkout", "HEAD~"])?;
883+
git.commit_file("test2", 2)?;
884+
git.run(&["checkout", "HEAD~"])?;
885+
886+
{
887+
{
888+
let (stdout, _stderr) = git.branchless("smartlog", &[])?;
889+
insta::assert_snapshot!(stdout, @r###"
890+
@ f777ecc (master) create initial.txt
891+
|\
892+
| o 62fc20d create test1.txt
893+
|
894+
o fe65c1f create test2.txt
895+
"###);
896+
}
897+
898+
let (_stdout, stderr) = git.branchless_with_options(
899+
"switch",
900+
&["descendants(master)"],
901+
&GitRunOptions {
902+
expected_exit_code: 1,
903+
..Default::default()
904+
},
905+
)?;
906+
insta::assert_snapshot!(stderr, @r###"
907+
Cannot switch to target: expected 'descendants(master)' to contain 1 head, but found 2.
908+
Target should be a commit or a set of commits with exactly 1 head. Aborting.
909+
"###);
910+
}
911+
912+
Ok(())
913+
}
914+
854915
#[test]
855916
#[cfg(unix)]
856917
fn test_switch_auto_switch_interactive() -> eyre::Result<()> {

0 commit comments

Comments
 (0)