DEV Community

Cover image for Boost your Git Squash-Rebase Workflow with these Commands! (auto-squash, auto-commit, auto-rebase, auto-push)
Parth Agarwal
Parth Agarwal

Posted on • Edited on • Originally published at blog.ra101.dev

2 1 1 1 1

Boost your Git Squash-Rebase Workflow with these Commands! (auto-squash, auto-commit, auto-rebase, auto-push)

I recently joined a company, that follows squash-rebase flow here, and as my Git KT was going on, I figured I should build these commands before actually starting development, now that I have started development, these turned out to be quite handy!


What is Git Squash-Rebase Workflow?

Git Squash Rebase is a workflow in which:

  • multiple commits are squashed into a single cohesive commit,
  • then rebased from the up-to-date main branch.

This results in a cleaner commit history, grouping of changes, easier code reviews, and streamlined collaboration. Of-course there are some drawbacks of this workflow, but that is beyond the scope of this post.


Git Squash (gsq):

gsq(){
    # Usage:
    ## gsq -b "base_branch_opt" -n "squash_count_opt" -m "msg" -d "desc" -D
    ##
    ##    -b: Base Branch Name (default: default origin branch)
    ##    -n: Squash Count (default: all until base branch)
    ##    -m: Commit Message, (default: latest commit message)
    ##        - Uses Auto Commit Command, if present.
    ##    -d: Commit Description, (default: null)
    ##    -D: if passed, the description is populated with all the previous commit msg and desc until sqaush_count

    # Get Base Branch -> Get Fork Point -> Get Squashable Count -> Squash!

    ## Even though this command seems big, but the flow is pretty simple.
    ## I use a very lite version of this code, there is alot of validation and
    ## edges cases here, which is not particularly I care about in my personal use.

    # Declare Local Var
    local base_branch_opt; local squash_count_opt; local commit_msg_opt;
    local commit_desc_opt; local add_full_desc_opt=false;

    # Get Optional Arguments!

    ## Reset the getopts state
    OPTIND=1
    ## Parse Through new opts.
    while getopts b:n:m:d:D flag
    do
        case "${flag}" in
            b) base_branch_opt=${OPTARG};;
            n) squash_count_opt=${OPTARG};;
            m) commit_msg_opt=${OPTARG};;
            d) commit_desc_opt=${OPTARG};;
            D) add_full_desc_opt=true;;
        esac
    done

    # Get Base Branch
    # ----------------

    ## Update base_branch_opt with default origin branch, if not passed!
    local current_branch=$(git rev-parse --abbrev-ref HEAD)
    if [[ $base_branch_opt == "" ]]; then
        base_branch_opt=$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')
    fi

    # Get Fork Point Commit
    # ----------------

    # Check if given base branch is actually an ancestor or not.
    local fork_point_commit=""
    if [[ $( git branch --merged HEAD 2> /dev/null | sed  -e 's/* \(.*\)/ \1/' -e  's/^ *//g' | grep -w "$base_branch_opt" ) != "" ]]; then
        ### Get the Last commit by `base_branch_opt`
        fork_point_commit=$(git merge-base --fork-point "$base_branch_opt")
    else
        ### Throw Error if Base Branch is not found as an ancestor.
        echo "Invalid Base Branch! " && return 1
    fi

    # Get Squashable Count
    # ----------------
    ## Exit, if Squashable Count is 0!
    ## We are okay with squashing 1 commit, the reason for that is, this allows us
    ## to stage the changes and use `gsq` to commit those changes into squashed commit


    ## squashable_count: Number of commits that can be to be squashed
    local current_commit=$(git rev-parse HEAD)
    local squashable_count=$(git rev-list --count $fork_point_commit..$current_commit)

    ## Exit, if passed squash_count_opt is `0` or not a `number`!
    if [[ $squash_count_opt != "" && ( ! $squash_count_opt =~ ^[0-9]+$ || $squash_count_opt == 0 )]]; then
        echo "Invalid Squash Count $squash_count_opt! " && return 1
    elif [[ $squash_count_opt == ""  ]]; then
      squash_count_opt=$squashable_count
    fi

    ## Exit, if found `squashable_count` turns about to be `0` !
    if [[ $squashable_count == "0" ]]; then
        echo "Nothing to Squash! " && return 1
    fi

    ## Squash Count Passed cannot be more than Squashable Count found!
    if [[ $squashable_count < $squash_count_opts ]]; then
        echo "Cannot Squash more than $squashable_count! " && return 1
    fi

    # Squash!: Get Commit Message and Description.
    # ----------------

    ## Get Commit Message, (Default: title from latest commit)
    if [[ $commit_msg_opt != "" ]]; then
      local commit_message=$commit_msg_opt
    else
      local commit_message=$(git log --format="%s" -n1)
    fi

    ## Get Full Commit Description, (Default: raw body from last n commit)
    if [[ $add_full_desc_opt == true ]]; then
      if [[ $commit_desc_opt != "" ]]; then
        commit_desc_opt="$(echo $commit_desc_opt) $(git log --format='%B' -n$squash_count_opt)"
      else
        commit_desc_opt="$(git log --format='%B' -n$squash_count_opt)"
      fi
    fi

    ## create commit_desc_if_there if commit_desc_opt is populated
    local commit_desc_if_there=""
    if [[ $commit_desc_opt != "" ]]; then
      commit_desc_if_there=$(echo "-m '$commit_desc_opt'")
    fi

    # Squash!: Reset Commits Softly before commiting back.
    # ----------------

    ## Normally Squashing is done using rebase,
    ## but I could not automate its interactive shell.
    git reset --soft HEAD~$squash_count_opt

    # Squash!: Commit to complete squashing!
    # ----------------

    ## Use Auto Commit Command if There, Else use Default `git commit`
    if [[ $commit_msg_opt == "" && $(command -v gac) ]]; then
        gac $commit_desc_if_there
    else
        git commit -m "$commit_message" $commit_desc_if_there
    fi
}
Enter fullscreen mode Exit fullscreen mode

