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",
|
"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]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -326,12 +336,14 @@ dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -465,6 +477,12 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indenter"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.2"
|
version = "1.9.2"
|
||||||
|
|
|
@ -24,3 +24,5 @@ dialoguer = "0.6.2"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
console = "0.11"
|
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(
|
pub async fn fetch_pull_requests_matching(
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
|
) -> eyre::Result<Vec<PullRequest>> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let request = api::base_request(
|
let request = api::base_request(
|
||||||
|
@ -75,8 +75,7 @@ pub async fn fetch_matching_pull_requests_from_repository(
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
repository: &str,
|
repository: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
|
) -> eyre::Result<Vec<PullRequest>> {
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let request = api::base_request(
|
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::build::CheckoutBuilder;
|
||||||
use git2::{CherrypickOptions, Commit, Index, Oid, Repository, Revwalk, Sort};
|
use git2::{CherrypickOptions, Commit, Index, Oid, Repository, Revwalk, Sort};
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
fn remote_ref(remote: &str, git_ref: &str) -> String {
|
fn remote_ref(remote: &str, git_ref: &str) -> String {
|
||||||
|
@ -149,7 +148,7 @@ pub async fn perform_rebase(
|
||||||
remote: &str,
|
remote: &str,
|
||||||
boundary: Option<&str>,
|
boundary: Option<&str>,
|
||||||
ci: bool,
|
ci: bool,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> eyre::Result<()> {
|
||||||
let deps = deps
|
let deps = deps
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)
|
.filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod filepersist;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -1,12 +1,16 @@
|
||||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||||
use console::style;
|
use console::style;
|
||||||
|
use eyre::{bail, Context, ContextCompat};
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use git_stack::api::PullRequest;
|
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::graph::FlatDep;
|
||||||
use git_stack::util::loop_until_confirm;
|
use git_stack::util::loop_until_confirm;
|
||||||
use git_stack::Credentials;
|
use git_stack::Credentials;
|
||||||
|
@ -93,6 +97,22 @@ fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||||
.arg(exclude.clone())
|
.arg(exclude.clone())
|
||||||
.arg(identifier.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")
|
let app = App::new("git-stack")
|
||||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||||
.setting(AppSettings::DisableVersion)
|
.setting(AppSettings::DisableVersion)
|
||||||
|
@ -101,7 +121,8 @@ 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);
|
||||||
|
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
@ -110,7 +131,7 @@ async fn build_pr_stack(
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
exclude: Vec<String>,
|
exclude: Vec<String>,
|
||||||
) -> Result<FlatDep, Box<dyn Error>> {
|
) -> eyre::Result<FlatDep> {
|
||||||
let prs = api::search::fetch_pull_requests_matching(pattern, &credentials).await?;
|
let prs = api::search::fetch_pull_requests_matching(pattern, &credentials).await?;
|
||||||
|
|
||||||
let prs = prs
|
let prs = prs
|
||||||
|
@ -128,7 +149,7 @@ async fn build_pr_stack_for_repo(
|
||||||
repository: &str,
|
repository: &str,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
exclude: Vec<String>,
|
exclude: Vec<String>,
|
||||||
) -> Result<FlatDep, Box<dyn Error>> {
|
) -> eyre::Result<FlatDep> {
|
||||||
let prs = api::search::fetch_matching_pull_requests_from_repository(
|
let prs = api::search::fetch_matching_pull_requests_from_repository(
|
||||||
pattern,
|
pattern,
|
||||||
repository,
|
repository,
|
||||||
|
@ -162,7 +183,7 @@ fn remove_title_prefixes(title: String, prefix: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> eyre::Result<()> {
|
||||||
dotenv::from_filename(".gh-stack.env").ok();
|
dotenv::from_filename(".gh-stack.env").ok();
|
||||||
|
|
||||||
let token = env::var("GHSTACK_OAUTH_TOKEN").expect("You didn't pass `GHSTACK_OAUTH_TOKEN`");
|
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 credentials = Credentials::new(&token);
|
||||||
let matches = clap().get_matches();
|
let matches = clap().get_matches();
|
||||||
|
|
||||||
|
let repo = Repository::open(".").context("Couldn't open git repository; are you in one?")?;
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("annotate", Some(m)) => {
|
("annotate", Some(m)) => {
|
||||||
let identifier = m.value_of("identifier").unwrap();
|
let identifier = m.value_of("identifier").unwrap();
|
||||||
|
@ -306,6 +329,69 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
println!("All done!");
|
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."),
|
(_, _) => panic!("Invalid subcommand."),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use crate::api::pull_request;
|
use crate::api::pull_request;
|
||||||
use crate::graph::FlatDep;
|
use crate::graph::FlatDep;
|
||||||
|
@ -45,7 +44,7 @@ pub async fn persist(
|
||||||
table: &str,
|
table: &str,
|
||||||
c: &Credentials,
|
c: &Credentials,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> eyre::Result<()> {
|
||||||
let futures = prs.iter().map(|(pr, _)| {
|
let futures = prs.iter().map(|(pr, _)| {
|
||||||
let body = table.replace(&pr.title()[..], &format!("👉 {}", pr.title())[..]);
|
let body = table.replace(&pr.title()[..], &format!("👉 {}", pr.title())[..]);
|
||||||
let body = remove_title_prefixes(body, prefix);
|
let body = remove_title_prefixes(body, prefix);
|
||||||
|
|
Loading…
Reference in New Issue