diff --git a/src/filepersist.rs b/src/filepersist.rs index 6334cac..2dc00da 100644 --- a/src/filepersist.rs +++ b/src/filepersist.rs @@ -2,6 +2,8 @@ use eyre::Context; use git2::Repository; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::rc::Rc; +use crate::api::PullRequest; #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct BranchPersistenceData { @@ -10,6 +12,35 @@ pub struct BranchPersistenceData { pub stack_tag: Option, } +impl BranchPersistenceData { + pub fn get_latest_base_commit(&self, pr_opt: Option<&Rc>) -> 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 { let persist_path = repo.path().join("git-stack"); if !persist_path.exists() { diff --git a/src/graph.rs b/src/graph.rs index ebc7887..3c2b12b 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -8,6 +8,10 @@ use crate::api::PullRequest; pub type FlatDep = Vec<(Rc, Option>)>; +pub fn find_pr_for_branch_in_flatdep<'a>(stack: &'a FlatDep, branch: &str) -> Option<&'a Rc> { + stack.iter().find(|(pr, _base_pr)| pr.head() == branch).map(|(pr, _)| pr) +} + pub fn build(prs: &[Rc]) -> Graph, usize> { let mut tree = Graph::, usize>::new(); let heads = prs.iter().map(|pr| pr.head()); diff --git a/src/lib.rs b/src/lib.rs index c401caa..245e35e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ pub mod markdown; pub mod persist; pub mod util; +pub mod rebase; + pub struct Credentials { // Personal access token token: String, diff --git a/src/main.rs b/src/main.rs index 46cfce3..f0992e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use git_stack::filepersist::{ }; use git_stack::graph::FlatDep; use git_stack::util::loop_until_confirm; -use git_stack::Credentials; +use git_stack::{Credentials, rebase}; use git_stack::{api, git, graph, markdown, persist}; fn clap<'a, 'b>() -> App<'a, 'b> { @@ -89,13 +89,10 @@ fn clap<'a, 'b>() -> App<'a, 'b> { .arg(ci.clone()) .arg(identifier.clone()); - let rebase = SubCommand::with_name("rebase") + let rebase = SubCommand::with_name("rebase-this") .about( - "Print a bash script to STDOUT that can rebase/update the stack (with a little help)", - ) - .setting(AppSettings::ArgRequiredElseHelp) - .arg(exclude.clone()) - .arg(identifier.clone()); + "Rebase this branch by replaying it on the latest version of its base.", + ); let branch = SubCommand::with_name("branch") .about( @@ -121,7 +118,7 @@ fn clap<'a, 'b>() -> App<'a, 'b> { .subcommand(annotate) .subcommand(log) .subcommand(rebase) - .subcommand(autorebase) + //.subcommand(autorebase) .subcommand(branch); app @@ -275,12 +272,23 @@ async fn main() -> eyre::Result<()> { } } - ("rebase", Some(m)) => { - let identifier = m.value_of("identifier").unwrap(); - let stack = build_pr_stack(identifier, &credentials, get_excluded(m)).await?; + ("rebase-this", Some(_)) => { + let head = repo.head()?; + 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); - println!("{}", script); + let head_stack_info = load_persistence_data_for_branch(&repo, current_branch) + .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)) => { diff --git a/src/rebase.rs b/src/rebase.rs new file mode 100644 index 0000000..3b98dab --- /dev/null +++ b/src/rebase.rs @@ -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(()) +}