big overhaul of the command-line interface (+ tiny refactor)
This commit is contained in:
parent
359f96fd2d
commit
17322a0bab
|
@ -9,12 +9,32 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi_term"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi 0.3.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62"
|
checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atty"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"winapi 0.3.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -60,6 +80,21 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "2.33.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"atty",
|
||||||
|
"bitflags",
|
||||||
|
"strsim",
|
||||||
|
"textwrap",
|
||||||
|
"unicode-width",
|
||||||
|
"vec_map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
@ -278,6 +313,8 @@ dependencies = [
|
||||||
name = "gh-stack"
|
name = "gh-stack"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"console",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
|
@ -1004,6 +1041,12 @@ dependencies = [
|
||||||
"winapi 0.3.8",
|
"winapi 0.3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.30"
|
version = "1.0.30"
|
||||||
|
@ -1048,6 +1091,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textwrap"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -1194,6 +1246,12 @@ version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c"
|
checksum = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vec_map"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
|
|
@ -21,3 +21,5 @@ petgraph = "0.5"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
git2 = "0.13"
|
git2 = "0.13"
|
||||||
dialoguer = "0.6.2"
|
dialoguer = "0.6.2"
|
||||||
|
clap = "2.33"
|
||||||
|
console = "0.11"
|
|
@ -14,10 +14,11 @@ I use this tool to help managed stacked pull requests on Github, which are notor
|
||||||
|
|
||||||
This tool assumes that:
|
This tool assumes that:
|
||||||
|
|
||||||
- All PRs in a single "stack" all have a unique identifier in their title (I typically use a Jira ticket number for this). It then looks for all PRs containing this containing this identifier and builds a dependency graph in memory. This can technically support a "branched stack" instead of a single chain, but I haven't really tried the latter style.
|
- All PRs in a single "stack" all have a unique identifier in their title (I typically use a Jira ticket number for this).
|
||||||
|
- All PRs in the stack live in a single GitHub repository.
|
||||||
- All remote branches that these PRs represent have local branches named identically.
|
- All remote branches that these PRs represent have local branches named identically.
|
||||||
|
|
||||||
With this graph built up, the tool can:
|
WIt then looks for all PRs containing this containing this identifier and builds a dependency graph in memory. This can technically support a "branched stack" instead of a single chain, but I haven't really tried the latter style. With this graph built up, the tool can:
|
||||||
|
|
||||||
- Add a markdown table to the PR description (idempotently) of each PR in the stack describing _all_ PRs in the stack.
|
- Add a markdown table to the PR description (idempotently) of each PR in the stack describing _all_ PRs in the stack.
|
||||||
- Log a simple list of all PRs in the stack (+ dependencies) to stdout.
|
- Log a simple list of all PRs in the stack (+ dependencies) to stdout.
|
||||||
|
|
16
src/git.rs
16
src/git.rs
|
@ -1,6 +1,6 @@
|
||||||
use crate::api::search::PullRequestStatus;
|
use crate::api::search::PullRequestStatus;
|
||||||
use crate::graph::FlatDep;
|
use crate::graph::FlatDep;
|
||||||
use dialoguer::Input;
|
use crate::util::loop_until_confirm;
|
||||||
use git2::build::CheckoutBuilder;
|
use git2::build::CheckoutBuilder;
|
||||||
use git2::{
|
use git2::{
|
||||||
CherrypickOptions,
|
CherrypickOptions,
|
||||||
|
@ -16,20 +16,6 @@ fn remote_ref(remote: &str, git_ref: &str) -> String {
|
||||||
format!("{}/{}", remote, git_ref)
|
format!("{}/{}", remote, git_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn loop_until_confirm(prompt: &str) {
|
|
||||||
let prompt = format!("{} Type 'yes' to continue", prompt);
|
|
||||||
loop {
|
|
||||||
let result = Input::<String>::new()
|
|
||||||
.with_prompt(&prompt)
|
|
||||||
.interact()
|
|
||||||
.unwrap();
|
|
||||||
match &result[..] {
|
|
||||||
"yes" => return,
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For all open pull requests in the graph, generate a series of commands
|
/// For all open pull requests in the graph, generate a series of commands
|
||||||
/// (force-pushes) that will rebase the entire stack. The "PREBASE" variable
|
/// (force-pushes) that will rebase the entire stack. The "PREBASE" variable
|
||||||
/// is a base for the first branch in the stack (essentially a "stop cherry-picking
|
/// is a base for the first branch in the stack (essentially a "stop cherry-picking
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod git;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
pub mod persist;
|
pub mod persist;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
// Personal access token
|
// Personal access token
|
||||||
|
|
180
src/main.rs
180
src/main.rs
|
@ -1,113 +1,152 @@
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use console::style;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use clap::{Arg, App, SubCommand, AppSettings};
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::process;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gh_stack::api::search::PullRequest;
|
use gh_stack::api::search::PullRequest;
|
||||||
|
use gh_stack::graph::FlatDep;
|
||||||
use gh_stack::Credentials;
|
use gh_stack::Credentials;
|
||||||
use gh_stack::{api, git, graph, markdown, persist};
|
use gh_stack::{api, git, graph, markdown, persist};
|
||||||
|
use gh_stack::util::loop_until_confirm;
|
||||||
|
|
||||||
pub fn read_cli_input(message: &str) -> String {
|
fn clap<'a, 'b>() -> App<'a, 'b> {
|
||||||
print!("{}", message);
|
let identifier = Arg::with_name("identifier")
|
||||||
io::stdout().flush().unwrap();
|
.index(1)
|
||||||
|
.required(true)
|
||||||
|
.help("All pull requests containing this identifier in their title form a stack");
|
||||||
|
|
||||||
let mut buf = String::new();
|
let annotate = SubCommand::with_name("annotate")
|
||||||
io::stdin().read_line(&mut buf).unwrap();
|
.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"));
|
||||||
|
|
||||||
buf.trim().to_owned()
|
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"))
|
||||||
|
.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
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_final_output(prelude_path: &str, tail: &str) -> String {
|
async fn build_pr_stack(pattern: &str, credentials: &Credentials) -> Result<FlatDep, Box<dyn Error>> {
|
||||||
let prelude = fs::read_to_string(prelude_path).unwrap();
|
let prs = api::search::fetch_pull_requests_matching(pattern, &credentials).await?;
|
||||||
let mut out = String::new();
|
let prs = prs
|
||||||
|
.into_iter()
|
||||||
out.push_str(&prelude);
|
.map(Rc::new)
|
||||||
out.push_str("\n");
|
.collect::<Vec<Rc<PullRequest>>>();
|
||||||
out.push_str(&tail);
|
let graph = graph::build(&prs);
|
||||||
|
let stack = graph::log(&graph);
|
||||||
out
|
Ok(stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let env: HashMap<String, String> = env::vars().collect();
|
let env: HashMap<String, String> = env::vars().collect();
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
if args.len() > 4 {
|
|
||||||
println!("usage: gh-stack <command=save|log|rebase> <pattern> <prelude_filename?>");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let command = &args[1][..];
|
|
||||||
let pattern = &args[2];
|
|
||||||
let prelude = args.get(3);
|
|
||||||
|
|
||||||
let token = env
|
let token = env
|
||||||
.get("GHSTACK_OAUTH_TOKEN")
|
.get("GHSTACK_OAUTH_TOKEN")
|
||||||
.expect("You didn't pass `GHSTACK_OAUTH_TOKEN`");
|
.expect("You didn't pass `GHSTACK_OAUTH_TOKEN`");
|
||||||
|
|
||||||
let credentials = Credentials::new(token);
|
let credentials = Credentials::new(token);
|
||||||
|
let matches = clap().get_matches();
|
||||||
|
|
||||||
let prs = api::search::fetch_pull_requests_matching(&pattern, &credentials).await?;
|
match matches.subcommand() {
|
||||||
let prs = prs
|
("annotate", Some(m)) => {
|
||||||
.into_iter()
|
let identifier = m.value_of("identifier").unwrap();
|
||||||
.map(Rc::new)
|
let stack = build_pr_stack(identifier, &credentials).await?;
|
||||||
.collect::<Vec<Rc<PullRequest>>>();
|
let table = markdown::build_table(&stack, identifier, m.value_of("prelude"));
|
||||||
let tree = graph::build(&prs);
|
|
||||||
|
|
||||||
match command {
|
for (pr, _) in stack.iter() {
|
||||||
"github" => {
|
|
||||||
let table = markdown::build_table(graph::log(&tree), pattern);
|
|
||||||
|
|
||||||
let output = match prelude {
|
|
||||||
Some(prelude) => build_final_output(prelude, &table),
|
|
||||||
None => table,
|
|
||||||
};
|
|
||||||
|
|
||||||
for pr in prs.iter() {
|
|
||||||
println!("{}: {}", pr.number(), pr.title());
|
println!("{}: {}", pr.number(), pr.title());
|
||||||
}
|
}
|
||||||
|
loop_until_confirm("Going to update these PRs ☝️ ");
|
||||||
|
|
||||||
let response = read_cli_input("Going to update these PRs ☝️ (y/n): ");
|
persist::persist(&stack, &table, &credentials).await?;
|
||||||
match &response[..] {
|
|
||||||
"y" => persist::persist(&prs, &output, &credentials).await?,
|
|
||||||
_ => std::process::exit(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Done!");
|
println!("Done!");
|
||||||
}
|
}
|
||||||
|
|
||||||
"rebase" => {
|
("log", Some(m)) => {
|
||||||
let deps = graph::log(&tree);
|
let identifier = m.value_of("identifier").unwrap();
|
||||||
let script = git::generate_rebase_script(deps);
|
let stack = build_pr_stack(identifier, &credentials).await?;
|
||||||
println!("{}", script);
|
|
||||||
}
|
|
||||||
|
|
||||||
"autorebase" => {
|
for (pr, maybe_parent) in stack {
|
||||||
let deps = graph::log(&tree);
|
|
||||||
let repo = Repository::open(prelude.unwrap()).unwrap();
|
|
||||||
// TODO: Make this configurable
|
|
||||||
let remote = repo.find_remote("heap").unwrap();
|
|
||||||
git::perform_rebase(deps, &repo, remote.name().unwrap()).await?;
|
|
||||||
println!("All done!");
|
|
||||||
}
|
|
||||||
|
|
||||||
"log" => {
|
|
||||||
let log = graph::log(&tree);
|
|
||||||
for (pr, maybe_parent) in log {
|
|
||||||
match maybe_parent {
|
match maybe_parent {
|
||||||
Some(parent) => println!("{} → {}", pr.head(), parent.head()),
|
Some(parent) => {
|
||||||
None => println!("{} → N/A", pr.head()),
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => panic!("Invalid command!"),
|
("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").unwrap();
|
||||||
|
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()).await?;
|
||||||
|
println!("All done!");
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, _) => panic!("Invalid subcommand.")
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
/*
|
/*
|
||||||
|
@ -120,6 +159,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
- [x] Accept a prelude via STDIN
|
- [x] Accept a prelude via STDIN
|
||||||
- [x] Log a textual representation of the graph
|
- [x] Log a textual representation of the graph
|
||||||
- [x] Automate rebase
|
- [x] Automate rebase
|
||||||
|
- [x] Better CLI args
|
||||||
- [ ] Build status icons
|
- [ ] Build status icons
|
||||||
- [ ] Panic on non-200s
|
- [ ] Panic on non-200s
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use crate::api::search::PullRequestStatus;
|
use crate::api::search::PullRequestStatus;
|
||||||
use crate::graph::FlatDep;
|
use crate::graph::FlatDep;
|
||||||
|
@ -9,12 +10,19 @@ fn process(row: String) -> String {
|
||||||
regex.replace_all(&row, "").into_owned()
|
regex.replace_all(&row, "").into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_table(deps: FlatDep, title: &str) -> String {
|
pub fn build_table(deps: &FlatDep, title: &str, prelude_path: Option<&str>) -> String {
|
||||||
let is_complete = deps
|
let is_complete = deps
|
||||||
.iter()
|
.iter()
|
||||||
.all(|(node, _)| node.state() == &PullRequestStatus::Closed);
|
.all(|(node, _)| node.state() == &PullRequestStatus::Closed);
|
||||||
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
|
|
||||||
|
if let Some(prelude_path) = prelude_path {
|
||||||
|
let prelude = fs::read_to_string(prelude_path).unwrap();
|
||||||
|
out.push_str(&prelude);
|
||||||
|
out.push_str("\n");
|
||||||
|
}
|
||||||
|
|
||||||
if is_complete {
|
if is_complete {
|
||||||
out.push_str(&format!("### ✅ Stacked PR Chain: {}\n", title));
|
out.push_str(&format!("### ✅ Stacked PR Chain: {}\n", title));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::api::pull_request;
|
use crate::api::pull_request;
|
||||||
use crate::api::search::PullRequest;
|
use crate::graph::FlatDep;
|
||||||
use crate::Credentials;
|
use crate::Credentials;
|
||||||
|
|
||||||
const SHIELD_OPEN: &str = "<!---GHSTACKOPEN-->";
|
const SHIELD_OPEN: &str = "<!---GHSTACKOPEN-->";
|
||||||
|
@ -29,11 +28,11 @@ fn safe_replace(body: &str, table: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn persist(
|
pub async fn persist(
|
||||||
prs: &[Rc<PullRequest>],
|
prs: &FlatDep,
|
||||||
table: &str,
|
table: &str,
|
||||||
c: &Credentials,
|
c: &Credentials,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let futures = prs.iter().map(|pr| {
|
let futures = prs.iter().map(|(pr, _)| {
|
||||||
let description = safe_replace(pr.body(), table);
|
let description = safe_replace(pr.body(), table);
|
||||||
pull_request::update_description(description, pr.clone(), c)
|
pull_request::update_description(description, pr.clone(), c)
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
use dialoguer::Input;
|
||||||
|
|
||||||
|
pub fn loop_until_confirm(prompt: &str) {
|
||||||
|
let prompt = format!("{} Type 'yes' to continue", prompt);
|
||||||
|
loop {
|
||||||
|
let result = Input::<String>::new()
|
||||||
|
.with_prompt(&prompt)
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
match &result[..] {
|
||||||
|
"yes" => return,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue