From 5b7db8551c807b4daa5c16be591345bda80c27dc Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Thu, 10 Jun 2021 20:27:37 +0100 Subject: [PATCH 1/5] chore: cargo +stable fmt --- src/lib.rs | 1 - tests/tests/basic.rs | 2 +- tests/tests/basic_conflict.rs | 2 +- tests/tests/checked_out.rs | 2 +- tests/tests/conflict_resume.rs | 3 +-- tests/tests/mod.rs | 2 +- tests/tests/multiple_branches.rs | 2 +- tests/tests/multiple_refs_on_branch.rs | 2 +- tests/tests/random.rs | 3 +-- tests/utils/random_repo.rs | 13 +++++++++---- 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a7b546c..6ebc9ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -605,7 +605,6 @@ fn get_current_branch_or_commit(worktree_path: &Path) -> Result return Ok(BranchOrCommit::Branch(branch)); } - let commit = get_commit_hash(worktree_path, "HEAD")?; Ok(BranchOrCommit::Commit(commit)) } diff --git a/tests/tests/basic.rs b/tests/tests/basic.rs index 91c496f..4298ffa 100644 --- a/tests/tests/basic.rs +++ b/tests/tests/basic.rs @@ -1,5 +1,5 @@ -use autorebase::autorebase; use crate::{commit_graph, utils::*}; +use autorebase::autorebase; // Test building a repo using `build_repo`. #[test] diff --git a/tests/tests/basic_conflict.rs b/tests/tests/basic_conflict.rs index 5f07b0d..911952a 100644 --- a/tests/tests/basic_conflict.rs +++ b/tests/tests/basic_conflict.rs @@ -1,5 +1,5 @@ -use autorebase::autorebase; use crate::{commit_graph, utils::*}; +use autorebase::autorebase; // Single branch that cannot be rebased all the way to `master` commit due to conflicts. #[test] diff --git a/tests/tests/checked_out.rs b/tests/tests/checked_out.rs index 92f28d6..348bac5 100644 --- a/tests/tests/checked_out.rs +++ b/tests/tests/checked_out.rs @@ -1,6 +1,6 @@ +use crate::{commit_graph, utils::*}; use autorebase::autorebase; use std::fs; -use crate::{commit_graph, utils::*}; // Check we can rebase with the current checked out branch. #[test] diff --git a/tests/tests/conflict_resume.rs b/tests/tests/conflict_resume.rs index 13c06c6..48c3472 100644 --- a/tests/tests/conflict_resume.rs +++ b/tests/tests/conflict_resume.rs @@ -1,7 +1,7 @@ +use crate::{commit_graph, utils::*}; use autorebase::autorebase; use git_commands::git; use std::fs; -use crate::{commit_graph, utils::*}; // Single branch that cannot be rebased all the way to `master` commit due to conflicts, // However we then change master so there's no conflict, but when we run `autorebase` @@ -163,7 +163,6 @@ fn conflict_resume(slow_conflict_detection: bool) { // Check out master again so `wip` can be autorebased. git(&["checkout", "master"], repo_dir).expect("error checking out master"); - // Ok if we run `autorebase` is should succesfully rebase to master. print_git_log_graph(&repo_dir); diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs index bc600a8..2bfd0b1 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -1,5 +1,5 @@ -mod basic_conflict; mod basic; +mod basic_conflict; mod checked_out; mod conflict_resume; mod multiple_branches; diff --git a/tests/tests/multiple_branches.rs b/tests/tests/multiple_branches.rs index 1c408eb..bbdf8bc 100644 --- a/tests/tests/multiple_branches.rs +++ b/tests/tests/multiple_branches.rs @@ -1,5 +1,5 @@ -use autorebase::autorebase; use crate::{commit_graph, utils::*}; +use autorebase::autorebase; // Basic test but there is more than one branch that needs to be rebased. #[test] diff --git a/tests/tests/multiple_refs_on_branch.rs b/tests/tests/multiple_refs_on_branch.rs index 2eb3c11..9290a26 100644 --- a/tests/tests/multiple_refs_on_branch.rs +++ b/tests/tests/multiple_refs_on_branch.rs @@ -1,5 +1,5 @@ -use autorebase::autorebase; use crate::{commit_graph, utils::*}; +use autorebase::autorebase; // Basic test but there are multiple chained refs on the branch. #[test] diff --git a/tests/tests/random.rs b/tests/tests/random.rs index 31faf7d..50b6191 100644 --- a/tests/tests/random.rs +++ b/tests/tests/random.rs @@ -1,5 +1,5 @@ -use autorebase::autorebase; use crate::utils::*; +use autorebase::autorebase; // Test randomly generated repos. #[test] @@ -41,7 +41,6 @@ fn random_test_many_fast() { } fn random_test_many(slow_conflict_detection: bool) { - // This takes about 0.5 seconds per iteration. for _ in 0..10 { random_test(slow_conflict_detection); diff --git a/tests/utils/random_repo.rs b/tests/utils/random_repo.rs index f87cfce..4f53973 100644 --- a/tests/utils/random_repo.rs +++ b/tests/utils/random_repo.rs @@ -1,4 +1,3 @@ - use super::CommitDescription; use rand::Rng; use std::collections::HashSet; @@ -12,7 +11,11 @@ pub fn random_repo(allow_merges: bool) -> CommitDescription { // This uses recursion so we need to set a maximum depth to avoid stack overflows. - fn randomise_commit(commit: &mut CommitDescription, branches: &mut HashSet, depth: u32) { + fn randomise_commit( + commit: &mut CommitDescription, + branches: &mut HashSet, + depth: u32, + ) { let mut rng = rand::thread_rng(); // Randomly set the name, branch, contents, etc. @@ -20,7 +23,10 @@ pub fn random_repo(allow_merges: bool) -> CommitDescription { // The filename and contents are drawn from a small distribution to // give a reasonable chance of writing the same file to make conflicts, // or serendipitously eliminating conflicts. - commit.changes.insert(format!("{}.txt", rng.gen_range(0..8)), Some(format!("{}", rng.gen_range(0..8)))); + commit.changes.insert( + format!("{}.txt", rng.gen_range(0..8)), + Some(format!("{}", rng.gen_range(0..8))), + ); if rng.gen_bool(0.1) { let branch_name = format!("branch_{}", rng.gen_range(0..1000000)); if !branches.contains(&branch_name) { @@ -49,7 +55,6 @@ pub fn random_repo(allow_merges: bool) -> CommitDescription { branches.insert("master".to_owned()); } - for _ in 0..num_children { // Add a child commit commit.children.push(Default::default()); From 4c56252ec20e756cf2be16cfb69a35d4c3e70384 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Thu, 10 Jun 2021 20:34:39 +0100 Subject: [PATCH 2/5] feat: --all-branches --- src/lib.rs | 5 +++-- src/main.rs | 10 +++++++++- tests/tests/basic.rs | 2 +- tests/tests/basic_conflict.rs | 2 +- tests/tests/checked_out.rs | 4 ++-- tests/tests/conflict_resume.rs | 6 +++--- tests/tests/multiple_branches.rs | 2 +- tests/tests/multiple_refs_on_branch.rs | 2 +- tests/tests/random.rs | 2 +- 9 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6ebc9ff..3e20d69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ pub fn autorebase( repo_path: &Path, onto_branch: &str, slow_conflict_detection: bool, + include_all_branches: bool, ) -> Result<()> { // Check the git version. `git switch` was introduced in 2.23. if git_version(repo_path)?.as_slice() < &[2, 23] { @@ -83,7 +84,7 @@ pub fn autorebase( for branch in all_branches.iter() { if branch.branch == onto_branch { eprintln!(" - {} (target branch)", branch.branch.blue().bold()); - } else if branch.upstream.is_some() { + } else if !include_all_branches && branch.upstream.is_some() { eprintln!( " - {} (skipping because it has an upstream)", branch.branch.bold() @@ -103,7 +104,7 @@ pub fn autorebase( .iter() .filter(|branch| { branch.branch != onto_branch - && branch.upstream.is_none() + && (include_all_branches || branch.upstream.is_none()) && !matches!(&branch.worktree, Some(worktree) if !worktree.clean) }) .collect(); diff --git a/src/main.rs b/src/main.rs index 31d3b42..0f14658 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,9 @@ struct CliOptions { /// target branch directly #[argh(switch)] slow: bool, + /// include branches which have an upstream, the default is to exclude these + #[argh(switch)] + all_branches: bool, } fn main() -> Result<()> { @@ -35,7 +38,12 @@ fn run() -> Result<()> { // Find the repo dir in the same way git does. let repo_path = get_repo_path()?; - autorebase(&repo_path, &options.onto, options.slow)?; + autorebase( + &repo_path, + &options.onto, + options.slow, + options.all_branches, + )?; Ok(()) } diff --git a/tests/tests/basic.rs b/tests/tests/basic.rs index 4298ffa..23ce67f 100644 --- a/tests/tests/basic.rs +++ b/tests/tests/basic.rs @@ -63,7 +63,7 @@ fn basic_autorebase(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/basic_conflict.rs b/tests/tests/basic_conflict.rs index 911952a..a856f2a 100644 --- a/tests/tests/basic_conflict.rs +++ b/tests/tests/basic_conflict.rs @@ -34,7 +34,7 @@ fn conflict(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/checked_out.rs b/tests/tests/checked_out.rs index 348bac5..b944308 100644 --- a/tests/tests/checked_out.rs +++ b/tests/tests/checked_out.rs @@ -27,7 +27,7 @@ fn checkedout_clean(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); @@ -88,7 +88,7 @@ fn checkedout_dirty(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/conflict_resume.rs b/tests/tests/conflict_resume.rs index 48c3472..847b7ae 100644 --- a/tests/tests/conflict_resume.rs +++ b/tests/tests/conflict_resume.rs @@ -40,7 +40,7 @@ fn conflict_resume(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); @@ -98,7 +98,7 @@ fn conflict_resume(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", true).expect("error autorebasing"); + autorebase(repo_dir, "master", true, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); @@ -167,7 +167,7 @@ fn conflict_resume(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", true).expect("error autorebasing"); + autorebase(repo_dir, "master", true, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/multiple_branches.rs b/tests/tests/multiple_branches.rs index bbdf8bc..74a22e3 100644 --- a/tests/tests/multiple_branches.rs +++ b/tests/tests/multiple_branches.rs @@ -27,7 +27,7 @@ fn multiple_branches(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/multiple_refs_on_branch.rs b/tests/tests/multiple_refs_on_branch.rs index 9290a26..e86413b 100644 --- a/tests/tests/multiple_refs_on_branch.rs +++ b/tests/tests/multiple_refs_on_branch.rs @@ -31,7 +31,7 @@ fn multiple_branches(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); diff --git a/tests/tests/random.rs b/tests/tests/random.rs index 50b6191..afba4fb 100644 --- a/tests/tests/random.rs +++ b/tests/tests/random.rs @@ -23,7 +23,7 @@ fn random_test(slow_conflict_detection: bool) { print_git_log_graph(&repo_dir); - autorebase(repo_dir, "master", slow_conflict_detection).expect("error autorebasing"); + autorebase(repo_dir, "master", slow_conflict_detection, false).expect("error autorebasing"); print_git_log_graph(&repo_dir); From af0e49ef40c4dc82a2367a360123167fee18257d Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Thu, 10 Jun 2021 20:38:29 +0100 Subject: [PATCH 3/5] feat: exclude temporary branch name --- src/lib.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3e20d69..7bba0bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -404,6 +404,10 @@ fn get_branches(repo_path: &Path) -> Result> { worktree, }) }) + .filter(|branch| match branch { + Ok(b) if b.branch == TEMPORARY_BRANCH_NAME => false, + _ => true, + }) .collect::>()?; Ok(branches) } @@ -472,6 +476,8 @@ fn attempt_rebase(repo_path: &Path, worktree_path: &Path, onto: &str) -> Result< Ok(RebaseResult::Conflict) } +const TEMPORARY_BRANCH_NAME: &'static str = "autorebase_tmp_safe_to_delete"; + /// Create a temporary branch at master (`onto`), then try to rebase it ont /// `branch`. Count how many commits were rebased successfully, and /// return that number. Then abort the rebase, and delete the branch. @@ -486,12 +492,7 @@ fn count_nonconflicting_commits_via_rebase( // Create a temporary branch at master. If it already exists (e.g. because // a previous command failed) just reset it to here. git( - &[ - "switch", - "--force-create", - "autorebase_tmp_safe_to_delete", - onto, - ], + &["switch", "--force-create", TEMPORARY_BRANCH_NAME, onto], worktree_path, )?; @@ -519,12 +520,7 @@ fn count_nonconflicting_commits_via_rebase( git(&["switch", "--detach", onto], worktree_path)?; git( - &[ - "branch", - "--delete", - "--force", - "autorebase_tmp_safe_to_delete", - ], + &["branch", "--delete", "--force", TEMPORARY_BRANCH_NAME], worktree_path, )?; From 1ee41fcd110d70aca2025409e70cd072acf8dff1 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Thu, 10 Jun 2021 21:15:24 +0100 Subject: [PATCH 4/5] chore: tests for with and without excluding upstream branches --- tests/tests/all_branches.rs | 81 +++++++++++++++++++++++++++++++++++++ tests/tests/mod.rs | 1 + tests/utils/mod.rs | 18 +++++++-- tests/utils/random_repo.rs | 4 +- 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/tests/all_branches.rs diff --git a/tests/tests/all_branches.rs b/tests/tests/all_branches.rs new file mode 100644 index 0000000..97fff4d --- /dev/null +++ b/tests/tests/all_branches.rs @@ -0,0 +1,81 @@ +use std::collections::BTreeMap; + +use autorebase::autorebase; + +use crate::{commit_graph, utils::*}; + +fn with_include(include_all_branches: bool) -> BTreeMap { + git_fixed_dates(); + + let root = commit("First") + .write("a.txt", "hello") + .child(commit("Second").write("a.txt", "world").branch("master")) + .child( + commit("Third") + .write("b.txt", "foo") + .branch_with_upstream("other_main", "master"), + ) + .child(commit("wip").write("c.txt", "foo").branch("wip")); + + let repo = build_repo(&root, Some("master")); + + let repo_dir = repo.path(); + + print_git_log_graph(&repo_dir); + + autorebase(repo_dir, "master", false, include_all_branches).expect("error autorebasing"); + + print_git_log_graph(&repo_dir); + + get_repo_graph(&repo_dir).expect("error getting repo graph") +} + +#[test] +fn skips_branches() { + let graph = with_include(false); + + let expected_graph = commit_graph!( + "6781625b397d4f2eeb6da4b1fea570052683629f": CommitGraphNode { + parents: ["d3591307bd5590f14ae24d03ab41121ab94e2a90"], + refs: {"other_main"}, + }, + "a6de41485a5af44adc18b599a63840c367043e39": CommitGraphNode { + parents: ["d3591307bd5590f14ae24d03ab41121ab94e2a90"], + refs: {"master"}, + }, + "d3591307bd5590f14ae24d03ab41121ab94e2a90": CommitGraphNode { + parents: [], + refs: {""}, + }, + "f7aad7ec74984d4cd89090e572de921d5f9d1fc4": CommitGraphNode { + parents: ["a6de41485a5af44adc18b599a63840c367043e39"], + refs: {"wip"} + } + ); + assert_eq!(graph, expected_graph); +} + +#[test] +fn includes_branches() { + let graph = with_include(true); + + let expected_graph = commit_graph!( + "089f39ba0066fd2380da7dbe5201ec4b13f01b4a": CommitGraphNode { + parents: ["a6de41485a5af44adc18b599a63840c367043e39"], + refs: {"other_main"} + }, + "a6de41485a5af44adc18b599a63840c367043e39": CommitGraphNode { + parents: ["d3591307bd5590f14ae24d03ab41121ab94e2a90"], + refs: {"master"} + }, + "d3591307bd5590f14ae24d03ab41121ab94e2a90": CommitGraphNode { + parents: [], + refs: {""} + }, + "f7aad7ec74984d4cd89090e572de921d5f9d1fc4": CommitGraphNode { + parents: ["a6de41485a5af44adc18b599a63840c367043e39"], + refs: {"wip"} + } + ); + assert_eq!(graph, expected_graph); +} diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs index 2bfd0b1..2e2be8e 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -1,3 +1,4 @@ +mod all_branches; mod basic; mod basic_conflict; mod checked_out; diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 2ae314a..1f95dc5 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -63,7 +63,7 @@ pub struct CommitDescription { /// Map from filename to the new contents or None to delete it. changes: HashMap>, /// Branch names on this commit. - branches: Vec, + branches: Vec<(String, Option)>, /// Child commits. children: Vec, /// ID, only used for merge_parents. @@ -89,7 +89,12 @@ impl CommitDescription { self } pub fn branch(mut self, branch_name: &str) -> Self { - self.branches.push(branch_name.to_owned()); + self.branches.push((branch_name.to_owned(), None)); + self + } + pub fn branch_with_upstream(mut self, branch_name: &str, upstream_name: &str) -> Self { + self.branches + .push((branch_name.to_owned(), Some(upstream_name.to_owned()))); self } pub fn child(mut self, commit: CommitDescription) -> Self { @@ -180,8 +185,15 @@ pub fn build_repo(root: &CommitDescription, checkout_when_done: Option<&str>) -> git(&["checkout", this_commit], repo_path).expect("error checking out commit"); // Set branches. - for branch in c.branches.iter() { + for (branch, upstream) in c.branches.iter() { git(&["branch", branch], repo_path).expect("error setting branch"); + if let Some(upstream) = upstream.as_ref() { + git( + &["branch", &format!("--set-upstream-to={}", upstream), branch], + repo_path, + ) + .expect("error setting branch upstream"); + } } if let Some(id) = c.id { diff --git a/tests/utils/random_repo.rs b/tests/utils/random_repo.rs index 4f53973..8f9be13 100644 --- a/tests/utils/random_repo.rs +++ b/tests/utils/random_repo.rs @@ -31,7 +31,7 @@ pub fn random_repo(allow_merges: bool) -> CommitDescription { let branch_name = format!("branch_{}", rng.gen_range(0..1000000)); if !branches.contains(&branch_name) { branches.insert(branch_name.clone()); - commit.branches.push(branch_name); + commit.branches.push((branch_name, None)); } } @@ -51,7 +51,7 @@ pub fn random_repo(allow_merges: bool) -> CommitDescription { // So that we guarantee something is `master`, the first tip will be master. if !branches.contains("master") { - commit.branches.push("master".to_owned()); + commit.branches.push(("master".to_owned(), None)); branches.insert("master".to_owned()); } From 8762d22f15fa16a1f504ed59b87e37007899d2b6 Mon Sep 17 00:00:00 2001 From: "Chris West (Faux)" Date: Thu, 10 Jun 2021 21:16:51 +0100 Subject: [PATCH 5/5] docs: --all-branches --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc9c47e..46a25ba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ -Autorebase automatically rebases all of your feature branches onto `master`. If conflicts are found it will rebase to the last commit that doesn't cause conflicts. Currently it will rebase all branches that don't have an upstream. You don't need to switch to any branch, the only limitation is that a branch that is checked out and not clean will not be rebased (though I may add that in future). +Autorebase automatically rebases all of your feature branches onto `master`. If conflicts are found it will rebase to the last commit that doesn't cause conflicts. +By default, branches with an upstream are excluded. +You don't need to switch to any branch, the only limitation is that a branch that is checked out and not clean will not be rebased (though I may add that in future). Here is a demo. Before autorebase we have a number of old feature branches. @@ -32,7 +34,7 @@ Just run `autorebase` in your repo. This will perform the following actions 1. Update `master`, by pulling it with `--ff-only` unless you have it checked out with pending changes. 2. Create a temporary work tree inside `.git/autorebase` (this is currently never deleted but you can do it manually with `git worktree remove autorebase_worktree`). -3. Get the list of branches that have no upstream, and aren't checked out with pending changes. +3. Get the list of branches that have no upstream (except with `--all-branches`), and aren't checked out with pending changes. 4. For each branch: 1. Try to rebase it onto `master`. 2. If that fails due to conflicts, abort and try to rebase it as far as possible. There are two strategies for this (see below).