#!/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) --staged-only Only commit staged changes (don't add untracked files) ARGUMENTS: message Optional custom commit message (overrides auto-generation) EXAMPLES: gp # Add all files, auto-generate commit message and push gp "Fix bug in parser" # Add all files with custom commit message gp --dry-run # Preview what would be committed gp --staged-only # Only commit staged changes (don't add untracked) 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 files_changed=$(git diff --cached --name-only) local files_count 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/*|tests/*|*test*|*spec*) has_tests=true ;; esac done <<< "$files_changed" # Create descriptive commit message if [ "$files_count" -eq 1 ]; then local single_file single_file=$(echo "$files_changed" | head -1) local change_type 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 and unpushed commits check_for_changes() { local has_staged=false local has_modified=false local has_untracked=false # Check for staged changes if ! git diff --cached --quiet; then has_staged=true fi # Check for modified files if ! git diff --quiet; then has_modified=true fi # Check for untracked files if [ -n "$(git ls-files --others --exclude-standard)" ]; then has_untracked=true fi # Check for unpushed commits local current_branch current_branch=$(git branch --show-current) local unpushed_commits="" if git rev-parse --verify "origin/$current_branch" >/dev/null 2>&1; then unpushed_commits=$(git rev-list "origin/$current_branch..HEAD" -- 2>/dev/null || true) fi # If add-all is enabled, check if we have any changes at all if [ "$ADD_ALL" = true ]; then if [ "$has_staged" = false ] && [ "$has_modified" = false ] && [ "$has_untracked" = false ]; then # No working tree changes, but check for unpushed commits if [ -n "$unpushed_commits" ]; then local commit_count commit_count=$(echo "$unpushed_commits" | wc -l) print_info "No working tree changes, but found $commit_count unpushed commit(s)" print_info "Latest unpushed commit: $(git log --oneline -1)" # Set a flag to indicate we should only push, not commit PUSH_ONLY=true return 0 else print_info "No changes to commit (working tree clean)" exit 0 fi fi else # If add-all is disabled, only check staged changes if [ "$has_staged" = false ]; then # No staged changes, but check for unpushed commits if [ -n "$unpushed_commits" ]; then local commit_count commit_count=$(echo "$unpushed_commits" | wc -l) print_info "No staged changes, but found $commit_count unpushed commit(s)" print_info "Latest unpushed commit: $(git log --oneline -1)" if [ "$has_modified" = true ] || [ "$has_untracked" = true ]; then print_warning "You have unstaged changes that won't be included" print_info "Use 'gp -a' to include all changes, or stage them first with 'git add'" fi # Set a flag to indicate we should only push, not commit PUSH_ONLY=true return 0 else if [ "$has_modified" = true ] || [ "$has_untracked" = true ]; then print_warning "No staged changes found" print_info "Use 'gp -a' to add all files, or stage changes first with 'git add'" else print_info "No changes to commit (working tree clean)" fi exit 0 fi fi fi } # Function to show what would be committed and get user confirmation show_status_and_confirm() { print_info "Current branch: $(git branch --show-current)" print_info "Repository: $(git remote get-url origin 2>/dev/null || echo 'No remote')" echo local has_staged_changes=false local has_unstaged_changes=false local has_untracked_files=false # Show staged changes if ! git diff --cached --quiet; then print_info "Staged changes:" git diff --cached --name-only -- | while IFS= read -r line; do echo " $line"; done has_staged_changes=true fi # Show unstaged changes if ! git diff --quiet; then if [ "$ADD_ALL" = true ]; then print_info "Modified files (will be added):" else print_info "Modified files (unstaged, will NOT be included):" fi git diff --name-only -- | while IFS= read -r line; do echo " $line"; done has_unstaged_changes=true fi # Show untracked files if [ "$ADD_ALL" = true ]; then local untracked untracked=$(git ls-files --others --exclude-standard) if [ -n "$untracked" ]; then print_info "Untracked files (will be added):" # Use while loop to add indent without sed while IFS= read -r line; do echo " $line" done <<< "$untracked" has_untracked_files=true fi fi # Show summary of what will happen echo if [ "$ADD_ALL" = true ]; then if [ "$has_unstaged_changes" = true ] || [ "$has_untracked_files" = true ]; then print_warning "Files will be automatically added before committing" fi if [ "$has_staged_changes" = false ] && [ "$has_unstaged_changes" = false ] && [ "$has_untracked_files" = false ]; then print_info "No changes found to commit" fi else if [ "$has_unstaged_changes" = true ]; then print_warning "Unstaged changes will NOT be included (use -a to include them)" fi if [ "$has_staged_changes" = false ]; then print_info "No staged changes to commit" fi fi # Confirmation for file additions (unless forced or dry-run) if [ "$ADD_ALL" = true ] && [ "$FORCE" = false ] && [ "$DRY_RUN" = false ]; then if [ "$has_unstaged_changes" = true ] || [ "$has_untracked_files" = true ]; then echo read -p "Add these files and continue? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Aborted by user" exit 0 fi 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 -A fi # Commit print_info "Committing with message: '$commit_msg'" git commit -m "$commit_msg" -- # Push print_info "Pushing to $target_branch..." # Check if branch exists on remote to determine if we need -u flag if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then # Branch exists on remote, normal push 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 else # Branch doesn't exist on remote, use -u to set upstream print_info "Setting upstream branch (first push to origin/$target_branch)" if git push -u origin "$target_branch"; then print_success "Successfully pushed and set upstream to origin/$target_branch" else print_error "Failed to push to origin/$target_branch" exit 1 fi fi } # Autocomplete function for bash autocomplete() { local args=("$@") if [ ${#args[@]} -eq 0 ]; then printf "%s\n" "--help" "--dry-run" "--force" "--add-all" "--staged-only" "--branch" "-h" "-n" "-f" "-a" "-b" fi } # Default values DRY_RUN=false FORCE=false ADD_ALL=true # Default to adding all files CUSTOM_MESSAGE="" TARGET_BRANCH="" PUSH_ONLY=false # 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 # Explicitly set (though it's default) shift ;; --staged-only) ADD_ALL=false shift ;; -b|--branch) if [ -z "${2:-}" ]; then print_error "Option -b/--branch requires a branch name" exit 1 fi 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 # Handle push-only case (unpushed commits but no working tree changes) if [ "$PUSH_ONLY" = true ]; then echo # Dry run mode for push-only if [ "$DRY_RUN" = true ]; then print_warning "DRY RUN MODE - No changes will be made" print_info "Would push existing commits to: origin/$TARGET_BRANCH" exit 0 fi # Safety confirmation for push-only (unless forced) if [ "$FORCE" = false ]; then echo read -p "Push existing commits to origin/$TARGET_BRANCH? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Aborted by user" exit 0 fi fi # Execute push only print_info "Pushing existing commits to $TARGET_BRANCH..." # Check if branch exists on remote to determine if we need -u flag if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then # Branch exists on remote, normal push if git push origin "$TARGET_BRANCH"; then print_success "Successfully pushed existing commits 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 else # Branch doesn't exist on remote, use -u to set upstream print_info "Setting upstream branch (first push to origin/$TARGET_BRANCH)" if git push -u origin "$TARGET_BRANCH"; then print_success "Successfully pushed existing commits and set upstream to origin/$TARGET_BRANCH" else print_error "Failed to push to origin/$TARGET_BRANCH" exit 1 fi fi exit 0 fi # Show current status and get confirmation for file additions show_status_and_confirm # 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..." if ! commit_message=$(generate_commit_message); 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" } # Run main function main "$@"