Add `branch` subcommand for starting a new branch

This commit is contained in:
Olivier 'reivilibre' 2022-12-02 20:17:47 +00:00
parent 56494383dc
commit de3a4beece
8 changed files with 169 additions and 13 deletions

18
Cargo.lock generated
View File

@ -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"

View File

@ -23,4 +23,6 @@ git2 = "0.13"
dialoguer = "0.6.2" 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"

View File

@ -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(

52
src/filepersist.rs Normal file
View File

@ -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(())
}

View File

@ -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)

View File

@ -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;

View File

@ -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, &current_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."),
} }

View File

@ -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);