Skip to content

Commit a32f2a7

Browse files
committed
Convert to using libgit2 for rebase
This will eventually allow me to bring branches over on various commits Functionality is currently reduced, it can no longer handle `squash` history.
1 parent 81b5731 commit a32f2a7

File tree

1 file changed

+139
-59
lines changed

1 file changed

+139
-59
lines changed

src/main.rs

Lines changed: 139 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414

1515
use std::collections::HashMap;
1616
use std::env;
17-
use std::error::Error;
1817
use std::process::Command;
1918

20-
use anyhow::bail;
19+
use anyhow::{anyhow, bail};
2120
use console::style;
2221
use dialoguer::{Confirm, Select};
23-
use git2::{Branch, Commit, Diff, Object, ObjectType, Oid, Repository};
22+
use git2::{Branch, Commit, Diff, Object, ObjectType, Oid, Rebase, Repository};
2423
use structopt::StructOpt;
2524

2625
const UPSTREAM_VAR: &str = "GIT_INSTAFIX_UPSTREAM";
@@ -72,53 +71,138 @@ fn main() {
7271
// An empty message means don't display any error message
7372
let msg = e.to_string();
7473
if !msg.is_empty() {
75-
println!("Error: {}", e);
74+
println!("Error: {:#}", e);
7675
}
7776
std::process::exit(1);
7877
}
7978
}
8079

8180
fn run(
82-
squash: bool,
81+
_squash: bool,
8382
max_commits: usize,
8483
message_pattern: Option<String>,
85-
) -> Result<(), Box<dyn Error>> {
84+
) -> Result<(), anyhow::Error> {
8685
let repo = Repository::open(".")?;
86+
let diff = create_diff(&repo)?;
8787
let head = repo
8888
.head()
89-
.map_err(|e| format!("HEAD is not pointing at a valid branch: {}", e))?;
90-
let head_tree = head.peel_to_tree()?;
89+
.map_err(|e| anyhow!("HEAD is not pointing at a valid branch: {}", e))?;
9190
let head_branch = Branch::wrap(head);
92-
let diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?;
91+
println!("head_branch: {:?}", head_branch.name().unwrap().unwrap());
9392
let upstream = get_upstream(&repo, &head_branch)?;
94-
let commit_to_amend = create_fixup_commit(
95-
&repo,
96-
&head_branch,
97-
upstream,
98-
&diff,
99-
squash,
100-
max_commits,
101-
&message_pattern,
102-
)?;
103-
println!(
104-
"selected: {} {}",
105-
&commit_to_amend.id().to_string()[0..10],
106-
commit_to_amend.summary().unwrap_or("")
107-
);
108-
// do the rebase
109-
let target_id = format!("{}~", commit_to_amend.id());
110-
Command::new("git")
111-
.args(&["rebase", "--interactive", "--autosquash", &target_id])
112-
.env("GIT_SEQUENCE_EDITOR", "true")
113-
.spawn()?
114-
.wait()?;
93+
let commit_to_amend = select_commit_to_amend(&repo, upstream, max_commits, &message_pattern)?;
94+
do_fixup_commit(&repo, &head_branch, &commit_to_amend, false)?;
95+
println!("selected: {}", disp(&commit_to_amend));
96+
let current_branch = Branch::wrap(repo.head()?);
97+
do_rebase(&repo, &current_branch, &commit_to_amend, &diff)?;
98+
99+
Ok(())
100+
}
101+
102+
fn do_rebase(
103+
repo: &Repository,
104+
branch: &Branch,
105+
commit_to_amend: &Commit,
106+
diff: &Diff,
107+
) -> Result<(), anyhow::Error> {
108+
let first_parent = repo.find_annotated_commit(commit_parent(commit_to_amend)?.id())?;
109+
let branch_commit = repo.reference_to_annotated_commit(branch.get())?;
110+
let fixup_commit = branch.get().peel_to_commit()?;
111+
let fixup_message = fixup_commit.message();
112+
113+
let rebase = &mut repo
114+
.rebase(Some(&branch_commit), Some(&first_parent), None, None)
115+
.map_err(|e| anyhow!("Error starting rebase: {}", e))?;
116+
match do_rebase_inner(repo, rebase, diff, fixup_message) {
117+
Ok(_) => {
118+
rebase.finish(None)?;
119+
Ok(())
120+
}
121+
Err(e) => {
122+
eprintln!("Aborting rebase, please apply it manualy via");
123+
eprintln!(
124+
" git rebase --interactive --autosquash {}~",
125+
first_parent.id()
126+
);
127+
rebase.abort()?;
128+
Err(e)
129+
}
130+
}
131+
}
132+
133+
fn do_rebase_inner(
134+
repo: &Repository,
135+
rebase: &mut Rebase,
136+
diff: &Diff,
137+
fixup_message: Option<&str>,
138+
) -> Result<(), anyhow::Error> {
139+
let sig = repo.signature()?;
140+
141+
match rebase.next() {
142+
Some(ref res) => {
143+
let op = res.as_ref().map_err(|e| anyhow!("No commit: {}", e))?;
144+
let target_commit = repo.find_commit(op.id())?;
145+
repo.apply(diff, git2::ApplyLocation::Both, None)?;
146+
let mut idx = repo.index()?;
147+
let oid = idx.write_tree()?;
148+
let tree = repo.find_tree(oid)?;
149+
150+
// TODO: Support squash amends
151+
152+
let rewrit_id = target_commit.amend(None, None, None, None, None, Some(&tree))?;
153+
repo.reset(
154+
&repo.find_object(rewrit_id, None)?,
155+
git2::ResetType::Soft,
156+
None,
157+
)?;
158+
159+
rewrit_id
160+
}
161+
None => bail!("Unable to start rebase: no first step in rebase"),
162+
};
163+
164+
while let Some(ref res) = rebase.next() {
165+
use git2::RebaseOperationType::*;
166+
167+
let op = res.as_ref().map_err(|e| anyhow!("Err: {}", e))?;
168+
match op.kind() {
169+
Some(Pick) => {
170+
let commit = repo.find_commit(op.id())?;
171+
if commit.message() != fixup_message {
172+
rebase.commit(None, &sig, None)?;
173+
}
174+
}
175+
Some(Fixup) | Some(Squash) | Some(Exec) | Some(Edit) | Some(Reword) => {
176+
// None of this should happen, we'd need to manually create the commits
177+
bail!("Unable to handle {:?} rebase operation", op.kind().unwrap())
178+
}
179+
None => {}
180+
}
181+
}
182+
115183
Ok(())
116184
}
117185

