Repository: drewwyatt/git-tidy
Branch: main
Commit: 270f8aa8f59b
Files: 14
Total size: 17.1 KB
Directory structure:
gitextract_8sk2voo7/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .vscode/
│ └── launch.json
├── Cargo.toml
├── LICENSE
├── README.md
└── src/
├── git/
│ ├── mod.rs
│ └── models.rs
├── i18n.rs
├── main.rs
└── prompt.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
FROM rust:1
# This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in
# devcontainer.json to use it. More info: https://aka.ms/vscode-remote/containers/non-root-user.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies.
RUN apt-get update \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
#
# Verify git, needed tools installed
&& apt-get -y install git openssh-client cmake less iproute2 procps lsb-release \
#
# Install lldb, vadimcn.vscode-lldb VSCode extension dependencies
&& apt-get install -y lldb python3-minimal libpython3.7 \
#
# Install Rust components
&& rustup update 2>&1 \
&& rustup component add rls rust-analysis rust-src rustfmt clippy 2>&1 \
#
# Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
&& groupadd --gid $USER_GID $USERNAME \
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
# [Optional] Add sudo support for the non-root user
&& apt-get install -y sudo \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\
&& chmod 0440 /etc/sudoers.d/$USERNAME \
#
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update \
# && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "Rust",
"dockerFile": "Dockerfile",
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"lldb.executable": "/usr/bin/lldb",
// VS Code don't watch files under ./target
"files.watcherExclude": {
"**/target/**": true
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"rust-lang.rust",
"bungcip.better-toml",
"vadimcn.vscode-lldb",
// (Optional) Displays the current CPU stats, memory/disk consumption, clock freq. etc. of the container host in the VS Code status bar.
"mutantdino.resourcemonitor"
]
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "rustc --version",
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
}
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "10:00"
open-pull-requests-limit: 10
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
push:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
cargo:
runs-on: ubuntu-latest
strategy:
matrix:
command: [fmt, build]
include:
- command: fmt
args: -- --check
- command: build
args: --verbose
steps:
- uses: actions/checkout@v2
- name: Setup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run ${{ matrix.command }}
uses: actions-rs/cargo@v1
with:
command: ${{ matrix.command }}
args: ${{ matrix.args }}
================================================
FILE: .gitignore
================================================
dist/
gitclean
git-tidy
*.snap
coverage.txt
#Added by cargo
/target
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'git-tidy'",
"cargo": {
"args": [
"build",
"--bin=git-tidy",
"--package=git-tidy"
],
"filter": {
"name": "git-tidy",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'git-tidy'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=git-tidy",
"--package=git-tidy"
],
"filter": {
"name": "git-tidy",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
================================================
FILE: Cargo.toml
================================================
[package]
name = "git-tidy"
description = "Tidy up stale git branches."
version = "2.0.1"
authors = ["Drew Wyatt <drew.j.wyatt@gmail.com>",
"Dalton Claybrook <dalton.claybrook@gmail.com>"]
license = "MIT"
edition = "2018"
repository = "https://github.com/drewwyatt/git-tidy"
keywords = ["git", "cli", "command-line", "command-line-tool"]
categories = ["command-line-utilities"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dialoguer = "0.7.1"
indicatif = "0.15.0"
regex = "1"
structopt = { version = "0.3" }
================================================
FILE: LICENSE
================================================
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🗑 git-tidy
[](https://crates.io/crates/git-tidy)
Tidy up stale git branches.
[](https://asciinema.org/a/389715)
## Installation
### Homebrew
```bash
$ brew tap drewwyatt/tap
$ brew install git-tidy
```
### Cargo
```bash
$ cargo install git-tidy
```
#### ⚠️ You may need to update `cargo` for this ⚠️
If you are seeing an error like the one in [this issue](https://github.com/drewwyatt/git-tidy/issues/45):
```
▪ cargo install git-tidy
Updating crates.io index
Installing git-tidy v2.0.1
error: failed to compile `git-tidy v2.0.1`, intermediate artifacts can be found at `/tmp/cargo-installgtcftB`
Caused by:
failed to select a version for the requirement `zeroize = "^0.9.3"`
candidate versions found which didn't match: 1.3.0, 1.2.0, 1.1.1, ...
location searched: crates.io index
required by package `dialoguer v0.7.1`
... which is depended on by `git-tidy v2.0.1`
```
You can probably fix this by updating cargo with:
```sh
rustup update
```
### Previous versions
Newer versions of `git-tidy` are (for now) only available from Homebrew and [crates.io](https://crates.io/crates/git-tidy), but you can still get `1.0.0` from the following places:
#### Snapcraft
```bash
$ sudo snap install git-tidy
```
#### Go
```bash
$ go get -u github.com/drewwyatt/git-tidy
```
## Usage
```bash
$ git tidy # executes "git branch -d" on ": gone" branches
```
### With force delete
```bash
$ git tidy -f # same as above, but with "-D" instead of "-d"
# or
$ git tidy --force
```
### Interactive
Present all stale (": gone") branches in a checkbox list, allowing user to opt-in to deletions.
```bash
$ git tidy -i
# or
$ git tidy --interactive
# with force
$ git tidy -if
# or
$ git tidy --interactive --force
```
================================================
FILE: src/git/mod.rs
================================================
pub mod models;
use regex::Regex;
use models::{GitError, GitExec};
pub struct Git<F>
where
F: Fn(&str) -> (),
{
gone_branch_regex: Regex,
output: Option<String>,
_report_progress: F,
}
impl<F> Git<F>
where
F: Fn(&str) -> (),
{
pub fn new(report_progress: F) -> Self {
Git {
_report_progress: report_progress,
output: None,
gone_branch_regex: Regex::new(
r"(?m)^(?:\*| ) ([^\s]+)\s+[a-z0-9]+ \[[^:\n]+: gone\].*$",
)
.unwrap(),
}
}
pub fn delete(&mut self, force: bool, branch_name: &str) -> Result<&mut Self, GitError> {
// TODO: figure out how to prevent getting the delete arg in 2 places
let delete_arg = if force { "-D" } else { "-d" };
self.report_progress(&format!(
"running 'git branch {} {}'",
delete_arg, branch_name
));
self.output = Some(GitExec::delete(force, branch_name)?);
Ok(self)
}
pub fn fetch(&mut self) -> Result<&mut Self, GitError> {
self.report_progress("running 'git fetch'...");
self.output = Some(GitExec::fetch()?);
Ok(self)
}
pub fn prune(&mut self) -> Result<&mut Self, GitError> {
self.report_progress("running 'git remote prune origin'...");
self.output = Some(GitExec::prune()?);
Ok(self)
}
pub fn list_branches(&mut self) -> Result<&mut Self, GitError> {
self.report_progress("running 'git branch -vv'...");
self.output = Some(GitExec::list_branches()?);
Ok(self)
}
pub fn branch_names(&mut self) -> Result<Vec<String>, GitError> {
self.output
.as_ref()
.map(|str| {
self.gone_branch_regex
.captures_iter(&str)
.map(|cap| String::from(&cap[1]))
.collect::<Vec<String>>()
})
.ok_or(GitError::UnknownError)
}
fn report_progress(&mut self, message: &str) {
let rp = &self._report_progress;
rp(message);
}
}
================================================
FILE: src/git/models.rs
================================================
#[derive(Debug)]
pub enum GitError {
CommandError(String),
ExecError(String),
ParseError(String),
UnknownError,
}
impl From<std::io::Error> for GitError {
fn from(err: std::io::Error) -> Self {
Self::ExecError(err.to_string())
}
}
impl From<std::string::FromUtf8Error> for GitError {
fn from(err: std::string::FromUtf8Error) -> Self {
Self::ParseError(err.to_string())
}
}
impl From<std::process::Output> for GitError {
fn from(output: std::process::Output) -> Self {
String::from_utf8(output.stderr)
.map(Self::CommandError)
.unwrap_or(Self::UnknownError)
}
}
use std::process::Command;
pub struct GitExec {}
impl GitExec {
pub fn delete(force: bool, branch_name: &str) -> Result<String, GitError> {
let delete_arg = if force { "-D" } else { "-d" };
let output = Command::new("git")
.arg("branch")
.arg(delete_arg)
.arg(branch_name)
.output()?;
if output.status.success() {
return Ok(String::from_utf8(output.stdout)?);
}
Err(GitError::from(output))
}
pub fn fetch() -> Result<String, GitError> {
let output = Command::new("git").arg("fetch").output()?;
if output.status.success() {
return Ok(String::from_utf8(output.stdout)?);
}
Err(GitError::from(output))
}
pub fn prune() -> Result<String, GitError> {
let output = Command::new("git")
.arg("remote")
.arg("prune")
.arg("origin")
.output()?;
if output.status.success() {
return Ok(String::from_utf8(output.stdout)?);
}
Err(GitError::from(output))
}
pub fn list_branches() -> Result<String, GitError> {
let output = Command::new("git").arg("branch").arg("-vv").output()?;
if output.status.success() {
return Ok(String::from_utf8(output.stdout)?);
}
Err(GitError::from(output))
}
}
================================================
FILE: src/i18n.rs
================================================
use std::string::ToString;
pub enum Text<'a> {
BranchesDeleted,
BranchesToDelete,
DeletingBranch(&'a str),
DeletingBranches,
DryRunEnabled,
FinishedWithErrors,
NoBranchesDeleted,
NothingToDo,
StartupMessage,
UnknownErrorEncountered,
}
impl<'a> ToString for Text<'a> {
fn to_string(&self) -> String {
match self {
Text::BranchesDeleted => "Branches deleted:".into(),
Text::BranchesToDelete => "Branches to delete:".into(),
Text::DeletingBranch(name) => format!("Deleting ‘{}’...", name),
Text::DeletingBranches => "Deleting branches...".into(),
Text::DryRunEnabled => {
"\n📣 NOTE: --dry-run enabled, no branches will be deleted.\n".into()
}
Text::FinishedWithErrors => "Finished with errors:".into(),
Text::NoBranchesDeleted => "No branches were deleted.".into(),
Text::NothingToDo => "Nothing to do!".into(),
Text::StartupMessage => "Tidying up...".into(),
Text::UnknownErrorEncountered => "An unknown error was encountered".into(),
}
}
}
================================================
FILE: src/main.rs
================================================
mod git;
mod i18n;
mod prompt;
use indicatif::ProgressBar;
use structopt::StructOpt;
use git::models::GitError;
use git::Git;
use i18n::Text;
use prompt::Prompt;
#[derive(StructOpt)]
#[structopt(
about = "Tidy up stale git branches.",
author = "Drew Wyatt <drew.j.wyatt@gmail.com>",
name = "git-tidy"
)]
struct Cli {
#[structopt(
short,
long,
help = "Allow deleting branches irrespective of their apparent merged status"
)]
force: bool,
#[structopt(
short,
long,
help = r#"Present all ": gone" branches in list form, allowing opt-in to deletions"#
)]
interactive: bool,
#[structopt(short, long, help = "Print output, but don't delete any branches")]
dry_run: bool,
}
fn main() -> Result<(), GitError> {
let args = Cli::from_args();
if args.dry_run {
println!("{}", Text::DryRunEnabled.to_string());
}
let spinner = ProgressBar::new_spinner();
spinner.set_message(&Text::StartupMessage.to_string());
spinner.enable_steady_tick(160);
let mut git = Git::new(|m| spinner.set_message(m));
let gone_branches = git.fetch()?.prune()?.list_branches()?.branch_names()?;
if gone_branches.is_empty() {
spinner.finish_with_message(&Text::NothingToDo.to_string());
return Ok(());
}
spinner.finish_and_clear();
let mut stale_branches = gone_branches;
if args.interactive {
stale_branches = Prompt::with(stale_branches);
if stale_branches.is_empty() {
println!("{}", Text::NothingToDo.to_string());
return Ok(());
}
}
if args.dry_run {
println!("{}", Text::BranchesToDelete.to_string());
for branch in stale_branches {
println!(" - {}", branch);
}
println!("")
} else {
let spinner = ProgressBar::new_spinner();
spinner.set_message(&Text::DeletingBranches.to_string());
spinner.enable_steady_tick(160);
let (deleted_branches, deletion_errors) =
stale_branches
.into_iter()
.fold((vec![], vec![]), |(mut del, mut err), branch_name| {
spinner.set_message(&Text::DeletingBranch(&branch_name).to_string());
match git.delete(args.force, &branch_name) {
Err(GitError::CommandError(msg)) => err.push((branch_name, msg)),
Err(GitError::ExecError(msg)) => err.push((branch_name, msg)),
Err(GitError::ParseError(msg)) => err.push((branch_name, msg)),
Err(GitError::UnknownError) => {
err.push((branch_name, Text::UnknownErrorEncountered.to_string()))
}
_ => del.push(branch_name),
};
(del, err)
});
spinner.finish_and_clear();
if deleted_branches.is_empty() {
println!("{}", Text::NoBranchesDeleted.to_string());
} else {
println!("{}", Text::BranchesDeleted.to_string());
for branch_name in deleted_branches {
println!(" - {}", branch_name);
}
}
if !deletion_errors.is_empty() {
println!("{}", Text::FinishedWithErrors.to_string());
for (branch_name, error) in deletion_errors {
println!(" - {}: {}", branch_name, error);
}
}
}
Ok(())
}
================================================
FILE: src/prompt.rs
================================================
use dialoguer::{theme::ColorfulTheme, MultiSelect};
pub struct Prompt {}
impl Prompt {
pub fn with(branches: Vec<String>) -> Vec<String> {
let mut branches = branches;
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt("Stale branches")
.items(&branches)
.interact()
.unwrap();
selections
.into_iter()
.map(|idx| branches.swap_remove(idx))
.rev() // sort back to ascending order
.collect()
}
}
gitextract_8sk2voo7/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .vscode/
│ └── launch.json
├── Cargo.toml
├── LICENSE
├── README.md
└── src/
├── git/
│ ├── mod.rs
│ └── models.rs
├── i18n.rs
├── main.rs
└── prompt.rs
SYMBOL INDEX (23 symbols across 5 files)
FILE: src/git/mod.rs
type Git (line 7) | pub struct Git<F>
function new (line 20) | pub fn new(report_progress: F) -> Self {
function delete (line 32) | pub fn delete(&mut self, force: bool, branch_name: &str) -> Result<&mut ...
function fetch (line 43) | pub fn fetch(&mut self) -> Result<&mut Self, GitError> {
function prune (line 49) | pub fn prune(&mut self) -> Result<&mut Self, GitError> {
function list_branches (line 55) | pub fn list_branches(&mut self) -> Result<&mut Self, GitError> {
function branch_names (line 61) | pub fn branch_names(&mut self) -> Result<Vec<String>, GitError> {
function report_progress (line 73) | fn report_progress(&mut self, message: &str) {
FILE: src/git/models.rs
type GitError (line 2) | pub enum GitError {
method from (line 10) | fn from(err: std::io::Error) -> Self {
method from (line 16) | fn from(err: std::string::FromUtf8Error) -> Self {
method from (line 22) | fn from(output: std::process::Output) -> Self {
type GitExec (line 31) | pub struct GitExec {}
method delete (line 34) | pub fn delete(force: bool, branch_name: &str) -> Result<String, GitErr...
method fetch (line 49) | pub fn fetch() -> Result<String, GitError> {
method prune (line 58) | pub fn prune() -> Result<String, GitError> {
method list_branches (line 71) | pub fn list_branches() -> Result<String, GitError> {
FILE: src/i18n.rs
type Text (line 3) | pub enum Text<'a> {
method to_string (line 17) | fn to_string(&self) -> String {
FILE: src/main.rs
type Cli (line 19) | struct Cli {
function main (line 38) | fn main() -> Result<(), GitError> {
FILE: src/prompt.rs
type Prompt (line 3) | pub struct Prompt {}
method with (line 6) | pub fn with(branches: Vec<String>) -> Vec<String> {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (19K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 1657,
"preview": "FROM rust:1\n\n# This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in\n# devcontainer"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 1120,
"preview": "{\n\t\"name\": \"Rust\",\n\t\"dockerFile\": \"Dockerfile\",\n\t\"runArgs\": [ \"--cap-add=SYS_PTRACE\", \"--security-opt\", \"seccomp=unconfi"
},
{
"path": ".github/dependabot.yml",
"chars": 145,
"preview": "version: 2\nupdates:\n- package-ecosystem: gomod\n directory: \"/\"\n schedule:\n interval: daily\n time: \"10:00\"\n open"
},
{
"path": ".github/workflows/ci.yml",
"chars": 610,
"preview": "name: CI\n\non:\n pull_request:\n push:\n branches: [\"main\"]\n\nenv:\n CARGO_TERM_COLOR: always\n\njobs:\n cargo:\n runs-o"
},
{
"path": ".gitignore",
"chars": 71,
"preview": "dist/\ngitclean\ngit-tidy\n*.snap\ncoverage.txt\n\n\n#Added by cargo\n\n/target\n"
},
{
"path": ".vscode/launch.json",
"chars": 1032,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n //"
},
{
"path": "Cargo.toml",
"chars": 588,
"preview": "[package]\nname = \"git-tidy\"\ndescription = \"Tidy up stale git branches.\"\nversion = \"2.0.1\"\nauthors = [\"Drew Wyatt <drew.j"
},
{
"path": "LICENSE",
"chars": 1036,
"preview": "MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associate"
},
{
"path": "README.md",
"chars": 1868,
"preview": "# 🗑 git-tidy\n\n[](https://crates.io/crates/git-ti"
},
{
"path": "src/git/mod.rs",
"chars": 2108,
"preview": "pub mod models;\n\nuse regex::Regex;\n\nuse models::{GitError, GitExec};\n\npub struct Git<F>\nwhere\n F: Fn(&str) -> (),\n{\n "
},
{
"path": "src/git/models.rs",
"chars": 2048,
"preview": "#[derive(Debug)]\npub enum GitError {\n CommandError(String),\n ExecError(String),\n ParseError(String),\n Unknow"
},
{
"path": "src/i18n.rs",
"chars": 1153,
"preview": "use std::string::ToString;\n\npub enum Text<'a> {\n BranchesDeleted,\n BranchesToDelete,\n DeletingBranch(&'a str),\n"
},
{
"path": "src/main.rs",
"chars": 3521,
"preview": "mod git;\nmod i18n;\nmod prompt;\n\nuse indicatif::ProgressBar;\nuse structopt::StructOpt;\n\nuse git::models::GitError;\nuse gi"
},
{
"path": "src/prompt.rs",
"chars": 555,
"preview": "use dialoguer::{theme::ColorfulTheme, MultiSelect};\n\npub struct Prompt {}\n\nimpl Prompt {\n pub fn with(branches: Vec<S"
}
]
About this extraction
This page contains the full source code of the drewwyatt/git-tidy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (17.1 KB), approximately 4.8k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.