diff --git a/Cargo.lock b/Cargo.lock index d611592..f1e582a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 3a45930..980ba44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,6 @@ git2 = "0.13" dialoguer = "0.6.2" clap = "2.33" console = "0.11" -dotenv = "0.15" \ No newline at end of file +dotenv = "0.15" +eyre = "0.6.8" +serde_json = "1.0.89" \ No newline at end of file diff --git a/src/api/search.rs b/src/api/search.rs index cf5714d..98a6fef 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -36,7 +36,7 @@ pub async fn fetch_reviews_for_pull_request( pub async fn fetch_pull_requests_matching( pattern: &str, credentials: &Credentials, -) -> Result, Box> { +) -> eyre::Result> { 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, Box> { - +) -> eyre::Result> { let client = reqwest::Client::new(); let request = api::base_request( diff --git a/src/filepersist.rs b/src/filepersist.rs new file mode 100644 index 0000000..6334cac --- /dev/null +++ b/src/filepersist.rs @@ -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, + pub base_commit: Option<(String, u64)>, + pub stack_tag: Option, +} + +pub fn find_persistence_path(repo: &Repository) -> eyre::Result { + 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 { + Ok(find_persistence_path(repo)?.join(branch)) +} + +pub fn load_persistence_data_for_branch( + repo: &Repository, + branch: &str, +) -> eyre::Result { + 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::(&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(()) +} diff --git a/src/git.rs b/src/git.rs index 961d6ae..5b9fb83 100644 --- a/src/git.rs +++ b/src/git.rs @@ -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> { +) -> eyre::Result<()> { let deps = deps .iter() .filter(|(dep, _)| *dep.state() == PullRequestStatus::Open) diff --git a/src/lib.rs b/src/lib.rs index 42d2a99..c401caa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod api; +pub mod filepersist; pub mod git; pub mod graph; pub mod markdown; diff --git a/src/main.rs b/src/main.rs index f032d10..46cfce3 100644 --- a/src/main.rs +++ b/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, -) -> Result> { +) -> eyre::Result { 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, -) -> Result> { +) -> eyre::Result { 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> { +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> { 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> { 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."), } diff --git a/src/persist.rs b/src/persist.rs index 87e34b3..552b5c1 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -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> { +) -> eyre::Result<()> { let futures = prs.iter().map(|(pr, _)| { let body = table.replace(&pr.title()[..], &format!("👉 {}", pr.title())[..]); let body = remove_title_prefixes(body, prefix);