186+
fn commit_parent<'a>(commit: &'a Commit) -> Result<Commit<'a>, anyhow::Error> {
187+
match commit.parents().next() {
188+
Some(c) => Ok(c),
189+
None => bail!("Commit '{}' has no parents", disp(&commit)),
190+
}
191+
}
192+
193+
/// Display a commit as "short_hash summary"
194+
fn disp(commit: &Commit) -> String {
195+
format!(
196+
"{} {}",
197+
&commit.id().to_string()[0..10],
198+
commit.summary().unwrap_or("<no summary>"),
199+
)
200+
}
201+
118202
fn get_upstream<'a>(
119203
repo: &'a Repository,
120204
head_branch: &'a Branch,
121-
) -> Result<Option<Object<'a>>, Box<dyn Error>> {
205+
) -> Result<Option<Object<'a>>, anyhow::Error> {
122206
let upstream = if let Ok(upstream_name) = env::var(UPSTREAM_VAR) {
123207
let branch = repo
124208
.branches(None)?
@@ -128,7 +212,7 @@ fn get_upstream<'a>(
128212
.map(|n| n.expect("valid utf8 branchname") == &upstream_name)
129213
.unwrap_or(false)
130214
})
131-
.ok_or_else(|| format!("cannot find branch with name {:?}", upstream_name))?;
215+
.ok_or_else(|| anyhow!("cannot find branch with name {:?}", upstream_name))?;
132216
branch.into_reference().peel(ObjectType::Commit)?
133217
} else {
134218
if let Ok(upstream) = head_branch.upstream() {
@@ -150,48 +234,44 @@ fn get_upstream<'a>(
150234
Ok(Some(commit))
151235
}
152236

153-
fn create_fixup_commit<'a>(
154-
repo: &'a Repository,
155-
head_branch: &'a Branch,
156-
upstream: Option<Object<'a>>,
157-
diff: &'a Diff,
158-
squash: bool,
159-
max_commits: usize,
160-
message_pattern: &Option<String>,
161-
) -> Result<Commit<'a>, Box<dyn Error>> {
162-
let diffstat = diff.stats()?;
163-
if diffstat.files_changed() == 0 {
164-
let dirty_workdir_stats = repo.diff_index_to_workdir(None, None)?.stats()?;
237+
/// Get a diff either from the index or the diff from the index to the working tree
238+
fn create_diff(repo: &Repository) -> Result<Diff, anyhow::Error> {
239+
let head = repo.head()?;
240+
let head_tree = head.peel_to_tree()?;
241+
let staged_diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?;
242+
let diffstat = staged_diff.stats()?;
243+
let diff = if diffstat.files_changed() == 0 {
244+
let diff = repo.diff_index_to_workdir(None, None)?;
245+
let dirty_workdir_stats = diff.stats()?;
165246
if dirty_workdir_stats.files_changed() > 0 {
166247
print_diff(Changes::Unstaged)?;
167248
if !Confirm::new()
168249
.with_prompt("Nothing staged, stage and commit everything?")
169250
.interact()?
170251
{
171-
return Err("".into());
252+
bail!("");
172253
}
173254
} else {
174-
return Err("Nothing staged and no tracked files have any changes".into());
255+
bail!("Nothing staged and no tracked files have any changes");
175256
}
176-
let pathspecs: Vec<&str> = vec![];
177-
let mut idx = repo.index()?;
178-
idx.update_all(&pathspecs, None)?;
179-
idx.write()?;
257+
repo.apply(&diff, git2::ApplyLocation::Index, None)?;
258+
diff
180259
} else {
181260
println!("Staged changes:");
182261
print_diff(Changes::Staged)?;
183-
}
184-
let commit_to_amend = select_commit_to_amend(&repo, upstream, max_commits, message_pattern)?;
185-
do_fixup_commit(&repo, &head_branch, &commit_to_amend, squash)?;
186-
Ok(commit_to_amend)
262+
staged_diff
263+
};
264+
265+
Ok(diff)
187266
}
188267

