Skip to content

Commit 80b9bfe

Browse files
committed
Add new git rebase-with-intermediates command
With this, it's straightforward to have a bunch of intermediate branches, a future script will allow `git push-all-intermediates` that will make working with github with multiple PRs that depend on each other more ergonomic. See the new test for exactly what this does.
1 parent eab646f commit 80b9bfe

File tree

3 files changed

+144
-25
lines changed

3 files changed

+144
-25
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use structopt::StructOpt;
2+
3+
#[derive(Debug, StructOpt)]
4+
#[structopt(
5+
about = "Perform a rebase, and pull all the branches that were pointing at commits being rebased",
6+
max_term_width = 100,
7+
setting = structopt::clap::AppSettings::UnifiedHelpMessage,
8+
setting = structopt::clap::AppSettings::ColoredHelp,
9+
)]
10+
struct Args {
11+
/// The target ref
12+
onto: String,
13+
}
14+
15+
fn main() {
16+
let args = Args::from_args();
17+
if let Err(e) = git_fixup::rebase_onto(&args.onto) {
18+
eprintln!("{:#}", e);
19+
std::process::exit(1);
20+
}
21+
}

src/lib.rs

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashMap;
22
use std::process::Command;
33

4-
use anyhow::{anyhow, bail};
4+
use anyhow::{anyhow, bail, Context};
55
use console::style;
66
use dialoguer::{Confirm, Select};
77
use git2::{Branch, Commit, Diff, Object, ObjectType, Oid, Rebase, Repository};
@@ -20,21 +20,42 @@ pub fn instafix(
2020
) -> Result<(), anyhow::Error> {
2121
let repo = Repository::open(".")?;
2222
let diff = create_diff(&repo)?;
23-
let head = repo
24-
.head()
25-
.map_err(|e| anyhow!("HEAD is not pointing at a valid branch: {}", e))?;
23+
let head = repo.head().context("finding head commit")?;
2624
let head_branch = Branch::wrap(head);
27-
println!("head_branch: {:?}", head_branch.name().unwrap().unwrap());
2825
let upstream = get_upstream(&repo, &head_branch, upstream_branch_name)?;
2926
let commit_to_amend = select_commit_to_amend(&repo, upstream, max_commits, &message_pattern)?;
27+
eprintln!("Selected {}", disp(&commit_to_amend));
3028
do_fixup_commit(&repo, &head_branch, &commit_to_amend, false)?;
31-
println!("selected: {}", disp(&commit_to_amend));
3229
let current_branch = Branch::wrap(repo.head()?);
3330
do_rebase(&repo, &current_branch, &commit_to_amend, &diff)?;
3431

3532
Ok(())
3633
}
3734

