From 0b4aa64cc4aa1570429541be5354d502cb813bee Mon Sep 17 00:00:00 2001 From: Christoph Siedentop Date: Fri, 13 Nov 2020 22:27:26 -0800 Subject: [PATCH] Provide porcelain output for scripts. Options: * local branches * remote branches * JSON output. \#138 --- .prettierrc | 1 + CHANGELOG.md | 14 +- Cargo.lock | 47 +++++- Cargo.toml | 2 + src/args.rs | 39 +++++ src/branch.rs | 7 +- src/core.rs | 7 +- src/lib.rs | 1 + src/main.rs | 21 ++- src/porcelain_outputs.rs | 53 +++++++ tests/filter_accidential_track_porcelain.rs | 155 ++++++++++++++++++++ 11 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 .prettierrc create mode 100644 src/porcelain_outputs.rs create mode 100644 tests/filter_accidential_track_porcelain.rs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..350f022 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{ "proseWrap": "always", "tabWidth": 4 } diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e94ec..83f84b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Types of Change @@ -16,6 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `--porcelain` option to provide output for scripting without deleting any + branches. Options are `local`, `remote` or `json`. Those list local branches + that should be deleted, remote branches or all output in structured JSON. + The JSON can be further filtered with _jq_ or _gron_. + ### Changed -- Performance increase for big repos. Associating each local branch with all remotes is now multiple orders of magnitude faster. There are still bottlenecks that make the use on big repos impractically slow. +- Performance increase for big repos. Associating each local branch with all + remotes is now multiple orders of magnitude faster. There are still + bottlenecks that make the use on big repos impractically slow. diff --git a/Cargo.lock b/Cargo.lock index f0e3ac4..6ae217e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,6 +256,8 @@ dependencies = [ "rayon", "regex", "rson_rs", + "serde", + "serde_json", "tempfile", "textwrap 0.12.1", "thiserror", @@ -341,6 +343,12 @@ dependencies = [ "regex", ] +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + [[package]] name = "jobserver" version = "0.1.21" @@ -698,6 +706,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "scopeguard" version = "1.1.0" @@ -709,6 +723,31 @@ name = "serde" version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "strsim" @@ -763,9 +802,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a14cd9f8c72704232f0bfc8455c0e861f0ad4eb60cc9ec8a170e231414c1e13" +checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1" dependencies = [ "libc", "winapi", @@ -865,9 +904,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae" [[package]] name = "unicode-width" diff --git a/Cargo.toml b/Cargo.toml index dfb93b1..860f763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,8 @@ man = { version = "0.3.0", optional = true } rson_rs = { version = "0.2.1", optional = true } regex = { version = "1.4.2", optional = true } indicatif = { version = "0.15.0" , features = ["rayon"]} +serde = { version = "1.0.117", features = ["derive"] } +serde_json = "1.0.59" [dev-dependencies] tempfile = "3.1.0" diff --git a/src/args.rs b/src/args.rs index 4badf7a..c1512c1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -40,6 +40,10 @@ pub struct Args { #[clap(long, hidden(true))] pub update: bool, + /// Output for scripting. Options are "json" for full structured output or "local" or "remote" for a list of branches to be deleted. + #[clap(long)] + pub porcelain: Option, + /// Prevents too frequent updates. Seconds between updates in seconds. 0 to disable. /// [default: 5] [config: trim.updateInterval] #[clap(long)] @@ -127,6 +131,41 @@ fn exclusive_bool( } } +/// Configuration of --porcelain format. +#[derive(Debug)] +pub enum PorcelainFormat { + /// List of to-be-deleted local branches + LocalBranches, + /// List of remote branches to be deleted. + RemoteBranches, + /// Full structured JSON output + JSON, +} + +impl FromStr for PorcelainFormat { + type Err = PorcelainFormatParseError; + + fn from_str(s: &str) -> Result { + match s.trim() { + "" => Err(PorcelainFormatParseError { + message: "Porcelain format is empty".to_owned(), + }), + "json" => Ok(PorcelainFormat::JSON), + "local" | "l" => Ok(PorcelainFormat::LocalBranches), + "remote" | "r" => Ok(PorcelainFormat::RemoteBranches), + unknown => Err(PorcelainFormatParseError { + message: format!("Unknown porcelain format: {}", unknown), + }), + } + } +} + +#[derive(Error, Debug)] +#[error("{message}")] +pub struct PorcelainFormatParseError { + message: String, +} + #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub enum Scope { All, diff --git a/src/branch.rs b/src/branch.rs index 9274b7d..98898c7 100644 --- a/src/branch.rs +++ b/src/branch.rs @@ -3,6 +3,7 @@ use std::convert::TryFrom; use anyhow::{Context, Result}; use git2::{Branch, Config, Direction, Reference, Repository}; use log::*; +use serde::Serialize; use thiserror::Error; use crate::config; @@ -12,7 +13,7 @@ pub trait Refname { fn refname(&self) -> &str; } -#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone)] +#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone, Serialize)] pub struct LocalBranch { pub refname: String, } @@ -83,7 +84,7 @@ impl<'repo> TryFrom<&git2::Reference<'repo>> for LocalBranch { } } -#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone)] +#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone, Serialize)] pub struct RemoteTrackingBranch { pub refname: String, } @@ -202,7 +203,7 @@ pub enum RemoteTrackingBranchStatus { None, } -#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)] +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug, Serialize)] pub struct RemoteBranch { pub remote: String, pub refname: String, diff --git a/src/core.rs b/src/core.rs index 4685db0..64fe938 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,6 +6,7 @@ use anyhow::{Context, Result}; use git2::{BranchType, Config, Repository}; use log::*; use rayon::prelude::*; +use serde::Serialize; use crate::args::DeleteFilter; use crate::branch::{ @@ -19,12 +20,14 @@ use crate::{config, BaseSpec, Git}; use indicatif::ParallelProgressIterator; use rayon::iter::ParallelIterator; +#[derive(Serialize)] pub struct TrimPlan { pub skipped: HashMap, pub to_delete: HashSet, pub preserved: Vec, } +#[derive(Serialize)] pub struct Preserved { pub branch: ClassifiedBranch, pub reason: String, @@ -392,7 +395,7 @@ impl TrimPlan { } } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Serialize)] pub enum SkipSuggestion { Tracking, TrackingRemote(String), @@ -436,7 +439,7 @@ fn get_protect_pattern<'a, B: Refname>( Ok(None) } -#[derive(Hash, Eq, PartialEq, Debug, Clone)] +#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize)] pub enum ClassifiedBranch { MergedLocal(LocalBranch), Stray(LocalBranch), diff --git a/src/lib.rs b/src/lib.rs index 6279b2c..b9edd52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod branch; pub mod config; mod core; mod merge_tracker; +pub mod porcelain_outputs; mod simple_glob; mod subprocess; mod util; diff --git a/src/main.rs b/src/main.rs index 593265a..a809f68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,9 @@ use dialoguer::Confirm; use git2::{BranchType, Repository}; use log::*; -use git_trim::args::Args; +use git_trim::args::{Args, PorcelainFormat}; use git_trim::config::{self, get, Config, ConfigValue}; +use git_trim::porcelain_outputs::{print_json, print_local, print_remote}; use git_trim::{ delete_local_branches, delete_remote_branches, get_trim_plan, ls_remote_head, remote_update, ClassifiedBranch, ForceSendSync, Git, LocalBranch, PlanParam, RemoteHead, RemoteTrackingBranch, @@ -58,7 +59,23 @@ fn main(args: Args) -> Result<()> { }, )?; - print_summary(&plan, &git.repo)?; + match args.porcelain { + None => { + print_summary(&plan, &git.repo)?; + } + Some(PorcelainFormat::LocalBranches) => { + print_local(&plan, &git.repo, &mut std::io::stdout())?; + return Ok(()); + } + Some(PorcelainFormat::RemoteBranches) => { + print_remote(&plan, &git.repo, &mut std::io::stdout())?; + return Ok(()); + } + Some(PorcelainFormat::JSON) => { + print_json(&plan, &git.repo, &mut std::io::stdout())?; + return Ok(()); + } + } let locals = plan.locals_to_delete(); let remotes = plan.remotes_to_delete(&git.repo)?; diff --git a/src/porcelain_outputs.rs b/src/porcelain_outputs.rs new file mode 100644 index 0000000..414fe6f --- /dev/null +++ b/src/porcelain_outputs.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use git2::Repository; + +use crate::TrimPlan; + +/// Prints all locally to-be-deleted branches. +pub fn print_local( + plan: &TrimPlan, + _repo: &Repository, + mut writer: impl std::io::Write, +) -> Result<()> { + let mut merged_locals = Vec::new(); + for branch in &plan.to_delete { + if let Some(local) = branch.local() { + merged_locals.push(local.short_name().to_owned()); + } + } + + merged_locals.sort(); + for branch in merged_locals { + writeln!(writer, "{}", branch)?; + } + + Ok(()) +} + +/// Print all remotely to-be-deleted branches in the form "/" +pub fn print_remote( + plan: &TrimPlan, + repo: &Repository, + mut writer: impl std::io::Write, +) -> Result<()> { + let mut merged_remotes = Vec::new(); + for branch in &plan.to_delete { + if let Some(remote) = branch.remote(repo)? { + merged_remotes.push(remote); + } + } + + merged_remotes.sort(); + for branch in merged_remotes { + let branch_name = &branch.refname["/refs/heads".len()..]; + writeln!(writer, "{}/{}", branch.remote, branch_name)?; + } + + Ok(()) +} + +pub fn print_json(plan: &TrimPlan, _repo: &Repository, writer: impl std::io::Write) -> Result<()> { + serde_json::to_writer(writer, &plan)?; + + Ok(()) +} diff --git a/tests/filter_accidential_track_porcelain.rs b/tests/filter_accidential_track_porcelain.rs new file mode 100644 index 0000000..bbf4480 --- /dev/null +++ b/tests/filter_accidential_track_porcelain.rs @@ -0,0 +1,155 @@ +mod fixture; + +use std::convert::TryFrom; +use std::iter::FromIterator; + +use anyhow::Result; +use git2::Repository; + +use git_trim::args::{DeleteFilter, DeleteRange, Scope}; +use git_trim::{ + get_trim_plan, ClassifiedBranch, Git, LocalBranch, PlanParam, RemoteTrackingBranch, +}; + +use git_trim::porcelain_outputs::*; + +use fixture::{rc, test_default_param, Fixture}; + +fn fixture() -> Fixture { + rc().append_fixture_trace( + r#" + git init origin --bare + + git clone origin local + local < README.md + git add README.md + git commit -m "Initial commit" + git push -u origin master + EOF + + git clone origin contributer + within contributer < PlanParam<'static> { + PlanParam { + delete: DeleteFilter::from_iter(vec![ + DeleteRange::MergedLocal, + DeleteRange::MergedRemote(Scope::Scoped("origin".to_string())), + ]), + ..test_default_param() + } +} + +#[test] +fn test_default_config_tries_to_delete_accidential_track() -> Result<()> { + let guard = fixture().prepare( + "local", + r#" + local < Result<()> { + let guard = fixture().prepare( + "local", + r#" + local <