268+
/// Commit the current index as a fixup or squash commit
189269
fn do_fixup_commit<'a>(
190270
repo: &'a Repository,
191271
head_branch: &'a Branch,
192272
commit_to_amend: &'a Commit,
193273
squash: bool,
194-
) -> Result<(), Box<dyn Error>> {
274+
) -> Result<(), anyhow::Error> {
195275
let msg = if squash {
196276
format!("squash! {}", commit_to_amend.id())
197277
} else {
@@ -278,9 +358,9 @@ fn select_commit_to_amend<'a>(
278358
})
279359
.collect::<Vec<_>>();
280360
if upstream.is_none() {
281-
eprintln!("Select a commit to amend (no upstream for HEAD):");
361+
println!("Select a commit to amend (no upstream for HEAD):");
282362
} else {
283-
eprintln!("Select a commit to amend:");
363+
println!("Select a commit to amend:");
284364
}
285365
let selected = Select::new().items(&rev_aliases).default(0).interact();
286366
Ok(repo.find_commit(commits[selected?].id())?)
@@ -293,7 +373,7 @@ fn format_ref(rf: &git2::Reference<'_>) -> Result<String, anyhow::Error> {
293373
Ok(format!("{} ({})", shorthand, &sha[..10]))
294374
}
295375

296-
fn print_diff(kind: Changes) -> Result<(), Box<dyn Error>> {
376+
fn print_diff(kind: Changes) -> Result<(), anyhow::Error> {
297377
let mut args = vec!["diff", "--stat"];
298378
if kind == Changes::Staged {
299379
args.push("--cached");
@@ -302,6 +382,6 @@ fn print_diff(kind: Changes) -> Result<(), Box<dyn Error>> {
302382
if status.success() {
303383
Ok(())
304384
} else {
305-
Err("git diff failed".into())
385+
bail!("git diff failed")
306386
}
307387
}

0 commit comments

Comments
 (0)