use git2::Repository;
use std::collections::HashMap;
use std::env;
use console::style;
use std::error::Error;
use clap::{Arg, App, SubCommand, AppSettings};
use std::rc::Rc;
use gh_stack::api::search::PullRequest;
use gh_stack::graph::FlatDep;
use gh_stack::Credentials;
use gh_stack::{api, git, graph, markdown, persist};
use gh_stack::util::loop_until_confirm;
fn clap<'a, 'b>() -> App<'a, 'b> {
let identifier = Arg::with_name("identifier")
.index(1)
.required(true)
.help("All pull requests containing this identifier in their title form a stack");
let annotate = SubCommand::with_name("annotate")
.about("Annotate the descriptions of all PRs in a stack with metadata about all PRs in the stack")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(identifier.clone())
.arg(Arg::with_name("prelude")
.long("prelude")
.short("p")
.value_name("FILE")
.help("Prepend the annotation with the contents of this file"));
let log = SubCommand::with_name("log")
.about("Print a list of all pull requests in a stack to STDOUT")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(identifier.clone());
let autorebase = SubCommand::with_name("autorebase")
.about("Rebuild a stack based on changes to local branches and mirror these changes up to the remote")
.arg(Arg::with_name("remote")
.long("remote")
.short("r")
.value_name("REMOTE")
.help("Name of the remote to (force-)push the updated stack to (default: `origin`)"))
.arg(Arg::with_name("repo")
.long("repo")
.short("C")
.value_name("PATH_TO_REPO")
.help("Path to a local copy of the repository"))
.arg(Arg::with_name("boundary")
.long("initial-cherry-pick-boundary")
.short("b")
.value_name("SHA")
.help("Stop the initial cherry-pick at this SHA (exclusive)"))
.setting(AppSettings::ArgRequiredElseHelp)
.arg(identifier.clone());
let rebase = SubCommand::with_name("rebase")
.about("Print a bash script to STDOUT that can rebase/update the stack (with a little help)")
.setting(AppSettings::ArgRequiredElseHelp)
.arg(identifier.clone());
let app = App::new("gh-stack")
.setting(AppSettings::SubcommandRequiredElseHelp)
.setting(AppSettings::DisableVersion)
.setting(AppSettings::VersionlessSubcommands)
.setting(AppSettings::DisableHelpSubcommand)
.subcommand(annotate)
.subcommand(log)
.subcommand(rebase)
.subcommand(autorebase);
app
}
async fn build_pr_stack(pattern: &str, credentials: &Credentials) -> Result<FlatDep, Box<dyn Error>> {
let prs = api::search::fetch_pull_requests_matching(pattern, &credentials).await?;
let prs = prs
.into_iter()
.map(Rc::new)
.collect::<Vec<Rc<PullRequest>>>();
let graph = graph::build(&prs);
let stack = graph::log(&graph);
Ok(stack)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let env: HashMap<String, String> = env::vars().collect();
let token = env
.get("GHSTACK_OAUTH_TOKEN")
.expect("You didn't pass `GHSTACK_OAUTH_TOKEN`");
let credentials = Credentials::new(token);
let matches = clap().get_matches();
match matches.subcommand() {
("annotate", Some(m)) => {
let identifier = m.value_of("identifier").unwrap();
let stack = build_pr_stack(identifier, &credentials).await?;
let table = markdown::build_table(&stack, identifier, m.value_of("prelude"));
for (pr, _) in stack.iter() {
println!("{}: {}", pr.number(), pr.title());
}
loop_until_confirm("Going to update these PRs ☝️ ");
persist::persist(&stack, &table, &credentials).await?;
println!("Done!");
}
("log", Some(m)) => {
let identifier = m.value_of("identifier").unwrap();
let stack = build_pr_stack(identifier, &credentials).await?;
for (pr, maybe_parent) in stack {
match maybe_parent {
Some(parent) => {
let into = style(format!("(Merges into #{})", parent.number())).green();
println!("#{}: {} {}", pr.number(), pr.title(), into);
}
None => {
let into = style("(Base)").red();
println!("#{}: {} {}", pr.number(), pr.title(), into);
}
}
}
}
("rebase", Some(m)) => {
let identifier = m.value_of("identifier").unwrap();
let stack = build_pr_stack(identifier, &credentials).await?;
let script = git::generate_rebase_script(stack);
println!("{}", script);
}
("autorebase", Some(m)) => {
let identifier = m.value_of("identifier").unwrap();
let stack = build_pr_stack(identifier, &credentials).await?;
let repo = m.value_of("repo").expect("The --repo argument is required.");
let repo = Repository::open(repo)?;
let remote = m.value_of("remote").unwrap_or("origin");
let remote = repo.find_remote(remote).unwrap();
git::perform_rebase(stack, &repo, remote.name().unwrap(), m.value_of("boundary")).await?;
println!("All done!");
}
(_, _) => panic!("Invalid subcommand.")
}
Ok(())
}