From f81148bdafeae95f434271305ebdf604fcda66f6 Mon Sep 17 00:00:00 2001 From: Waleed Khan Date: Tue, 2 Nov 2021 21:58:07 -0700 Subject: [PATCH] feat(navigation): add `--branch` flag to `git next`/`git prev` --- CHANGELOG.md | 1 + src/commands/navigation.rs | 122 +++++++++++++++++++++++++------ src/opts.rs | 4 + tests/command/test_navigation.rs | 103 ++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92fbfcf1d..d6a04ed6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - References created for garbage collection purposes no longer appear in `git log` by default. - References created for garbage collection purposes are no longer generated unless you've run `git branchless init` in the repository. - `git next` and `git prev` accept `-a`/`--all` to take you all the way to a head or root commit for your commit stack, respectively. +- `git next` and `git prev` accept `-b`/`--branch` to take you to the next or previous branch for your commit stack, respectively. ### Fixed diff --git a/src/commands/navigation.rs b/src/commands/navigation.rs index 5387343c3..bcb4613df 100644 --- a/src/commands/navigation.rs +++ b/src/commands/navigation.rs @@ -30,11 +30,14 @@ pub enum Command { /// The number of commits to traverse. #[derive(Clone, Copy, Debug)] pub enum Distance { - /// Traverse this number of commits. - NumCommits(usize), + /// Traverse this number of commits or branches. + NumCommits { + amount: usize, + move_by_branches: bool, + }, /// Traverse as many commits as possible. - AllTheWay, + AllTheWay { move_by_branches: bool }, } /// Some commits have multiple children, which makes `next` ambiguous. These @@ -82,24 +85,86 @@ fn advance( loop { let candidate_commits = match command { Command::Next => { - let children = dag - .query() - .children(CommitSet::from(current_oid))? - .difference(&dag.obsolete_commits); + let child_commits = || -> eyre::Result { + let result = dag + .query() + .children(CommitSet::from(current_oid))? + .difference(&dag.obsolete_commits); + Ok(result) + }; + + let descendant_branches = || -> eyre::Result { + let descendant_commits = dag.query().descendants(child_commits()?)?; + let descendant_branches = dag.branch_commits.intersection(&descendant_commits); + let descendants = dag.query().descendants(descendant_branches)?; + let nearest_descendant_branches = dag.query().roots(descendants)?; + Ok(nearest_descendant_branches) + }; + + let children = match distance { + Distance::AllTheWay { + move_by_branches: false, + } + | Distance::NumCommits { + amount: _, + move_by_branches: false, + } => child_commits()?, + + Distance::AllTheWay { + move_by_branches: true, + } + | Distance::NumCommits { + amount: _, + move_by_branches: true, + } => descendant_branches()?, + }; + sort_commit_set(repo, dag, &children)? } Command::Prev => { - let parents = dag.query().parents(CommitSet::from(current_oid))?; + let parent_commits = || -> eyre::Result { + let result = dag.query().parents(CommitSet::from(current_oid))?; + Ok(result) + }; + let ancestor_branches = || -> eyre::Result { + let ancestor_commits = dag.query().ancestors(parent_commits()?)?; + let ancestor_branches = dag.branch_commits.intersection(&ancestor_commits); + let nearest_ancestor_branches = + dag.query().heads_ancestors(ancestor_branches)?; + Ok(nearest_ancestor_branches) + }; - // The `--all` flag for `git prev` isn't useful if all it does - // is take you to the root commit for the repository. Instead, - // we assume that the user wanted to get to the root commit for - // their current *commit stack*. We filter out commits which - // aren't part of the commit stack so that we stop early here. let parents = match distance { - Distance::AllTheWay => parents.difference(&public_commits), - Distance::NumCommits(_) => parents, + Distance::AllTheWay { + move_by_branches: false, + } => { + // The `--all` flag for `git prev` isn't useful if all it does + // is take you to the root commit for the repository. Instead, + // we assume that the user wanted to get to the root commit for + // their current *commit stack*. We filter out commits which + // aren't part of the commit stack so that we stop early here. + let parents = parent_commits()?; + parents.difference(&public_commits) + } + + Distance::AllTheWay { + move_by_branches: true, + } => { + // See above case. + let parents = ancestor_branches()?; + parents.difference(&public_commits) + } + + Distance::NumCommits { + amount: _, + move_by_branches: false, + } => parent_commits()?, + + Distance::NumCommits { + amount: _, + move_by_branches: true, + } => ancestor_branches()?, }; sort_commit_set(repo, dag, &parents)? @@ -107,13 +172,18 @@ fn advance( }; match distance { - Distance::NumCommits(num_commits) => { - if i == num_commits { + Distance::NumCommits { + amount, + move_by_branches: _, + } => { + if i == amount { break; } } - Distance::AllTheWay => { + Distance::AllTheWay { + move_by_branches: _, + } => { if candidate_commits.is_empty() { break; } @@ -218,15 +288,25 @@ pub fn traverse_commits( let TraverseCommitsOptions { num_commits, all_the_way, + move_by_branches, oldest, newest, interactive, } = *options; let distance = match (all_the_way, num_commits) { - (false, None) => Distance::NumCommits(1), - (false, Some(num_commits)) => Distance::NumCommits(num_commits), - (true, None) => Distance::AllTheWay, + (false, None) => Distance::NumCommits { + amount: 1, + move_by_branches, + }, + + (false, Some(amount)) => Distance::NumCommits { + amount, + move_by_branches, + }, + + (true, None) => Distance::AllTheWay { move_by_branches }, + (true, Some(_)) => { eyre::bail!("num_commits and --all cannot both be set") } diff --git a/src/opts.rs b/src/opts.rs index 980d9da66..48a8bd0cf 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -54,6 +54,10 @@ pub struct TraverseCommitsOptions { #[clap(short = 'a', long = "all")] pub all_the_way: bool, + /// Move the specified number of branches rather than commits. + #[clap(short = 'b', long = "branch")] + pub move_by_branches: bool, + /// When encountering multiple next commits, choose the oldest. #[clap(short = 'o', long = "oldest")] pub oldest: bool, diff --git a/tests/command/test_navigation.rs b/tests/command/test_navigation.rs index 7d50216a0..155fb9485 100644 --- a/tests/command/test_navigation.rs +++ b/tests/command/test_navigation.rs @@ -478,3 +478,106 @@ fn test_navigation_traverse_all_the_way() -> eyre::Result<()> { Ok(()) } + +#[test] +fn test_navigation_traverse_branches() -> eyre::Result<()> { + let git = make_git()?; + + git.init_repo()?; + git.detach_head()?; + git.commit_file("test1", 1)?; + git.commit_file("test2", 2)?; + git.run(&["branch", "foo"])?; + git.commit_file("test3", 3)?; + git.commit_file("test4", 4)?; + git.run(&["branch", "bar"])?; + git.commit_file("test5", 5)?; + + { + let (stdout, _stderr) = git.run(&["prev", "-b", "2"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 96d1c37a3d4363611c49f7e52186e189a04c531f + O f777ecc9 (master) create initial.txt + | + o 62fc20d2 create test1.txt + | + @ 96d1c37a (foo) create test2.txt + | + o 70deb1e2 create test3.txt + | + o 355e173b (bar) create test4.txt + | + o f81d55c0 create test5.txt + "###); + + let (stdout, _stderr) = git.run(&["prev", "-a", "-b"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 96d1c37a3d4363611c49f7e52186e189a04c531f + O f777ecc9 (master) create initial.txt + | + o 62fc20d2 create test1.txt + | + @ 96d1c37a (foo) create test2.txt + | + o 70deb1e2 create test3.txt + | + o 355e173b (bar) create test4.txt + | + o f81d55c0 create test5.txt + "###); + + let (stdout, _stderr) = git.run(&["prev", "-b"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout f777ecc9b0db5ed372b2615695191a8a17f79f24 + @ f777ecc9 (master) create initial.txt + | + o 62fc20d2 create test1.txt + | + o 96d1c37a (foo) create test2.txt + | + o 70deb1e2 create test3.txt + | + o 355e173b (bar) create test4.txt + | + o f81d55c0 create test5.txt + "###); + } + + { + let (stdout, _stderr) = git.run(&["next", "-b", "2"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 355e173bf9c5d2efac2e451da0cdad3fb82b869a + O f777ecc9 (master) create initial.txt + | + o 62fc20d2 create test1.txt + | + o 96d1c37a (foo) create test2.txt + | + o 70deb1e2 create test3.txt + | + @ 355e173b (bar) create test4.txt + | + o f81d55c0 create test5.txt + "###); + } + + { + let (stdout, _stderr) = git.run(&["next", "-a", "-b"])?; + insta::assert_snapshot!(stdout, @r###" + branchless: running command: checkout 355e173bf9c5d2efac2e451da0cdad3fb82b869a + O f777ecc9 (master) create initial.txt + | + o 62fc20d2 create test1.txt + | + o 96d1c37a (foo) create test2.txt + | + o 70deb1e2 create test3.txt + | + @ 355e173b (bar) create test4.txt + | + o f81d55c0 create test5.txt + "###); + } + + Ok(()) +}