Skip to content

Commit

Permalink
feat(navigation): add --branch flag to git next/git prev
Browse files Browse the repository at this point in the history
  • Loading branch information
arxanas committed Nov 3, 2021
1 parent 477a311 commit f81148b
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
122 changes: 101 additions & 21 deletions src/commands/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,38 +85,105 @@ 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<CommitSet> {
let result = dag
.query()
.children(CommitSet::from(current_oid))?
.difference(&dag.obsolete_commits);
Ok(result)
};

let descendant_branches = || -> eyre::Result<CommitSet> {
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<CommitSet> {
let result = dag.query().parents(CommitSet::from(current_oid))?;
Ok(result)
};
let ancestor_branches = || -> eyre::Result<CommitSet> {
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)?
}
};

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;
}
Expand Down Expand Up @@ -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")
}
Expand Down
4 changes: 4 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
103 changes: 103 additions & 0 deletions tests/command/test_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <git-executable> 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: <git-executable> 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: <git-executable> 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: <git-executable> 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: <git-executable> 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(())
}

0 comments on commit f81148b

Please sign in to comment.