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",
]
[[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"

View File

@ -23,4 +23,6 @@ git2 = "0.13"
dialoguer = "0.6.2"
clap = "2.33"
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(
pattern: &str,
credentials: &Credentials,
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
) -> eyre::Result<Vec<PullRequest>> {
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<Vec<PullRequest>, Box<dyn Error>> {
) -> eyre::Result<Vec<PullRequest>> {
let client = reqwest::Client::new();
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::{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<dyn Error>> {
) -> eyre::Result<()> {
let deps = deps
.iter()
.filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)

View File

@ -1,4 +1,5 @@
pub mod api;
pub mod filepersist;
pub mod git;
pub mod graph;
pub mod markdown;

View File

@ -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<String>,
) -> Result<FlatDep, Box<dyn Error>> {
) -> eyre::Result<FlatDep> {
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<String>,
) -> Result<FlatDep, Box<dyn Error>> {
) -> eyre::Result<FlatDep> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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."),
}

View File

@ -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<dyn Error>> {
) -> eyre::Result<()> {
let futures = prs.iter().map(|(pr, _)| {
let body = table.replace(&pr.title()[..], &format!("👉 {}", pr.title())[..]);
let body = remove_title_prefixes(body, prefix);