diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml new file mode 100644 index 0000000..dc6fcc8 --- /dev/null +++ b/.github/workflows/makefile.yml @@ -0,0 +1,170 @@ +name: Makefile CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +defaults: + run: + shell: bash + +jobs: + test-and-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for proper git versioning + + - name: Setup Git for tests + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + - name: Install dependencies + uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: shellcheck help2man dpkg devscripts make + version: 1.0 + + - name: Make the script executable + run: chmod +x git-smart-squash + + - name: Run syntax check + run: | + bash -n git-smart-squash + echo "✅ Syntax check passed" + + - name: Run ShellCheck + run: | + if command -v shellcheck >/dev/null 2>&1; then + # Skip SC1090 (dynamic source paths) + shellcheck -e SC1090 -e SC2155 git-smart-squash + echo "✅ ShellCheck passed" + else + echo "⚠️ ShellCheck not available, skipping" + fi + + - name: Run Makefile targets + run: | + make build + make test + make lint + + - name: Build Debian package + run: make deb + + - name: Build distribution tarball + run: make dist + + - name: Test installation + run: | + make install DESTDIR=./test-install + echo "✅ Installation test passed" + + - name: Verify help command works + run: | + ./git-smart-squash --help + echo "✅ Help command works" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + retention-days: 7 + + multi-platform-test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + include: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + - name: Install dependencies + run: | + if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then + sudo apt-get update + sudo apt-get install -y shellcheck make + else + brew install shellcheck make + fi + + - name: Make script executable + run: chmod +x git-smart-squash + + - name: Basic functionality test + run: | + ./git-smart-squash --help + echo "✅ Basic functionality verified on ${{ matrix.os }}" + + - name: Syntax check + run: | + bash -n git-smart-squash + echo "✅ Syntax check passed on ${{ matrix.os }}" + + security-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run ShellCheck security scan + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + shellcheck --severity=warning -e SC1090 -e SC2155 git-smart-squash + + #- name: Check for secrets + # uses: gitleaks/gitleaks-action@v2.3.4 + # with: + # config-path: .gitleaks.toml + # continue-on-error: false # Fail build if secrets are found + + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: [test-and-build, multi-platform-test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg devscripts make + + - name: Build release artifacts + run: | + make deb + make dist + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + dist/*.deb + dist/*.tar.gz + generate_release_notes: true diff --git a/README.md b/README.md index a402df3..29cc0c5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Perfect for maintaining clean, conventional commit histories in development work ## 📦 Installation ### Method 1: Direct Download ``` -curl -L https://github.com/Forz70043/git-smart-squash/blob/master/git-smart-squash.sh -o /usr/local/bin/git-smart-squash +curl -L https://raw.githubusercontent.com/Forz70043/git-smart-squash/refs/heads/master/git-smart-squash.sh -o /usr/local/bin/git-smart-squash chmod +x /usr/local/bin/git-smart-squash ``` ### Method 2: Debian Package (Coming Soon) diff --git a/git-smart-squash.sh b/git-smart-squash.sh index e78484c..34511bd 100755 --- a/git-smart-squash.sh +++ b/git-smart-squash.sh @@ -1,42 +1,26 @@ #!/bin/bash # git-smart-squash - Smart automatic git history cleaning +# Author: Forz () +# License: GPL-3.0 +# Version: 1.0.0 +# Repository: https://github.com/Forz70043/git-smart-squash + +set -Eeuo pipefail +trap 'echo "❌ Error on line $LINENO"; exit 1' ERR VERSION="1.0.0" CONFIG_FILE="$HOME/.git-smart-squash.conf" -# Configurazione default +# Default configuration DEFAULT_COMMIT_MSG_PATTERN="^(fix|feat|chore|docs|style|refactor|test|perf):" DEFAULT_MAX_COMMITS=20 -show_version() { - echo "git-smart-squash version $VERSION" -} - -show_help() { - cat << EOF - Usage: git-smart-squash [OPTIONS] - Automatically squash and organize git commits intelligently. - - OPTIONS: - -b, --branch BRANCH Branch to squash (default: current) - -m, --main MAIN_BRANCH Main branch (default: main) - -n, --dry-run Show what would be done without executing - -c, --config FILE Use custom config file - --auto-commit Automatically create commit messages - --help Show this help - - EXAMPLES: - git-smart-squash - git-smart-squash -b feature/xyz -m develop --dry-run - git-smart-squash --auto-commit -EOF -} - load_config() { if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/dev/null source "$CONFIG_FILE" else - # Default config + # Crea config di default cat > "$CONFIG_FILE" << EOF # Git Smart Squash Configuration COMMIT_MSG_PATTERN="$DEFAULT_COMMIT_MSG_PATTERN" @@ -44,34 +28,67 @@ MAX_COMMITS_TO_SQUASH=$DEFAULT_MAX_COMMITS EXCLUDE_BRANCHES=("main" "master" "develop") AUTO_COMMIT_GROUPING=true EOF + # shellcheck source=/dev/null source "$CONFIG_FILE" fi } +show_help() { + cat << EOF +Usage: git-smart-squash [OPTIONS] +Automatically squash and organize git commits intelligently. + +OPTIONS: + -b, --branch BRANCH Branch to squash (default: current) + -m, --main MAIN_BRANCH Main branch (default: main) + -n, --dry-run Show what would be done without executing + -c, --config FILE Use custom config file + --auto-commit Automatically create commit messages + --help Show this help + -v, --version Show version + +EXAMPLES: + git-smart-squash + git-smart-squash -b feature/xyz -m develop --dry-run + git-smart-squash --auto-commit +EOF +} + analyze_commits() { local branch="$1" local main_branch="$2" + local commits + local commit_count + local good_commits=0 + local bad_commits=0 + local commit + local msg echo "🔍 Analyzing commits on '$branch' since '$main_branch'..." - - # Get list of commits - local commits=$(git log --oneline --reverse "$main_branch..$branch" 2>/dev/null) - local commit_count=$(echo "$commits" | wc -l) - - if [[ $commit_count -eq 0 ]]; then - echo "❌ No commits found to squash" - exit 1 + git fetch origin + #Get commit list - Resolution SC2155 + #commits=$(git log --oneline --reverse "$main_branch..$branch" 2>/dev/null) + #commit_count=$(echo "$commits" | wc -l) + if ! commits=$(git log --oneline --reverse "origin/$main_branch..$branch" 2>/dev/null); then + echo "❌ Failed to read commits between $main_branch and $branch" + return 1 + fi + + if [[ -z "$commits" ]]; then + echo "❌ No commits found to squash (are you behind origin?)" + return 1 fi echo "📊 Found $commit_count commits:" echo "$commits" - # Check pattern in messages - local good_commits=0 - local bad_commits=0 - + # Analyze commit messages while IFS= read -r commit; do - local msg=$(echo "$commit" | cut -d' ' -f2-) + [[ -z "$commit" ]] && continue + + # Resolution SC2155 + msg=$(echo "$commit" | cut -d' ' -f2-) + if [[ "$msg" =~ $COMMIT_MSG_PATTERN ]]; then ((good_commits++)) else @@ -88,10 +105,17 @@ analyze_commits() { group_commits_by_type() { local commits="$1" - declare -A groups + local commit + local msg + local type + local count + while IFS= read -r commit; do - local msg=$(echo "$commit" | cut -d' ' -f2-) + [[ -z "$commit" ]] && continue + + # Resolution SC2155 + msg=$(echo "$commit" | cut -d' ' -f2-) if [[ "$msg" =~ ^(fix): ]]; then groups[fix]+="$commit"$'\n' @@ -107,43 +131,70 @@ group_commits_by_type() { done <<< "$commits" for type in "${!groups[@]}"; do - local count=$(echo "${groups[$type]}" | grep -c '^') + # Resolution SC2155 + count=$(echo "${groups[$type]}" | grep -c '^') echo "📁 $type: $count commits" done - - declare -p groups } -smart_squash() { - local branch="${1:-$(git branch --show-current)}" - local main_branch="${2:-main}" - local dry_run="$3" - local auto_commit="$4" - - # Check if we are in a git repository +check_git_repository() { if ! git rev-parse --git-dir > /dev/null 2>&1; then echo "❌ Not a git repository" - exit 1 + return 1 fi - - # Check if the branch exists + return 0 +} + +check_branch_exists() { + local branch="$1" if ! git show-ref --verify --quiet "refs/heads/$branch"; then echo "❌ Branch '$branch' does not exist" - exit 1 + return 1 fi + return 0 +} + +is_protected_branch() { + local branch="$1" + local excluded - load_config - - # Check if branch is excluded for excluded in "${EXCLUDE_BRANCHES[@]}"; do if [[ "$branch" == "$excluded" ]]; then echo "❌ Cannot squash protected branch: $branch" - exit 1 + return 1 fi done + return 0 +} + +smart_squash() { + local branch="${1:-$(git branch --show-current)}" + local main_branch="${2:-main}" + local dry_run="$3" + local auto_commit="$4" + local bad_commits - analyze_commits "$branch" "$main_branch" - local bad_commits=$? + # Check if in git repository + if ! check_git_repository; then + return 1 + fi + + # Check if branch exists + if ! check_branch_exists "$branch"; then + return 1 + fi + + load_config + + # Check if branch is protected + if is_protected_branch "$branch"; then + return 1 + fi + + if ! analyze_commits "$branch" "$main_branch"; then + return 1 + fi + bad_commits=$? if [[ $bad_commits -eq 0 ]]; then echo "🎉 All commits already follow conventions. No squashing needed." @@ -152,66 +203,76 @@ smart_squash() { if [[ "$dry_run" == "true" ]]; then echo "📝 DRY RUN: Would squash $bad_commits problematic commits" + echo "Commits that would be squashed:" + git log --oneline "$main_branch..$branch" + echo "" return 0 fi echo "🚀 Starting smart squash..." - - # Perform interactive rebase + + # Run interactive rebase if [[ "$auto_commit" == "true" ]]; then echo "🤖 Auto-commit mode enabled" - # Here you would implement the logic to create automatic commit messages - git rebase -i "origin/$main_branch" - else - git rebase -i "origin/$main_branch" + if ! git rebase -i "origin/$main_branch"; then + echo "❌ Rebase failed. Resolve conflicts and run: git rebase --continue" + return 1 + fi fi echo "✅ Smart squash completed!" } -# Parse arguments -BRANCH="" -MAIN_BRANCH="main" -DRY_RUN=false -AUTO_COMMIT=false - -while [[ $# -gt 0 ]]; do - case $1 in - -b|--branch) - BRANCH="$2" - shift 2 - ;; - -m|--main) - MAIN_BRANCH="$2" - shift 2 - ;; - -n|--dry-run) - DRY_RUN=true - shift - ;; - -c|--config) - CONFIG_FILE="$2" - shift 2 - ;; - --auto-commit) - AUTO_COMMIT=true - shift - ;; - --help|-h) - show_help - exit 0 - ;; - --version|-v) - show_version - exit 0 - ;; - *) - echo "Unknown option: $1" - show_help - exit 1 - ;; - esac -done - -# Run the main script -smart_squash "$BRANCH" "$MAIN_BRANCH" "$DRY_RUN" "$AUTO_COMMIT" \ No newline at end of file +main() { + local BRANCH="" + local MAIN_BRANCH="main" + local DRY_RUN=false + local AUTO_COMMIT=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + -b|--branch) + BRANCH="$2" + shift 2 + ;; + -m|--main) + MAIN_BRANCH="$2" + shift 2 + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + -c|--config) + CONFIG_FILE="$2" + shift 2 + ;; + --auto-commit) + AUTO_COMMIT=true + shift + ;; + --help) + show_help + exit 0 + ;; + -v|--version) + echo "git-smart-squash v$VERSION" + exit 0 + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac + done + + # Run main functionality + smart_squash "$BRANCH" "$MAIN_BRANCH" "$DRY_RUN" "$AUTO_COMMIT" +} + +# Run main only if the script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file