Add `branch` subcommand for starting a new branch
This commit is contained in:
parent
56494383dc
commit
de3a4beece
|
@ -168,6 +168,16 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
|
@ -326,12 +336,14 @@ dependencies = [
|
|||
"console",
|
||||
"dialoguer",
|
||||
"dotenv",
|
||||
"eyre",
|
||||
"futures",
|
||||
"git2",
|
||||
"petgraph",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -465,6 +477,12 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.2"
|
||||
|
|
|
@ -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"
|
|
@ -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,52 @@
|
|||
use eyre::Context;
|
||||
use git2::Repository;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod api;
|
||||
pub mod filepersist;
|
||||
pub mod git;
|
||||
pub mod graph;
|
||||
pub mod markdown;
|
||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -1,12 +1,16 @@
|
|||
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 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;
|
||||
|
@ -93,6 +97,22 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
|||
.arg(exclude.clone())
|
||||
.arg(identifier.clone());
|
||||
|
||||
let branch = SubCommand::with_name("branch")
|
||||
.about(
|
||||
"Create a new branch that is part of a stack."
|
||||
)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.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("git-stack")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.setting(AppSettings::DisableVersion)
|
||||
|
@ -101,7 +121,8 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
|||
.subcommand(annotate)
|
||||
.subcommand(log)
|
||||
.subcommand(rebase)
|
||||
.subcommand(autorebase);
|
||||
.subcommand(autorebase)
|
||||
.subcommand(branch);
|
||||
|
||||
app
|
||||
}
|
||||
|
@ -110,7 +131,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 +149,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 +183,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 +192,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();
|
||||
|
@ -306,6 +329,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);
|
||||
|
|
Loading…
Reference in New Issue