`rebase-this` command for helping you rebase your branch
This commit is contained in:
parent
aa78e83aca
commit
b090591c52
|
@ -2,6 +2,8 @@ use eyre::Context;
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use crate::api::PullRequest;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct BranchPersistenceData {
|
pub struct BranchPersistenceData {
|
||||||
|
@ -10,6 +12,35 @@ pub struct BranchPersistenceData {
|
||||||
pub stack_tag: Option<String>,
|
pub stack_tag: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BranchPersistenceData {
|
||||||
|
pub fn get_latest_base_commit(&self, pr_opt: Option<&Rc<PullRequest>>) -> Option<(String, u64)> {
|
||||||
|
if let Some(pr) = pr_opt {
|
||||||
|
if let Some(ref base_branch) = self.base_branch {
|
||||||
|
if pr.base() != base_branch {
|
||||||
|
eprintln!("PR for branch {:?} does not have same base as persistent data!", pr.head());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose which one to use in the case of disagreement.
|
||||||
|
match (&self.base_commit, pr.base_commit()) {
|
||||||
|
(Some((here_base, here_ts)), Some((remote_base, remote_ts))) => {
|
||||||
|
if *here_ts >= remote_ts {
|
||||||
|
Some((here_base.to_owned(), *here_ts))
|
||||||
|
} else {
|
||||||
|
Some((remote_base.to_owned(), remote_ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(a), None) => Some(a.to_owned()),
|
||||||
|
(None, Some((a, b))) => Some((a.to_owned(), b)),
|
||||||
|
(None, None) => None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.base_commit.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_persistence_path(repo: &Repository) -> eyre::Result<PathBuf> {
|
pub fn find_persistence_path(repo: &Repository) -> eyre::Result<PathBuf> {
|
||||||
let persist_path = repo.path().join("git-stack");
|
let persist_path = repo.path().join("git-stack");
|
||||||
if !persist_path.exists() {
|
if !persist_path.exists() {
|
||||||
|
|
|
@ -8,6 +8,10 @@ use crate::api::PullRequest;
|
||||||
|
|
||||||
pub type FlatDep = Vec<(Rc<PullRequest>, Option<Rc<PullRequest>>)>;
|
pub type FlatDep = Vec<(Rc<PullRequest>, Option<Rc<PullRequest>>)>;
|
||||||
|
|
||||||
|
pub fn find_pr_for_branch_in_flatdep<'a>(stack: &'a FlatDep, branch: &str) -> Option<&'a Rc<PullRequest>> {
|
||||||
|
stack.iter().find(|(pr, _base_pr)| pr.head() == branch).map(|(pr, _)| pr)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(prs: &[Rc<PullRequest>]) -> Graph<Rc<PullRequest>, usize> {
|
pub fn build(prs: &[Rc<PullRequest>]) -> Graph<Rc<PullRequest>, usize> {
|
||||||
let mut tree = Graph::<Rc<PullRequest>, usize>::new();
|
let mut tree = Graph::<Rc<PullRequest>, usize>::new();
|
||||||
let heads = prs.iter().map(|pr| pr.head());
|
let heads = prs.iter().map(|pr| pr.head());
|
||||||
|
|
|
@ -6,6 +6,8 @@ pub mod markdown;
|
||||||
pub mod persist;
|
pub mod persist;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
pub mod rebase;
|
||||||
|
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
// Personal access token
|
// Personal access token
|
||||||
token: String,
|
token: String,
|
||||||
|
|
34
src/main.rs
34
src/main.rs
|
@ -13,7 +13,7 @@ use git_stack::filepersist::{
|
||||||
};
|
};
|
||||||
use git_stack::graph::FlatDep;
|
use git_stack::graph::FlatDep;
|
||||||
use git_stack::util::loop_until_confirm;
|
use git_stack::util::loop_until_confirm;
|
||||||
use git_stack::Credentials;
|
use git_stack::{Credentials, rebase};
|
||||||
use git_stack::{api, git, graph, markdown, persist};
|
use git_stack::{api, git, graph, markdown, persist};
|
||||||
|
|
||||||
fn clap<'a, 'b>() -> App<'a, 'b> {
|
fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||||
|
@ -89,13 +89,10 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||||
.arg(ci.clone())
|
.arg(ci.clone())
|
||||||
.arg(identifier.clone());
|
.arg(identifier.clone());
|
||||||
|
|
||||||
let rebase = SubCommand::with_name("rebase")
|
let rebase = SubCommand::with_name("rebase-this")
|
||||||
.about(
|
.about(
|
||||||
"Print a bash script to STDOUT that can rebase/update the stack (with a little help)",
|
"Rebase this branch by replaying it on the latest version of its base.",
|
||||||
)
|
);
|
||||||
.setting(AppSettings::ArgRequiredElseHelp)
|
|
||||||
.arg(exclude.clone())
|
|
||||||
.arg(identifier.clone());
|
|
||||||
|
|
||||||
let branch = SubCommand::with_name("branch")
|
let branch = SubCommand::with_name("branch")
|
||||||
.about(
|
.about(
|
||||||
|
@ -121,7 +118,7 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||||
.subcommand(annotate)
|
.subcommand(annotate)
|
||||||
.subcommand(log)
|
.subcommand(log)
|
||||||
.subcommand(rebase)
|
.subcommand(rebase)
|
||||||
.subcommand(autorebase)
|
//.subcommand(autorebase)
|
||||||
.subcommand(branch);
|
.subcommand(branch);
|
||||||
|
|
||||||
app
|
app
|
||||||
|
@ -275,12 +272,23 @@ async fn main() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
("rebase", Some(m)) => {
|
("rebase-this", Some(_)) => {
|
||||||
let identifier = m.value_of("identifier").unwrap();
|
let head = repo.head()?;
|
||||||
let stack = build_pr_stack(identifier, &credentials, get_excluded(m)).await?;
|
if !head.is_branch() {
|
||||||
|
bail!("Current HEAD isn't a branch.");
|
||||||
|
}
|
||||||
|
let current_branch = head
|
||||||
|
.shorthand()
|
||||||
|
.context("No name for the current reference")?;
|
||||||
|
|
||||||
let script = git::generate_rebase_script(stack);
|
let head_stack_info = load_persistence_data_for_branch(&repo, current_branch)
|
||||||
println!("{}", script);
|
.context("Failed to load branch persistence data for current branch")?;
|
||||||
|
|
||||||
|
|
||||||
|
let identifier = head_stack_info.stack_tag.as_ref().context("No stack tag registered for this branch!")?;
|
||||||
|
let stack = build_pr_stack(&identifier, &credentials, vec![]).await?;
|
||||||
|
|
||||||
|
rebase::rebase(&repo, &stack, head_stack_info, current_branch)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
("autorebase", Some(m)) => {
|
("autorebase", Some(m)) => {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
use eyre::{bail, Context, ContextCompat};
|
||||||
|
use git2::{BranchType, Oid, Repository};
|
||||||
|
use crate::filepersist::BranchPersistenceData;
|
||||||
|
use crate::graph::{find_pr_for_branch_in_flatdep, FlatDep};
|
||||||
|
|
||||||
|
pub fn rebase(repo: &Repository, stack: &FlatDep, head_stack_info: BranchPersistenceData, current_branch: &str) -> eyre::Result<()> {
|
||||||
|
let pr_opt = find_pr_for_branch_in_flatdep(stack, current_branch);
|
||||||
|
|
||||||
|
// First find out what the old commit was that we based this branch on in the first place!
|
||||||
|
let (base_commit, base_commit_ts) = head_stack_info.get_latest_base_commit(pr_opt)
|
||||||
|
.context("Couldn't get base commit from anywhere")?;
|
||||||
|
eprintln!("old base commit is {base_commit:?} at {base_commit_ts:?}!");
|
||||||
|
|
||||||
|
|
||||||
|
// Now find out what we should use as our new base!
|
||||||
|
// Use whatever we find on the active PR first because that gets changed if an underlying
|
||||||
|
// branch moves...
|
||||||
|
let base_branch_name = pr_opt.map(|pr| pr.base()).or(head_stack_info.base_branch.as_ref().map(|x| x.as_str()))
|
||||||
|
.with_context(|| format!("Can't find a new base branch for the branch {current_branch:?}"))?;
|
||||||
|
|
||||||
|
let base_branch = repo.find_branch(base_branch_name, BranchType::Local)
|
||||||
|
.context("Couldn't find base branch in git repo")?;
|
||||||
|
|
||||||
|
let current_branch = repo.find_branch(current_branch, BranchType::Local)
|
||||||
|
.context("Couldn't find current branch in git repo")?;
|
||||||
|
let current_branch_commit = current_branch.get().peel_to_commit().context("can't peel current branch to commit")?;
|
||||||
|
|
||||||
|
let new_base_commit = base_branch.get().peel_to_commit().context("")?.id();
|
||||||
|
eprintln!("new base commit likely {new_base_commit}");
|
||||||
|
let old_base_commit = Oid::from_str(&base_commit).context("can't look up old commit")?;
|
||||||
|
|
||||||
|
let merge_base_head_old = repo.merge_base(current_branch_commit.id(), old_base_commit).context("can't find merge base between current branch tip and old base")?;
|
||||||
|
eprintln!("merge base between old and current tip is {merge_base_head_old}.");
|
||||||
|
if merge_base_head_old != old_base_commit {
|
||||||
|
bail!("branch tip drifted away from old base commit?!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let merge_base_head_new = repo.merge_base(new_base_commit, old_base_commit).context("can't find merge base")?;
|
||||||
|
eprintln!("merge base between new and current tip is {merge_base_head_new}.");
|
||||||
|
if merge_base_head_new == new_base_commit {
|
||||||
|
bail!("branch tip is already descended from new base commit?!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
println!("git rebase --onto {new_base_commit} {old_base_commit}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue