Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 45 additions & 46 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,6 @@ pub fn instafix(
Ok(())
}

pub fn rebase_onto(onto: &str) -> Result<(), anyhow::Error> {
let repo = Repository::open(".")?;
let onto = repo
.reference_to_annotated_commit(
repo.find_branch(onto, git2::BranchType::Local)
.context("Chosing parent")?
.get(),
)
.context("creating onto annotated commit")?;
let head = repo
.reference_to_annotated_commit(&repo.head().context("finding head")?)
.context("choosing branch")?;
let rebase = &mut repo
.rebase(Some(&head), None, Some(&onto), None)
.context("creating rebase")?;

if do_rebase_inner(&repo, rebase, None).is_ok() {
rebase.finish(None).context("finishing")?;
}

Ok(())
}

fn do_rebase(
repo: &Repository,
branch: &Branch,
Expand All @@ -81,9 +58,11 @@ fn do_rebase(
.rebase(Some(&branch_commit), Some(&first_parent), None, None)
.context("starting rebase")?;

apply_diff_in_rebase(repo, rebase, diff)?;
let mut branches = RepoBranches::for_repo(repo)?;

apply_diff_in_rebase(repo, rebase, diff, &mut branches)?;

match do_rebase_inner(repo, rebase, fixup_message) {
match do_rebase_inner(repo, rebase, fixup_message, branches) {
Ok(_) => {
rebase.finish(None)?;
Ok(())
Expand All @@ -104,6 +83,7 @@ fn apply_diff_in_rebase(
repo: &Repository,
rebase: &mut Rebase,
diff: &Diff,
branches: &mut RepoBranches,
) -> Result<(), anyhow::Error> {
match rebase.next() {
Some(ref res) => {
Expand All @@ -117,11 +97,11 @@ fn apply_diff_in_rebase(
// TODO: Support squash amends

let rewrit_id = target_commit.amend(None, None, None, None, None, Some(&tree))?;
repo.reset(
&repo.find_object(rewrit_id, None)?,
git2::ResetType::Soft,
None,
)?;
let rewrit_object = repo.find_object(rewrit_id, None)?;
let rewrit_commit_id = repo.find_commit(rewrit_object.id())?.id();
branches.retarget_branches(target_commit.id(), rewrit_commit_id, rebase)?;

repo.reset(&rewrit_object, git2::ResetType::Soft, None)?;
}
None => bail!("Unable to start rebase: no first step in rebase"),
};
Expand All @@ -133,16 +113,10 @@ fn do_rebase_inner(
repo: &Repository,
rebase: &mut Rebase,
fixup_message: Option<&str>,
mut branches: RepoBranches,
) -> Result<(), anyhow::Error> {
let sig = repo.signature()?;

let mut branches: HashMap<Oid, Branch> = HashMap::new();
for (branch, _type) in repo.branches(Some(git2::BranchType::Local))?.flatten() {
let oid = branch.get().peel_to_commit()?.id();
// TODO: handle multiple branches pointing to the same commit
branches.insert(oid, branch);
}

while let Some(ref res) = rebase.next() {
use git2::RebaseOperationType::*;

Expand All @@ -153,15 +127,7 @@ fn do_rebase_inner(
let message = commit.message();
if message.is_some() && message != fixup_message {
let new_id = rebase.commit(None, &sig, None)?;
if let Some(branch) = branches.get_mut(&commit.id()) {
// Don't retarget the last branch, rebase.finish does that for us
// TODO: handle multiple branches
if rebase.operation_current() != Some(rebase.len() - 1) {
branch
.get_mut()
.set_target(new_id, "git-instafix retarget historical branch")?;
}
}
branches.retarget_branches(commit.id(), new_id, rebase)?;
}
}
Some(Fixup) | Some(Squash) | Some(Exec) | Some(Edit) | Some(Reword) => {
Expand All @@ -175,6 +141,39 @@ fn do_rebase_inner(
Ok(())
}

struct RepoBranches<'a>(HashMap<Oid, Branch<'a>>);

impl<'a> RepoBranches<'a> {
fn for_repo(repo: &'a Repository) -> Result<RepoBranches<'a>, anyhow::Error> {
let mut branches: HashMap<Oid, Branch> = HashMap::new();
for (branch, _type) in repo.branches(Some(git2::BranchType::Local))?.flatten() {
let oid = branch.get().peel_to_commit()?.id();
// TODO: handle multiple branches pointing to the same commit
branches.insert(oid, branch);
}
Ok(RepoBranches(branches))
}

/// Move branches whos commits have moved
fn retarget_branches(
&mut self,
original_commit: Oid,
target_commit: Oid,
rebase: &mut Rebase<'_>,
) -> Result<(), anyhow::Error> {
if let Some(branch) = self.0.get_mut(&original_commit) {
// Don't retarget the last branch, rebase.finish does that for us
// TODO: handle multiple branches
if rebase.operation_current() != Some(rebase.len() - 1) {
branch
.get_mut()
.set_target(target_commit, "git-instafix retarget historical branch")?;
}
}
Ok(())
}
}

fn commit_parent<'a>(commit: &'a Commit) -> Result<Commit<'a>, anyhow::Error> {
match commit.parents().next() {
Some(c) => Ok(c),
Expand Down
57 changes: 57 additions & 0 deletions tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,63 @@ new
assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected);
}

#[test]
fn retarget_branch_target_of_edit() {
let td = assert_fs::TempDir::new().unwrap();
git_init(&td);

git_commits(&["a", "b"], &td);
git(&["checkout", "-b", "intermediate"], &td);
git_commits(&["c", "d", "target"], &td);

git(&["checkout", "-b", "changes"], &td);
git_commits(&["e", "f"], &td);

let expected = "\
* f HEAD -> changes
* e
* target intermediate
* d
* c
* b main
* a
";
let out = git_log(&td);
assert_eq!(
out, expected,
"before rebase:\nactual:\n{}\nexpected:\n{}",
out, expected
);

td.child("new").touch().unwrap();
git(&["add", "new"], &td);

fixup(&td).args(&["-P", "target"]).assert().success();

let out = git_log(&td);
assert_eq!(
out, expected,
"after rebase\nactual:\n{}\nexpected:\n{}",
out, expected
);

let (files, err) = git_changed_files("target", &td);
assert_eq!(
files,
"\
file_target
new
",
"out: {} err: {}",
files,
err
);

// should be identical to before
let out = git_log(&td);
assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected);
}

///////////////////////////////////////////////////////////////////////////////
// Helpers

Expand Down