35+
pub fn rebase_onto(onto: &str) -> Result<(), anyhow::Error> {
36+
let repo = Repository::open(".")?;
37+
let onto = repo
38+
.reference_to_annotated_commit(
39+
&repo
40+
.find_branch(onto, git2::BranchType::Local)
41+
.context("Chosing parent")?
42+
.get(),
43+
)
44+
.context("creating onto annotated commit")?;
45+
let head = repo
46+
.reference_to_annotated_commit(&repo.head().context("finding head")?)
47+
.context("choosing branch")?;
48+
let rebase = &mut repo
49+
.rebase(Some(&head), None, Some(&onto), None)
50+
.context("creating rebase")?;
51+
52+
if let Ok(_) = do_rebase_inner(&repo, rebase, None) {
53+
rebase.finish(None).context("finishing")?;
54+
}
55+
56+
Ok(())
57+
}
58+
3859
fn do_rebase(
3960
repo: &Repository,
4061
branch: &Branch,
@@ -48,8 +69,11 @@ fn do_rebase(
4869

4970
let rebase = &mut repo
5071
.rebase(Some(&branch_commit), Some(&first_parent), None, None)
51-
.map_err(|e| anyhow!("Error starting rebase: {}", e))?;
52-
match do_rebase_inner(repo, rebase, diff, fixup_message) {
72+
.context("starting rebase")?;
73+
74+
apply_diff_in_rebase(repo, rebase, diff)?;
75+
76+
match do_rebase_inner(repo, rebase, fixup_message) {
5377
Ok(_) => {
5478
rebase.finish(None)?;
5579
Ok(())
@@ -66,21 +90,11 @@ fn do_rebase(
6690
}
6791
}
6892

69-
fn do_rebase_inner(
93+
fn apply_diff_in_rebase(
7094
repo: &Repository,
7195
rebase: &mut Rebase,
7296
diff: &Diff,
73-
fixup_message: Option<&str>,
7497
) -> Result<(), anyhow::Error> {
75-
let sig = repo.signature()?;
76-
77-
let mut branches: HashMap<Oid, Branch> = HashMap::new();
78-
for (branch, _type) in repo.branches(Some(git2::BranchType::Local))?.flatten() {
79-
let oid = branch.get().peel_to_commit()?.id();
80-
// TODO: handle multiple branches pointing to the same commit
81-
branches.insert(oid, branch);
82-
}
83-
8498
match rebase.next() {
8599
Some(ref res) => {
86100
let op = res.as_ref().map_err(|e| anyhow!("No commit: {}", e))?;
@@ -98,11 +112,26 @@ fn do_rebase_inner(
98112
git2::ResetType::Soft,
99113
None,
100114
)?;
101-
102-
rewrit_id
103115
}
104116
None => bail!("Unable to start rebase: no first step in rebase"),
105117
};
118+
Ok(())
119+
}
120+
121+
/// Do a rebase, pulling all intermediate branches along the way
122+
fn do_rebase_inner(
123+
repo: &Repository,
124+
rebase: &mut Rebase,
125+
fixup_message: Option<&str>,
126+
) -> Result<(), anyhow::Error> {
127+
let sig = repo.signature()?;
128+
129+
let mut branches: HashMap<Oid, Branch> = HashMap::new();
130+
for (branch, _type) in repo.branches(Some(git2::BranchType::Local))?.flatten() {
131+
let oid = branch.get().peel_to_commit()?.id();
132+
// TODO: handle multiple branches pointing to the same commit
133+
branches.insert(oid, branch);
134+
}
106135

107136
while let Some(ref res) = rebase.next() {
108137
use git2::RebaseOperationType::*;
@@ -111,12 +140,17 @@ fn do_rebase_inner(
111140
match op.kind() {
112141
Some(Pick) => {
113142
let commit = repo.find_commit(op.id())?;
114-
if commit.message() != fixup_message {
143+
let message = commit.message();
144+
if message.is_some() && message != fixup_message {
115145
let new_id = rebase.commit(None, &sig, None)?;
116146
if let Some(branch) = branches.get_mut(&commit.id()) {
117-
branch
118-
.get_mut()
119-
.set_target(new_id, "git-fixup retarget historical branch")?;
147+
// Don't retarget the last branch, rebase.finish does that for us
148+
// TODO: handle multiple branches
149+
if rebase.operation_current() != Some(rebase.len() - 1) {
150+
branch
151+
.get_mut()
152+
.set_target(new_id, "git-fixup retarget historical branch")?;
153+
}
120154
}
121155
}
122156
}

tests/basic.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,63 @@ new
210210
assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected);
211211
}
212212

213+
///////////////////////////////////////////////////////////////////////////////
214+
// rebase-with-intermediates tests
215+
216+
#[test]
217+
fn test_rebase_with_intermediates() {
218+
let td = assert_fs::TempDir::new().unwrap();
219+
git_init(&td);
220+
221+
git_commits(&["a", "b", "c"], &td);
222+
git(&["checkout", "-b", "int_1", ":/b"], &td);
223+
git_commits(&["d", "e"], &td);
224+
git(&["checkout", "-b", "int_2"], &td);
225+
226+
git_commits(&["f", "g"], &td);
227+
228+
git(&["checkout", "-b", "changes"], &td);
229+
230+
git_commits(&["h", "i"], &td);
231+
232+
let out = git_log(&td);
233+
let expected = "\
234+
* i HEAD -> changes
235+
* h
236+
* g int_2
237+
* f
238+
* e int_1
239+
* d
240+
| * c main
241+
|/
242+
* b
243+
* a
244+
";
245+
assert_eq!(
246+
out, expected,
247+
"pre rebase\nactual:\n{}\nexpected:\n{}",
248+
out, expected
249+
);
250+
251+
if let Err(e) = rebase("main", &td).ok() {
252+
panic!("ERROR: {}", e);
253+
}
254+
255+
let out = git_log(&td);
256+
let expected = "\
257+
* i HEAD -> changes
258+
* h
259+
* g int_2
260+
* f
261+
* e int_1
262+
* d
263+
* c main
264+
* b
265+
* a
266+
";
267+
assert_eq!(out, expected, "\nactual:\n{}\nexpected:\n{}", out, expected);
268+
}
269+
213270
///////////////////////////////////////////////////////////////////////////////
214271
// Helpers
215272

@@ -287,3 +344,10 @@ fn fixup(dir: &assert_fs::TempDir) -> Command {
287344
c.current_dir(&dir.path());
288345
c
289346
}
347+
348+
fn rebase(onto: &str, dir: &assert_fs::TempDir) -> Command {
349+
let mut c = Command::cargo_bin("git-rebase-with-intermediates").unwrap();
350+
c.current_dir(&dir.path());
351+
c.arg(onto);
352+
c
353+
}

0 commit comments

Comments
 (0)