#!/bin/bash # gp - Git Push with AI-generated commit messages # Improved version with better error handling, safety checks, and functionality set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Function to print colored output print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Function to show help show_help() { cat << 'EOF' gp - Git Push with smart commit messages USAGE: gp [options] [message] OPTIONS: -h, --help Show this help message -n, --dry-run Show what would be committed without actually doing it -f, --force Skip safety checks (use with caution) -b, --branch Specify branch to push to (default: current branch) -a, --add-all Add all files including untracked (default: only staged + modified) ARGUMENTS: message Optional custom commit message (overrides auto-generation) EXAMPLES: gp # Auto-generate commit message and push gp "Fix bug in parser" # Use custom commit message gp --dry-run # Preview what would be committed gp -b develop # Push to develop branch instead of current EOF } # Function to generate commit message based on changes generate_commit_message() { local files_changed=$(git diff --cached --name-only) local files_count=$(echo "$files_changed" | wc -l) if [ -z "$files_changed" ]; then files_changed=$(git diff --name-only) files_count=$(echo "$files_changed" | wc -l) fi # If add-all is enabled, also include untracked files if [ "$ADD_ALL" = true ] && [ -z "$files_changed" ]; then files_changed=$(git ls-files --others --exclude-standard) files_count=$(echo "$files_changed" | wc -l) fi if [ -z "$files_changed" ]; then echo "No changes to commit" return 1 fi # Generate smart commit message based on file types and changes local has_source_files=false local has_config_files=false local has_docs=false local has_tests=false local message="" while IFS= read -r file; do [ -z "$file" ] && continue case "$file" in *.cpp|*.hpp|*.c|*.h|*.js|*.ts|*.py|*.go|*.rs|*.java) has_source_files=true ;; *.json|*.yml|*.yaml|*.toml|*.ini|*.conf|CMakeLists.txt|Makefile) has_config_files=true ;; *.md|*.txt|*.rst|docs/*|README*) has_docs=true ;; *test*|*spec*|test/*|tests/*) has_tests=true ;; esac done <<< "$files_changed" # Create descriptive commit message if [ "$files_count" -eq 1 ]; then local single_file=$(echo "$files_changed" | head -1) local change_type=$(git diff --cached --name-status "$single_file" 2>/dev/null || git diff --name-status "$single_file") case "${change_type:0:1}" in A) message="Add $single_file" ;; M) message="Update $single_file" ;; D) message="Remove $single_file" ;; R) message="Rename $single_file" ;; *) message="Modify $single_file" ;; esac else local prefix="" if $has_tests; then prefix="test: " elif $has_docs; then prefix="docs: " elif $has_config_files; then prefix="config: " elif $has_source_files; then prefix="feat: " fi message="${prefix}Update $files_count files" fi echo "$message" } # Function to check if we're in a git repository check_git_repo() { if ! git rev-parse --git-dir >/dev/null 2>&1; then print_error "Not in a git repository" exit 1 fi } # Function to check for uncommitted changes check_for_changes() { if git diff --cached --quiet && git diff --quiet; then if [ "$ADD_ALL" = false ]; then print_warning "No staged changes found" print_info "Use 'gp -a' to add all files, or stage changes first with 'git add'" exit 0 fi fi } # Function to show what would be committed show_status() { print_info "Current branch: $(git branch --show-current)" print_info "Repository: $(git remote get-url origin 2>/dev/null || echo 'No remote')" echo if git diff --cached --quiet; then print_info "Staged changes: None" if ! git diff --quiet; then print_info "Modified files (unstaged):" git diff --name-only | sed 's/^/ /' fi else print_info "Staged changes:" git diff --cached --name-only | sed 's/^/ /' fi if [ "$ADD_ALL" = true ]; then local untracked=$(git ls-files --others --exclude-standard) if [ -n "$untracked" ]; then print_info "Untracked files (will be added):" echo "$untracked" | sed 's/^/ /' fi fi } # Function to perform the actual commit and push do_commit_and_push() { local commit_msg="$1" local target_branch="$2" # Add files if requested if [ "$ADD_ALL" = true ]; then print_info "Adding all files..." git add . fi # Commit print_info "Committing with message: '$commit_msg'" git commit -m "$commit_msg" # Push print_info "Pushing to $target_branch..." if git push origin "$target_branch"; then print_success "Successfully pushed to origin/$target_branch" else print_error "Failed to push to origin/$target_branch" print_info "You may need to pull first: git pull origin $target_branch" exit 1 fi } # Default values DRY_RUN=false FORCE=false ADD_ALL=false CUSTOM_MESSAGE="" TARGET_BRANCH="" # Handle special commands first case "${1:-}" in autocomplete) shift autocomplete "$@" exit 0 ;; version) echo "gp version 2.0.0" exit 0 ;; esac # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -n|--dry-run) DRY_RUN=true shift ;; -f|--force) FORCE=true shift ;; -a|--add-all) ADD_ALL=true shift ;; -b|--branch) TARGET_BRANCH="$2" shift 2 ;; -*) print_error "Unknown option: $1" echo "Use 'gp --help' for usage information" exit 1 ;; *) CUSTOM_MESSAGE="$1" shift ;; esac done # Main execution main() { # Safety checks check_git_repo # Set target branch if not specified if [ -z "$TARGET_BRANCH" ]; then TARGET_BRANCH=$(git branch --show-current) fi # Check for changes check_for_changes # Show current status show_status echo # Generate or use custom commit message if [ -n "$CUSTOM_MESSAGE" ]; then commit_message="$CUSTOM_MESSAGE" print_info "Using custom commit message: '$commit_message'" else print_info "Generating commit message..." commit_message=$(generate_commit_message) if [ $? -ne 0 ]; then print_error "Failed to generate commit message" exit 1 fi print_info "Generated commit message: '$commit_message'" fi # Dry run mode if [ "$DRY_RUN" = true ]; then print_warning "DRY RUN MODE - No changes will be made" print_info "Would commit with message: '$commit_message'" print_info "Would push to: origin/$TARGET_BRANCH" exit 0 fi # Safety confirmation (unless forced) if [ "$FORCE" = false ]; then echo read -p "Proceed with commit and push? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Aborted by user" exit 0 fi fi # Execute the commit and push do_commit_and_push "$commit_message" "$TARGET_BRANCH" } # Autocomplete function for bash autocomplete() { local args=("$@") if [ ${#args[@]} -eq 0 ]; then echo "--help" echo "--dry-run" echo "--force" echo "--add-all" echo "--branch" echo "-h" echo "-n" echo "-f" echo "-a" echo "-b" fi } # Run main function main "$@"