Compare commits
8 Commits
luqven/mas
...
master
Author | SHA1 | Date |
---|---|---|
Olivier 'reivilibre' | b090591c52 | |
Olivier 'reivilibre' | aa78e83aca | |
Olivier 'reivilibre' | c283492fa7 | |
Olivier 'reivilibre' | de3a4beece | |
Olivier 'reivilibre' | 56494383dc | |
Olivier 'reivilibre' | 33f6c621b8 | |
Olivier 'reivilibre' | 331ff0d20c | |
Olivier 'reivilibre' | 3e1d0e8048 |
|
@ -8,4 +8,7 @@ Cargo.lock
|
|||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
# IDEA
|
||||
/.idea
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "gh-stack"
|
||||
name = "git-stack"
|
||||
version = "0.3.0"
|
||||
authors = ["Timothy Andrew <mail@timothyandrew.net>, Luis Ball <luqven@gmail.com>"]
|
||||
authors = ["Timothy Andrew <mail@timothyandrew.net>, Luis Ball <luqven@gmail.com>, Olivier 'reivilibre' <olivier@librepush.net>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/luqven/gh-stack"
|
||||
repository = "https://git.emunest.net/reivilibre/git-stack"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
description = "Manage stacked PR workflows on Github"
|
||||
|
@ -23,4 +23,6 @@ git2 = "0.13"
|
|||
dialoguer = "0.6.2"
|
||||
clap = "2.33"
|
||||
console = "0.11"
|
||||
dotenv = "0.15"
|
||||
dotenv = "0.15"
|
||||
eyre = "0.6.8"
|
||||
serde_json = "1.0.89"
|
18
README.md
18
README.md
|
@ -1,3 +1,21 @@
|
|||
# reivilibre fork of gh-stack
|
||||
|
||||
Wishes:
|
||||
|
||||
- [ ] make it play nicely with a workflow that uses 'squash merges' for PRs
|
||||
- [ ] preserve info about bases/stack even after first PRs have been merged
|
||||
- [ ] alternative to autorebase that doesn't force-push unchanged PRs (thus invalidating reviews)
|
||||
- [x] rename to git-stack so that we can eventually support other code forges and also fit into git aliases more easily.
|
||||
|
||||
Potential wishes:
|
||||
|
||||
- [ ] support for other forges e.g. Gitea
|
||||
|
||||
|
||||
Original README from https://github.com/luqven/gh-stack follows...
|
||||
|
||||
---
|
||||
|
||||
# gh-stack
|
||||
|
||||
> This README and tool were originally written by [@timothyandrew](https://github.com/timothyandrew/gh-stack). I highly recommend reading his blog post on sacked-PR workflows [here](https://0xc0d1.com/blog/git-stack/).
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::error::Error;
|
||||
|
@ -70,6 +71,38 @@ impl PullRequest {
|
|||
&self.base.gitref
|
||||
}
|
||||
|
||||
/// The base commit of the branch is stored in the pull request for later.
|
||||
/// This digs it out if possible.
|
||||
/// There is a timestamp included.
|
||||
pub fn base_commit(&self) -> Option<(&str, u64)> {
|
||||
let regex = Regex::new("git-stack-base-commit:([a-z0-9]+)@([0-9]+)").unwrap();
|
||||
self.body
|
||||
.as_ref()
|
||||
.map(|body| {
|
||||
regex.captures(&body).map(|re_match| {
|
||||
(
|
||||
re_match.get(1).unwrap().as_str(),
|
||||
re_match[2].parse().unwrap(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Stored version of the base branch in the pull request body.
|
||||
/// Needed for when the original branch is merged and so it vanishes.
|
||||
pub fn base_branch_orig(&self) -> Option<&str> {
|
||||
let regex = Regex::new("git-stack-base-branch:([^ ]+)").unwrap();
|
||||
self.body
|
||||
.as_ref()
|
||||
.map(|body| {
|
||||
regex
|
||||
.captures(&body)
|
||||
.map(|re_match| re_match.get(1).unwrap().as_str())
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ pub async fn fetch_reviews_for_pull_request(
|
|||
pub async fn fetch_pull_requests_matching(
|
||||
pattern: &str,
|
||||
credentials: &Credentials,
|
||||
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
|
||||
) -> eyre::Result<Vec<PullRequest>> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let request = api::base_request(
|
||||
|
@ -75,8 +75,7 @@ pub async fn fetch_matching_pull_requests_from_repository(
|
|||
pattern: &str,
|
||||
repository: &str,
|
||||
credentials: &Credentials,
|
||||
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
|
||||
|
||||
) -> eyre::Result<Vec<PullRequest>> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let request = api::base_request(
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
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 {
|
||||
pub base_branch: Option<String>,
|
||||
pub base_commit: Option<(String, u64)>,
|
||||
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> {
|
||||
let persist_path = repo.path().join("git-stack");
|
||||
if !persist_path.exists() {
|
||||
std::fs::create_dir(&persist_path).context("Failed to create persistence path")?;
|
||||
}
|
||||
Ok(persist_path)
|
||||
}
|
||||
|
||||
pub fn persistence_path_for_branch(repo: &Repository, branch: &str) -> eyre::Result<PathBuf> {
|
||||
Ok(find_persistence_path(repo)?.join(branch))
|
||||
}
|
||||
|
||||
pub fn load_persistence_data_for_branch(
|
||||
repo: &Repository,
|
||||
branch: &str,
|
||||
) -> eyre::Result<BranchPersistenceData> {
|
||||
let path = persistence_path_for_branch(repo, branch)?;
|
||||
if !path.exists() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
let content = std::fs::read(&path).with_context(|| format!("failed to read {path:?}"))?;
|
||||
Ok(serde_json::from_slice::<BranchPersistenceData>(&content)
|
||||
.with_context(|| format!("failed to deserialise {path:?}"))?)
|
||||
}
|
||||
|
||||
pub fn save_persistence_data_for_branch(
|
||||
repo: &Repository,
|
||||
branch: &str,
|
||||
data: &BranchPersistenceData,
|
||||
) -> eyre::Result<()> {
|
||||
let path = persistence_path_for_branch(repo, branch)?;
|
||||
let parent = path.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create dirs {:?}", parent))?;
|
||||
}
|
||||
let json = serde_json::to_vec(data)?;
|
||||
std::fs::write(&path, json).with_context(|| format!("failed to write {path:?}"))?;
|
||||
Ok(())
|
||||
}
|
|
@ -4,7 +4,6 @@ use crate::util::loop_until_confirm;
|
|||
use git2::build::CheckoutBuilder;
|
||||
use git2::{CherrypickOptions, Commit, Index, Oid, Repository, Revwalk, Sort};
|
||||
|
||||
use std::error::Error;
|
||||
use tokio::process::Command;
|
||||
|
||||
fn remote_ref(remote: &str, git_ref: &str) -> String {
|
||||
|
@ -149,7 +148,7 @@ pub async fn perform_rebase(
|
|||
remote: &str,
|
||||
boundary: Option<&str>,
|
||||
ci: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
) -> eyre::Result<()> {
|
||||
let deps = deps
|
||||
.iter()
|
||||
.filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)
|
||||
|
|
|
@ -8,6 +8,10 @@ use crate::api::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> {
|
||||
let mut tree = Graph::<Rc<PullRequest>, usize>::new();
|
||||
let heads = prs.iter().map(|pr| pr.head());
|
||||
|
@ -16,7 +20,8 @@ pub fn build(prs: &[Rc<PullRequest>]) -> Graph<Rc<PullRequest>, usize> {
|
|||
|
||||
for (i, pr) in prs.iter().enumerate() {
|
||||
let head_handle = handles[i];
|
||||
if let Some(&base_handle) = handles_by_head.get(pr.base()) {
|
||||
let base_to_use = pr.base_branch_orig().unwrap_or(pr.base());
|
||||
if let Some(&base_handle) = handles_by_head.get(base_to_use) {
|
||||
tree.add_edge(*base_handle, head_handle, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
pub mod api;
|
||||
pub mod filepersist;
|
||||
pub mod git;
|
||||
pub mod graph;
|
||||
pub mod markdown;
|
||||
pub mod persist;
|
||||
pub mod util;
|
||||
|
||||
pub mod rebase;
|
||||
|
||||
pub struct Credentials {
|
||||
// Personal access token
|
||||
token: String,
|
||||
|
|
134
src/main.rs
134
src/main.rs
|
@ -1,16 +1,20 @@
|
|||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use console::style;
|
||||
use eyre::{bail, Context, ContextCompat};
|
||||
use git2::Repository;
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::rc::Rc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gh_stack::api::PullRequest;
|
||||
use gh_stack::graph::FlatDep;
|
||||
use gh_stack::util::loop_until_confirm;
|
||||
use gh_stack::Credentials;
|
||||
use gh_stack::{api, git, graph, markdown, persist};
|
||||
use git_stack::api::PullRequest;
|
||||
use git_stack::filepersist::{
|
||||
load_persistence_data_for_branch, save_persistence_data_for_branch, BranchPersistenceData,
|
||||
};
|
||||
use git_stack::graph::FlatDep;
|
||||
use git_stack::util::loop_until_confirm;
|
||||
use git_stack::{Credentials, rebase};
|
||||
use git_stack::{api, git, graph, markdown, persist};
|
||||
|
||||
fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||
let identifier = Arg::with_name("identifier")
|
||||
|
@ -85,15 +89,28 @@ 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)",
|
||||
"Rebase this branch by replaying it on the latest version of its base.",
|
||||
);
|
||||
|
||||
let branch = SubCommand::with_name("branch")
|
||||
.about(
|
||||
"Create a new branch that is part of a stack."
|
||||
)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.arg(exclude.clone())
|
||||
.arg(identifier.clone());
|
||||
.arg(Arg::with_name("name")
|
||||
.help("Branch name")
|
||||
.required(true)
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("identifier")
|
||||
.required(false)
|
||||
.long("identifier")
|
||||
.short("i")
|
||||
.takes_value(true)
|
||||
.help("Name of the stack (overrides current stack; mainly useful for starting a stack)"));
|
||||
|
||||
let app = App::new("gh-stack")
|
||||
let app = App::new("git-stack")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.setting(AppSettings::DisableVersion)
|
||||
.setting(AppSettings::VersionlessSubcommands)
|
||||
|
@ -101,7 +118,8 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
|||
.subcommand(annotate)
|
||||
.subcommand(log)
|
||||
.subcommand(rebase)
|
||||
.subcommand(autorebase);
|
||||
//.subcommand(autorebase)
|
||||
.subcommand(branch);
|
||||
|
||||
app
|
||||
}
|
||||
|
@ -110,7 +128,7 @@ async fn build_pr_stack(
|
|||
pattern: &str,
|
||||
credentials: &Credentials,
|
||||
exclude: Vec<String>,
|
||||
) -> Result<FlatDep, Box<dyn Error>> {
|
||||
) -> eyre::Result<FlatDep> {
|
||||
let prs = api::search::fetch_pull_requests_matching(pattern, &credentials).await?;
|
||||
|
||||
let prs = prs
|
||||
|
@ -128,7 +146,7 @@ async fn build_pr_stack_for_repo(
|
|||
repository: &str,
|
||||
credentials: &Credentials,
|
||||
exclude: Vec<String>,
|
||||
) -> Result<FlatDep, Box<dyn Error>> {
|
||||
) -> eyre::Result<FlatDep> {
|
||||
let prs = api::search::fetch_matching_pull_requests_from_repository(
|
||||
pattern,
|
||||
repository,
|
||||
|
@ -162,7 +180,7 @@ fn remove_title_prefixes(title: String, prefix: &str) -> String {
|
|||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
async fn main() -> eyre::Result<()> {
|
||||
dotenv::from_filename(".gh-stack.env").ok();
|
||||
|
||||
let token = env::var("GHSTACK_OAUTH_TOKEN").expect("You didn't pass `GHSTACK_OAUTH_TOKEN`");
|
||||
|
@ -171,6 +189,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
let credentials = Credentials::new(&token);
|
||||
let matches = clap().get_matches();
|
||||
|
||||
let repo = Repository::open(".").context("Couldn't open git repository; are you in one?")?;
|
||||
|
||||
match matches.subcommand() {
|
||||
("annotate", Some(m)) => {
|
||||
let identifier = m.value_of("identifier").unwrap();
|
||||
|
@ -252,12 +272,23 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
("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)) => {
|
||||
|
@ -306,6 +337,69 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
println!("All done!");
|
||||
}
|
||||
|
||||
("branch", Some(m)) => {
|
||||
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 current_commit = head.peel_to_commit().context("Can't peel to commit")?;
|
||||
|
||||
let head_stack_info = load_persistence_data_for_branch(&repo, current_branch)
|
||||
.context("Failed to load branch persistence data for current branch")?;
|
||||
|
||||
println!(
|
||||
"Current branch is {current_branch} @ {}.",
|
||||
current_commit.id()
|
||||
);
|
||||
|
||||
let branch_name = m.value_of("name").unwrap();
|
||||
|
||||
let new_stack_identifier = match head_stack_info.stack_tag {
|
||||
None => {
|
||||
println!("{current_branch} is not part of a stack.");
|
||||
match m.value_of("identifier") {
|
||||
None => {
|
||||
bail!("Current branch not part of a stack and a stack identifier was not specified with -i!");
|
||||
}
|
||||
Some(ident) => ident.to_owned(),
|
||||
}
|
||||
}
|
||||
Some(cur_stack) => {
|
||||
println!("{current_branch} is part of the [{cur_stack}] stack.");
|
||||
match m.value_of("identifier") {
|
||||
None => cur_stack,
|
||||
Some(_) => {
|
||||
bail!("Current branch is part of a stack but a stack identifier was specified with -i!");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_branch = repo
|
||||
.branch(branch_name, ¤t_commit, false)
|
||||
.context("Can't create branch")?;
|
||||
repo.set_head(new_branch.into_reference().name().unwrap())
|
||||
.context("Couldn't switch to new branch")?;
|
||||
save_persistence_data_for_branch(
|
||||
&repo,
|
||||
branch_name,
|
||||
&BranchPersistenceData {
|
||||
base_branch: Some(current_branch.to_owned()),
|
||||
base_commit: Some((
|
||||
current_commit.id().to_string(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
)),
|
||||
stack_tag: Some(new_stack_identifier),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
(_, _) => panic!("Invalid subcommand."),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use futures::future::join_all;
|
||||
use regex::Regex;
|
||||
use std::error::Error;
|
||||
|
||||
use crate::api::pull_request;
|
||||
use crate::graph::FlatDep;
|
||||
|
@ -45,7 +44,7 @@ pub async fn persist(
|
|||
table: &str,
|
||||
c: &Credentials,
|
||||
prefix: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
) -> eyre::Result<()> {
|
||||
let futures = prs.iter().map(|(pr, _)| {
|
||||
let body = table.replace(&pr.title()[..], &format!("👉 {}", pr.title())[..]);
|
||||
let body = remove_title_prefixes(body, prefix);
|
||||
|
|
|
@ -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