From 41bd5a8bcbb97e6e69516d746f8de678606f097d Mon Sep 17 00:00:00 2001 From: Peter Luladjiev Date: Thu, 11 Sep 2025 14:41:34 +0300 Subject: [PATCH 1/4] Add support for external git subcommands Allow passing unknown subcommands to git as a fallback. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cli.rs | 4 ++++ src/commands.rs | 1 + src/commands/git_fallback.rs | 15 +++++++++++++++ src/main.rs | 5 ++++- 6 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/commands/git_fallback.rs diff --git a/Cargo.lock b/Cargo.lock index bb8cf84..b729e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,7 +198,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lgit" -version = "0.8.1" +version = "0.9.0" dependencies = [ "clap", "dialoguer", diff --git a/Cargo.toml b/Cargo.toml index ae12e01..21e375c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lgit" -version = "0.8.1" +version = "0.9.0" edition = "2021" authors = ["Peter Luladjiev"] description = "CLI tool for managing git repositories" diff --git a/src/cli.rs b/src/cli.rs index 19fdce7..8994b40 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(author, version, about)] +#[command(allow_external_subcommands = true)] pub struct Args { #[arg(short, long, default_value_t = false, help = "Verbose output")] pub verbose: bool, @@ -82,4 +83,7 @@ pub enum Commands { #[arg(short, long, default_value_t = 25, help = "Number of commits to show")] number: u32, }, + + #[command(external_subcommand)] + External(Vec), } diff --git a/src/commands.rs b/src/commands.rs index 50d2779..e687cdc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,6 +8,7 @@ pub mod checkout; pub mod cherry_pick; pub mod delete_branches; pub mod fixup; +pub mod git_fallback; pub mod rebase; pub trait Exec { diff --git a/src/commands/git_fallback.rs b/src/commands/git_fallback.rs new file mode 100644 index 0000000..e32c0d1 --- /dev/null +++ b/src/commands/git_fallback.rs @@ -0,0 +1,15 @@ +use crate::commands::Exec; + +pub fn run(cmd: &dyn Exec, args: &[String], verbose: bool) -> Result<(), &'static str> { + let str_args: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + + match cmd.exec(&str_args, verbose) { + Ok(output) => { + if !output.trim().is_empty() { + println!("{}", output.trim()); + } + Ok(()) + } + Err(_) => Err("Git command failed"), + } +} diff --git a/src/main.rs b/src/main.rs index 2a0f073..685d554 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ use clap::Parser; use crate::cli::{Args, Commands}; -use crate::commands::{autosquash, branch, checkout, cherry_pick, delete_branches, rebase, Cmd}; +use crate::commands::{ + autosquash, branch, checkout, cherry_pick, delete_branches, git_fallback, rebase, Cmd, +}; use crate::utils::get_base; mod cli; @@ -39,6 +41,7 @@ fn main() { Some(Commands::CherryPick { branch, number }) => { cherry_pick::run(&command, &branch, number, cli.verbose) } + Some(Commands::External(args)) => git_fallback::run(&command, &args, cli.verbose), None => Err("No command specified, please run with --help for more info"), }; From 322fddc6dfa72770a56fb52e733e57bb4f6db379 Mon Sep 17 00:00:00 2001 From: Peter Luladjiev Date: Thu, 11 Sep 2025 14:43:29 +0300 Subject: [PATCH 2/4] Document git command fallback feature in README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 5982c70..42c4aec 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ repositories. It provides a set of commands that streamline common git operation - **DeleteBranches**: Safely delete all branches for which remotes are gone. Use with caution! - **Fixup**: Commit as a fixup, simplifying your commit history. - **Rebase**: Rebase the current branch on top of freshly pulled BASE branch with a single command. +- **Git Command Fallback**: For any git command not directly supported by lgit, the tool will automatically pass the command through to git. ## Requirements @@ -59,6 +60,26 @@ Each command has a dedicated help page that can be accessed by running `lgit Date: Thu, 11 Sep 2025 14:54:53 +0300 Subject: [PATCH 3/4] Expand README with badges, usage, examples, and contribution guide --- README.md | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 343 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 42c4aec..6a85615 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,149 @@ # lgit-rs +[![Rust](https://github.com/Luladjiev/lgit-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/Luladjiev/lgit-rs/actions/workflows/rust.yml) +[![Crates.io](https://img.shields.io/crates/v/lgit.svg)](https://crates.io/crates/lgit) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Downloads](https://img.shields.io/crates/d/lgit.svg)](https://crates.io/crates/lgit) + `lgit-rs` is a powerful, opinionated command-line interface (CLI) tool, designed to simplify the management of git repositories. It provides a set of commands that streamline common git operations, making your workflow more efficient. +## Table of Contents + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Usage](#usage) +- [Configuration](#configuration) +- [Why lgit?](#why-lgit) +- [Development](#development) +- [Contributing](#contributing) +- [FAQ & Troubleshooting](#faq--troubleshooting) +- [License](#license) + ## Features -- **Autosquash**: Automatically squash all fixup commits in the current branch. -- **Branch**: Quickly create a new branch from a freshly pulled BASE branch. -- **Checkout**: Checkout a branch by name, or by selecting from a list of all branches. -- **CherryPick**: Interactively select and cherry-pick commits from another branch. -- **DeleteBranches**: Safely delete all branches for which remotes are gone. Use with caution! -- **Fixup**: Commit as a fixup, simplifying your commit history. -- **Rebase**: Rebase the current branch on top of freshly pulled BASE branch with a single command. -- **Git Command Fallback**: For any git command not directly supported by lgit, the tool will automatically pass the command through to git. +- **Autosquash** (`as`): Automatically squash all fixup commits in the current branch, cleaning up your commit history with interactive rebase. Perfect for consolidating work-in-progress commits. + +- **Branch** (`b`): Quickly create a new branch from a freshly pulled BASE branch (defaults to main/master). Ensures you're always branching from the latest code. + +- **Checkout** (`co`): Checkout a branch by name with fuzzy matching, or interactively select from a list of all local/remote branches. Supports `--remote` and `--all` flags for filtering. + +- **CherryPick** (`cp`): Interactively select and cherry-pick commits from another branch using a fuzzy finder. Makes it easy to apply specific commits across branches. + +- **DeleteBranches**: Safely delete all local branches whose remote tracking branches no longer exist. Helps keep your local repository clean. + +- **Fixup** (`f`): Commit changes as a fixup commit that can later be automatically squashed with autosquash. Streamlines the process of fixing up previous commits. + +- **Rebase** (`r`): Rebase the current branch on top of a freshly pulled BASE branch with a single command. Keeps your feature branches up to date. + +- **Git Command Fallback**: For any git command not directly supported by lgit, the tool will automatically pass the command through to git, making lgit a drop-in replacement. ## Requirements -### Git +- **Git**: Version 2.0 or higher +- **Rust**: Version 1.70 or higher (if building from source) -`lgit-rs` requires git to be installed on your system. You can check if git is installed by running the following -command: +You can check your versions: ```bash git --version +rustc --version # if building from source ``` ## Installation -[Archives of precompiled binaries for lgit are available for Windows, macOS and Linux.](https://github.com/Luladjiev/lgit-rs/releases) +### Precompiled Binaries (Recommended) + +[Download precompiled binaries for Windows, macOS and Linux](https://github.com/Luladjiev/lgit-rs/releases) from the releases page. ### Using Cargo -Installing `lgit-rs` through Cargo is the easiest way to get started. You can install it by running the following -command: +Installing through Cargo is the easiest way if you have Rust installed: + +```bash +cargo install lgit +``` +To update to the latest version: ```bash cargo install lgit ``` -### Building from source +### Building from Source -You can also build `lgit-rs` from source by running the following command: +Clone and build the project locally: ```bash +git clone https://github.com/Luladjiev/lgit-rs.git +cd lgit-rs cargo install --path . ``` +### Troubleshooting Installation + +**Cargo not found**: Install Rust and Cargo from [rustup.rs](https://rustup.rs/) + +**Permission denied**: On macOS/Linux, you might need to add `~/.cargo/bin` to your PATH: +```bash +echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc +source ~/.bashrc +``` + +**Old version**: Make sure you're getting the latest version: +```bash +cargo install lgit --force +``` + +## Quick Start + +Here are some common workflows to get you started with lgit: + +### Creating and Working on a Feature Branch + +```bash +# Create a new feature branch from latest main +lgit branch feature/awesome-feature + +# Make your changes, then commit as fixup for easy cleanup later +lgit fixup "Add awesome feature" + +# Make more changes, create another fixup +lgit fixup "Fix typo in awesome feature" + +# Squash all fixup commits when you're ready +lgit autosquash + +# Keep your branch up to date with main +lgit rebase +``` + +### Branch Management + +```bash +# Checkout a branch interactively +lgit checkout + +# Clean up old branches whose remotes are gone +lgit delete-branches + +# Cherry-pick commits from another branch interactively +lgit cherry-pick other-branch +``` + +### Using Git Commands + +```bash +# Any git command works through lgit +lgit status +lgit log --oneline +lgit push origin main + +# Explicitly pass commands to git with -- +lgit -- branch -d old-feature +``` + ## Usage To get a comprehensive list of all available commands and options, you can use the `--help` flag: @@ -80,6 +178,111 @@ lgit -- log --graph --all lgit -- reset --hard HEAD~1 ``` +### Command Examples + +#### Autosquash +```bash +# Squash all fixup commits in current branch +lgit autosquash + +# Squash last 3 commits +lgit as --number 3 + +# Squash commits since branching from main +lgit as --base main +``` + +#### Branch Operations +```bash +# Create branch from default base (main/master) +lgit branch my-feature + +# Create branch from specific base +lgit b my-feature --base develop + +# Interactive checkout +lgit checkout + +# Checkout with fuzzy matching +lgit co my-feat # matches "my-feature" + +# List only remote branches +lgit co --remote + +# List all branches (local + remote) +lgit co --all +``` + +#### Fixup Commits +```bash +# Create fixup commit with staged changes +lgit fixup "Fix the bug" + +# Shorthand +lgit f "Update docs" +``` + +#### Cherry-pick +```bash +# Interactive cherry-pick from another branch +lgit cherry-pick feature/other-branch + +# Shorthand +lgit cp main +``` + +#### Rebase +```bash +# Rebase current branch on freshly pulled main +lgit rebase + +# Rebase on specific base branch +lgit r --base develop +``` + +#### Cleanup +```bash +# Delete branches whose remotes are gone +lgit delete-branches +``` + +## Configuration + +lgit uses your existing git configuration and doesn't require additional setup. However, you can configure some behaviors: + +### Default Base Branch + +lgit automatically detects your main branch (main, master). + +### Git Integration + +lgit respects all your existing git configurations including: +- User name and email +- Remote configurations +- Git aliases +- Git hooks + +## Why lgit? + +lgit streamlines common git workflows by providing opinionated, high-level commands that combine multiple git operations. Here's how lgit compares to standard git commands: + +| Task | Git Commands | lgit Command | +|------|-------------|--------------| +| Create branch from latest main | `git checkout main && git pull && git checkout -b feature` | `lgit branch feature` | +| Fixup and squash commits | `git add -A && git commit --fixup=HEAD~1 && git rebase -i --autosquash HEAD~3` | `lgit fixup "fix" && lgit autosquash` | +| Interactive branch checkout | `git branch -a` โ†’ copy/paste branch name โ†’ `git checkout branch` | `lgit checkout` | +| Clean up merged branches | `git branch -d branch1 && git branch -d branch2...` | `lgit delete-branches` | +| Rebase on latest main | `git checkout main && git pull && git checkout - && git rebase main` | `lgit rebase` | + +### Key Benefits + +- **Fewer Commands**: Complex workflows become single commands +- **Interactive Menus**: Fuzzy-finding for branches, commits, and more +- **Smart Defaults**: Automatically detects main branch, pulls latest changes +- **Safety First**: Confirmation prompts for destructive operations +- **Git Compatibility**: Drop-in replacement - all git commands still work +- **Workflow Focused**: Designed around real development workflows, not just git primitives + ## Development `lgit-rs` is developed using the [Rust programming language](https://www.rust-lang.org/) and @@ -96,8 +299,130 @@ cargo run ## Contributing -We welcome contributions from the community! Feel free to submit a Pull Request or open an issue if you find any bugs or -have suggestions for improvements. +We welcome contributions from the community! Here's how you can help improve lgit: + +### Getting Started + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR-USERNAME/lgit-rs.git + cd lgit-rs + ``` +3. **Create a feature branch**: + ```bash + lgit branch feature/your-feature-name + ``` + +### Development Workflow + +```bash +# Install dependencies and build +cargo build + +# Run tests +cargo test + +# Run lgit locally during development +cargo run -- --help + +# Format code (use rustfmt) +cargo fmt + +# Run linting +cargo clippy + +# Before submitting, run all checks +cargo test && cargo fmt && cargo clippy +``` + +### Submitting Changes + +1. **Test your changes** thoroughly +2. **Update documentation** if needed +3. **Commit your changes** using conventional commits: + ```bash + lgit fixup "feat: add new awesome feature" + lgit autosquash + ``` +4. **Push to your fork** and **create a Pull Request** + +### What to Contribute + +- ๐Ÿ› **Bug fixes** - Help us squash bugs! +- โœจ **New features** - Add new git workflow commands +- ๐Ÿ“š **Documentation** - Improve README, add examples +- ๐Ÿงช **Tests** - Increase test coverage +- ๐ŸŽจ **Code quality** - Refactoring, performance improvements + +### Project Structure + +``` +src/ +โ”œโ”€โ”€ commands/ # Individual command implementations +โ”‚ โ”œโ”€โ”€ autosquash.rs +โ”‚ โ”œโ”€โ”€ branch.rs +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ cli.rs # Command-line interface definitions +โ”œโ”€โ”€ commands.rs # Command dispatch logic +โ”œโ”€โ”€ main.rs # Application entry point +โ””โ”€โ”€ utils.rs # Shared utilities +``` + +### Reporting Issues + +Found a bug? Have a feature request? [Open an issue](https://github.com/Luladjiev/lgit-rs/issues) with: +- Clear description of the problem or feature +- Steps to reproduce (for bugs) +- Expected vs actual behavior +- Your system info (OS, git version, lgit version) + +## FAQ & Troubleshooting + +### Common Questions + +**Q: Does lgit work with existing git repositories?** +A: Yes! lgit works with any existing git repository. It uses your current git configuration and doesn't modify your repository structure. + +**Q: Can I use lgit alongside regular git commands?** +A: Absolutely. lgit is designed as a complement to git, not a replacement. You can mix lgit and git commands freely. + +**Q: What happens if I run a git command that lgit doesn't support?** +A: lgit will automatically pass the command through to git, so `lgit status` works the same as `git status`. + +**Q: Does lgit support git hooks?** +A: Yes, lgit respects all existing git hooks since it uses git under the hood. + +### Troubleshooting + +**"Command not found: lgit"** +- Make sure `~/.cargo/bin` is in your PATH +- Try running `cargo install lgit --force` to reinstall + +**"Git command failed"** +- Ensure you're in a git repository: `git status` +- Check that git is working: `git --version` +- Verify you have the necessary permissions + +**"No base branch found"** +- lgit looks for main or master branches +- Or specify manually: `lgit rebase --base your-branch` + +**Interactive menus not working** +- Ensure you're using a compatible terminal +- Try updating to the latest version: `cargo install lgit --force` +- Check that your terminal supports interactive input + +**"Branch already exists"** +- Use `lgit checkout existing-branch` to switch to existing branches +- Use `lgit branch new-branch` only for creating new branches + +### Compatibility + +- **Git Version**: Requires git 2.0+ +- **Operating Systems**: Windows, macOS, Linux +- **Terminals**: Works with all major terminal emulators +- **Git Workflows**: Compatible with GitFlow, GitHub Flow, and custom workflows ## License From 50be5b6ccfdcd3549458426c2639df5f065a8235 Mon Sep 17 00:00:00 2001 From: Peter Luladjiev Date: Thu, 11 Sep 2025 15:14:12 +0300 Subject: [PATCH 4/4] Change error types from &'static str to String throughout code and update fallback error to be more specific --- src/commands/autosquash.rs | 4 ++-- src/commands/branch.rs | 6 +++--- src/commands/checkout.rs | 18 +++++++++--------- src/commands/cherry_pick.rs | 14 +++++++------- src/commands/delete_branches.rs | 12 ++++++------ src/commands/fixup.rs | 14 +++++++------- src/commands/git_fallback.rs | 8 ++++++-- src/commands/rebase.rs | 8 ++++---- src/main.rs | 2 +- src/utils.rs | 20 ++++++++++---------- 10 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/commands/autosquash.rs b/src/commands/autosquash.rs index 1bb6e26..b4dd42d 100644 --- a/src/commands/autosquash.rs +++ b/src/commands/autosquash.rs @@ -5,7 +5,7 @@ pub fn run( base: &str, number: Option, verbose: bool, -) -> Result<(), &'static str> { +) -> Result<(), String> { let mut args = vec![ "-c", "sequence.editor=:", // used in order to prevent --interactive blocking the autosquash @@ -25,7 +25,7 @@ pub fn run( cmd.exec(&args, verbose) .map(|_| ()) - .map_err(|()| "Failed to auto squash commits") + .map_err(|()| "Failed to auto squash commits".to_string()) } #[cfg(test)] diff --git a/src/commands/branch.rs b/src/commands/branch.rs index f21634c..e7d810e 100644 --- a/src/commands/branch.rs +++ b/src/commands/branch.rs @@ -6,14 +6,14 @@ pub fn run( name: &str, base: &str, verbose: bool, -) -> Result<(), &'static str> { +) -> Result<(), String> { let unsaved_changes = stash(command, verbose)?; - refresh_base(command, base, verbose).map_err(|()| "Failed to refresh base branch")?; + refresh_base(command, base, verbose).map_err(|()| "Failed to refresh base branch".to_string())?; command .exec(&["checkout", "-b", name], verbose) - .map_err(|()| "Failed to create branch")?; + .map_err(|()| "Failed to create branch".to_string())?; if unsaved_changes { unstash(command, verbose)?; diff --git a/src/commands/checkout.rs b/src/commands/checkout.rs index 6f6cd09..3156418 100644 --- a/src/commands/checkout.rs +++ b/src/commands/checkout.rs @@ -9,7 +9,7 @@ pub fn run( remote: bool, all: bool, verbose: bool, -) -> Result<(), &str> { +) -> Result<(), String> { if let Some(name) = name { return do_checkout(cmd, &name, verbose); } @@ -19,17 +19,17 @@ pub fn run( do_checkout(cmd, &branch, verbose) } -fn do_checkout(cmd: &T, branch: &str, verbose: bool) -> Result<(), &'static str> { +fn do_checkout(cmd: &T, branch: &str, verbose: bool) -> Result<(), String> { cmd.exec(&["checkout", branch], verbose) - .map_err(|()| "Failed to checkout branch")?; + .map_err(|()| "Failed to checkout branch".to_string())?; Ok(()) } -fn get_branches(cmd: &T, remote: bool, all: bool, verbose: bool) -> Result { +fn get_branches(cmd: &T, remote: bool, all: bool, verbose: bool) -> Result { let remotes: Vec = cmd .exec(&["remote"], verbose) - .map_err(|()| "Failed to get remotes")? + .map_err(|()| "Failed to get remotes".to_string())? .lines() .map(String::from) .collect(); @@ -45,7 +45,7 @@ fn get_branches(cmd: &T, remote: bool, all: bool, verbose: bool) -> Res let mut branches: Vec = cmd .exec(&branch_args, verbose) - .map_err(|()| "Failed to list branches")? + .map_err(|()| "Failed to list branches".to_string())? .lines() .map(|line| { let mut line = String::from(line); @@ -65,7 +65,7 @@ fn get_branches(cmd: &T, remote: bool, all: bool, verbose: bool) -> Res branches.dedup(); if branches.is_empty() { - return Err("No branches found"); + return Err("No branches found".to_string()); } let option = FuzzySelect::with_theme(&ColorfulTheme::default()) @@ -78,13 +78,13 @@ fn get_branches(cmd: &T, remote: bool, all: bool, verbose: bool) -> Res println!("{err}"); } - "There was an error determining the branch" + "There was an error determining the branch".to_string() })?; let branch = branches.get(option); if branch.is_none() { - return Err("There was an error getting the branch"); + return Err("There was an error getting the branch".to_string()); } let branch = branch.unwrap(); diff --git a/src/commands/cherry_pick.rs b/src/commands/cherry_pick.rs index 4767805..d906b2c 100644 --- a/src/commands/cherry_pick.rs +++ b/src/commands/cherry_pick.rs @@ -2,17 +2,17 @@ use dialoguer::MultiSelect; use crate::commands::Exec; -pub fn run(cmd: &dyn Exec, branch: &str, number: u32, verbose: bool) -> Result<(), &'static str> { +pub fn run(cmd: &dyn Exec, branch: &str, number: u32, verbose: bool) -> Result<(), String> { let commits = get_commits(cmd, branch, number, verbose)?; let selections = MultiSelect::new() .with_prompt("Select commits to cherry-pick (use space to select, enter to confirm)") .items(&commits) .interact() - .map_err(|_| "Failed to get user input")?; + .map_err(|_| "Failed to get user input".to_string())?; if selections.is_empty() { - return Err("No commits selected"); + return Err("No commits selected".to_string()); } let selected_commits: Result, String> = selections @@ -30,7 +30,7 @@ pub fn run(cmd: &dyn Exec, branch: &str, number: u32, verbose: bool) -> Result<( println!("{err}"); } - return Err("Failed to parse commits format"); + return Err("Failed to parse commits format".to_string()); } }; @@ -38,7 +38,7 @@ pub fn run(cmd: &dyn Exec, branch: &str, number: u32, verbose: bool) -> Result<( for commit in selected_commits { if cmd.exec(&["cherry-pick", commit], verbose).is_err() { - return Err("Failed to cherry-pick commit"); + return Err("Failed to cherry-pick commit".to_string()); } } @@ -50,7 +50,7 @@ fn get_commits( branch: &str, number: u32, verbose: bool, -) -> Result, &'static str> { +) -> Result, String> { let output = cmd .exec( &[ @@ -61,7 +61,7 @@ fn get_commits( ], verbose, ) - .map_err(|()| "Failed to get commit history")?; + .map_err(|()| "Failed to get commit history".to_string())?; Ok(output.lines().map(String::from).collect()) } diff --git a/src/commands/delete_branches.rs b/src/commands/delete_branches.rs index 7746757..7e5c02f 100644 --- a/src/commands/delete_branches.rs +++ b/src/commands/delete_branches.rs @@ -1,6 +1,6 @@ use crate::commands::Exec; -pub fn run(command: &T, dry_run: bool, verbose: bool) -> Result<(), &str> { +pub fn run(command: &T, dry_run: bool, verbose: bool) -> Result<(), String> { match delete_branches(command, dry_run, verbose) { Ok(output) => { println!("{output}"); @@ -14,14 +14,14 @@ fn delete_branches( command: &T, dry_run: bool, verbose: bool, -) -> Result { +) -> Result { command .exec(&["fetch", "--prune"], verbose) - .map_err(|()| "Failed to fetch")?; + .map_err(|()| "Failed to fetch".to_string())?; let branches = command .exec(&["branch", "-vv"], verbose) - .map_err(|()| "Failed to get branches")?; + .map_err(|()| "Failed to get branches".to_string())?; let mut result = Vec::new(); @@ -33,12 +33,12 @@ fn delete_branches( let branch_name = line .split_whitespace() .next() - .ok_or("Failed to parse branch name")?; + .ok_or("Failed to parse branch name".to_string())?; if !dry_run { command .exec(&["branch", "-D", branch_name], verbose) - .map_err(|()| "Failed to delete branch")?; + .map_err(|()| "Failed to delete branch".to_string())?; } result.push(format!("Deleted branch {branch_name}")); diff --git a/src/commands/fixup.rs b/src/commands/fixup.rs index 14ee66d..e48991c 100644 --- a/src/commands/fixup.rs +++ b/src/commands/fixup.rs @@ -3,17 +3,17 @@ use dialoguer::FuzzySelect; use crate::commands::Exec; -pub fn run(command: &T, number: u32, verbose: bool) -> Result<(), &'static str> { +pub fn run(command: &T, number: u32, verbose: bool) -> Result<(), String> { let commit = get_sha(command, number, verbose)?; let result = command.exec(&["commit", "--fixup", commit.as_str()], verbose); match result { Ok(_) => Ok(()), - Err(()) => Err("Failed to fixup commit"), + Err(()) => Err("Failed to fixup commit".to_string()), } } -fn get_sha(command: &T, number: u32, verbose: bool) -> Result { +fn get_sha(command: &T, number: u32, verbose: bool) -> Result { let options = get_log(command, number, verbose); let options = options?; @@ -27,13 +27,13 @@ fn get_sha(command: &T, number: u32, verbose: bool) -> Result(command: &T, number: u32, verbose: bool) -> Result(command: &T, number: u32, verbose: bool) -> Result, &'static str> { +fn get_log(command: &T, number: u32, verbose: bool) -> Result, String> { let log = command .exec( &["log", "--format=%h %s", "-n", &number.to_string()], verbose, ) - .map_err(|()| "Failed to fetch git log")?; + .map_err(|()| "Failed to fetch git log".to_string())?; let log = log.lines().map(String::from); let log = log.collect(); diff --git a/src/commands/git_fallback.rs b/src/commands/git_fallback.rs index e32c0d1..128f306 100644 --- a/src/commands/git_fallback.rs +++ b/src/commands/git_fallback.rs @@ -1,6 +1,6 @@ use crate::commands::Exec; -pub fn run(cmd: &dyn Exec, args: &[String], verbose: bool) -> Result<(), &'static str> { +pub fn run(cmd: &dyn Exec, args: &[String], verbose: bool) -> Result<(), String> { let str_args: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); match cmd.exec(&str_args, verbose) { @@ -10,6 +10,10 @@ pub fn run(cmd: &dyn Exec, args: &[String], verbose: bool) -> Result<(), &'stati } Ok(()) } - Err(_) => Err("Git command failed"), + Err(_) => { + let command = format!("git {}", args.join(" ")); + let error_msg = format!("Git command '{}' failed", command); + Err(error_msg) + } } } diff --git a/src/commands/rebase.rs b/src/commands/rebase.rs index c859156..61555c0 100644 --- a/src/commands/rebase.rs +++ b/src/commands/rebase.rs @@ -1,18 +1,18 @@ use crate::commands::Exec; use crate::utils::{refresh_base, stash, unstash}; -pub fn run(command: &T, base: &str, verbose: bool) -> Result<(), &'static str> { +pub fn run(command: &T, base: &str, verbose: bool) -> Result<(), String> { let unsaved_changes = stash(command, verbose)?; - refresh_base(command, base, verbose).map_err(|()| "Failed to refresh base branch")?; + refresh_base(command, base, verbose).map_err(|()| "Failed to refresh base branch".to_string())?; command .exec(&["checkout", "-"], verbose) - .map_err(|()| "Failed to checkout back to initial branch")?; + .map_err(|()| "Failed to checkout back to initial branch".to_string())?; command .exec(&["rebase", base], verbose) - .map_err(|()| "Failed to rebase")?; + .map_err(|()| "Failed to rebase".to_string())?; if unsaved_changes { unstash(command, verbose)?; diff --git a/src/main.rs b/src/main.rs index 685d554..c71f41e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ fn main() { cherry_pick::run(&command, &branch, number, cli.verbose) } Some(Commands::External(args)) => git_fallback::run(&command, &args, cli.verbose), - None => Err("No command specified, please run with --help for more info"), + None => Err("No command specified, please run with --help for more info".to_string()), }; if let Err(err) = result { diff --git a/src/utils.rs b/src/utils.rs index 472e3d8..84c7ee2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,14 +3,14 @@ use crate::commands::Exec; pub fn get_default_branch( command: &T, verbose: bool, -) -> Result<&'static str, &'static str> { +) -> Result<&'static str, String> { for branch in ["main", "master"] { if search_branch(command, branch, verbose).is_ok() { return Ok(branch); } } - Err("Failed to determine default branch") + Err("Failed to determine default branch".to_string()) } pub fn get_base(command: &T, base: Option, verbose: bool) -> String { @@ -25,22 +25,22 @@ pub fn refresh_base<'a, T: Exec>(command: &T, base: &'a str, verbose: bool) -> R command.exec(&["pull"], verbose).map(|_| base) } -fn search_branch(command: &T, branch: &str, verbose: bool) -> Result<(), &'static str> { +fn search_branch(command: &T, branch: &str, verbose: bool) -> Result<(), String> { let result = command .exec(&["branch", "-l", branch], verbose) - .map_err(|()| "Failed to list branch")?; + .map_err(|()| "Failed to list branch".to_string())?; if result.is_empty() { - Err("Branch not found") + Err("Branch not found".to_string()) } else { Ok(()) } } -pub fn stash(command: &T, verbose: bool) -> Result { +pub fn stash(command: &T, verbose: bool) -> Result { let result = command .exec(&["status", "--porcelain"], verbose) - .map_err(|()| "Failed to retrieve branch status")?; + .map_err(|()| "Failed to retrieve branch status".to_string())?; if result.is_empty() { return Ok(false); @@ -48,15 +48,15 @@ pub fn stash(command: &T, verbose: bool) -> Result command .exec(&["stash", "-u"], verbose) - .map_err(|()| "Failed to stash changes")?; + .map_err(|()| "Failed to stash changes".to_string())?; Ok(true) } -pub fn unstash(command: &T, verbose: bool) -> Result<(), &'static str> { +pub fn unstash(command: &T, verbose: bool) -> Result<(), String> { command .exec(&["stash", "pop"], verbose) - .map_err(|()| "Failed to unstash changes")?; + .map_err(|()| "Failed to unstash changes".to_string())?; Ok(()) }