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). diff --git a/src/lib.rs b/src/lib.rs index a7b546c..7bba0bd 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(); @@ -403,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) } @@ -471,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. @@ -485,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, )?; @@ -518,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, )?; @@ -605,7 +602,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/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/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/basic.rs b/tests/tests/basic.rs index 91c496f..23ce67f 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] @@ -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 5f07b0d..a856f2a 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] @@ -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 92f28d6..b944308 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] @@ -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 13c06c6..847b7ae 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` @@ -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); @@ -163,12 +163,11 @@ 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); - 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/mod.rs b/tests/tests/mod.rs index bc600a8..2e2be8e 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -1,5 +1,6 @@ -mod basic_conflict; +mod all_branches; 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..74a22e3 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] @@ -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 2eb3c11..e86413b 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] @@ -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 31faf7d..afba4fb 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] @@ -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); @@ -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/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 f87cfce..8f9be13 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,12 +23,15 @@ 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) { branches.insert(branch_name.clone()); - commit.branches.push(branch_name); + commit.branches.push((branch_name, None)); } } @@ -45,11 +51,10 @@ 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()); } - for _ in 0..num_children { // Add a child commit commit.children.push(Default::default());