Git Auto Commit (gac):

gac() {
    ## The Idea is to create a commit message from branch name
    ## For Example:
    ##    if branch name is `feature/jira-123-this-is-issue-desc`
    ##    then commit would be `[Enhancement JIRA-123] This Is Issue Desc`

    # Exit, if there is nothing to commit!
    if [[ $(git diff --staged) == "" ]]; then
        echo "Nothing to Commit! " && return 1
    fi


    # Fetch Details
    # ----------------

    ## `feature/jira-123-this-is-issue-desc`, all in lower case
    local current_branch=$(git rev-parse --abbrev-ref HEAD | tr '[:upper:]' '[:lower:]')

    ## `feature`
    local issue_type=$(echo $current_branch | cut -d '/' -f 1)

    ## `jira-123`
    local issue_no=$(echo $current_branch | cut -d '/' -f 2- | cut -d '-' -f -2)

    ## `this-is-issue-desc`
    local issue_desc=$(echo $current_branch | cut -d '/' -f 2- | cut -d '-' -f 3- )


    # Format Details
    # ----------------

    ## We will use this map to get issue_type out of branch name
    declare -A branch_map; branch_map["feature"]="Enhancement";
    branch_map["bugfix"]="Patch"; branch_map["version"]="Upgrade";

    ## `feature` -> `Enhancement`; `random` -> `Random`
    issue_type=${branch_map[$issue_type]-${issue_type^}}

    ## `jira-123` -> `JIRA-123`
    issue_no=$(echo $issue_no | tr '[:lower:]' '[:upper:]')

    ## `this-is-issue-desc` -> `This Is Issue Desc`
    issue_desc=$(echo $issue_desc | sed 's/-/ /g' | awk '{for (i=1; i<=NF; i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')

    ## `[Enhancement JIRA-123] This Is Issue Desc`
    local commit_msg="[$issue_type $issue_no] $issue_desc"

    # Commit!
    # ----------------

    ## Along with this message, I have added $@, So that all the
    ## flags and arguments of `git commit` can be passed in this command.
    git commit -m "$commit_msg" $@
}
Enter fullscreen mode Exit fullscreen mode

Git Fetch Rebase (gfrb):

gfrb() {
    ## If $1 is not passed, then it will fetch the default branch
    local base_branch=${1:-$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')}
    git fetch origin "$base_branch":"$base_branch"

    ## Rebase requires stashing, but any file that has both
    ## staged and unstaged changes will lose all its
    ## unstaged changes! So uncomment at your own risk.

    # git stash --all

    git rebase "$base_branch"

    # git stash pop
}
Enter fullscreen mode Exit fullscreen mode

Git Upstream Push (gup):

gup() {
    ## Although `--force` is frequently used in this workflow.
    ## It still feels destructive to add here.
    ## Instead I have added $@, So that all the `git push` 
    ## flags and arguments can be passed in this command.
    local current_branch=$(git rev-parse --abbrev-ref HEAD)
    git push --set-upstream origin "$current_branch" $@
}
Enter fullscreen mode Exit fullscreen mode

Update your ~/.[ba|z]shrc with these productive commands and save those few minutes. Happy Commiting!

You also might like these:

PS: Feel free to share your productivity Hack!

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)

Eliminate Context Switching and Maximize Productivity

Pieces.app

Pieces Copilot is your personalized workflow assistant, working alongside your favorite apps. Ask questions about entire repositories, generate contextualized code, save and reuse useful snippets, and streamline your development process.

Learn more