Secrets of Git – git gotchas, tips & tricks

Git is by-and-large the only game in town when it comes to source control these days. Bitbucket’s recent decision of sunsetting its support for mercurial has all but cemented git as the way developers share and track code. Git itself is quite old by some standards, created in 2005 by Linus Torvalds for easier distributed code management for building his Linux operating system (The torch was then quickly passed to Junio Hamano, who has been the maintainer of git since 2005 and whose blog and public repositories hold a wealth of information on git). Such a popular and long-maintained tool has many helpful scripts and shortcuts hidden in its documentation. If you like digging through the docs as much as we do then some of these “secrets” might be familiar, but we’ve seen many of these tricks underused, and we’d like to share a few that we have found.

The Easy Wins – Alias & Autocorrect

If you use git, you probably use git all day, that usually means a lot of the same old usual commands. git status, git add, git commit, git push, git pull:
$ git status
 ...
 modified:   some_file.txt
 ... 
$ git add some_file.txt
$ git commit -m 'added some file'
 ...
$ git push
 ...
$ git pull
A quick look at your bash_history can probably confirm it, but a huge majority of your commands probably begin with git. If it does, a quick and easy win is to alias the commands you use most often.  Enter the git alias config command.
$ git config --global alias.s status
  
$ git config --global alias.c commit
  
$ git config --global alias.co checkout
  
$ git config --global alias.p push
  
$ git config --global alias.pl pull
Globally aliasing git commands vastly reduces the amount of manual typing you need to do. Take a look at some of the commands above, aliased for convenience.
$ git s # same as git status

$ git c -m 'added some file' 
Just be careful not to make the aliases too vague to be useful. 🙂
$ git p # git push or pull? Depends on what you defined
A common reason to alias larger commands is so that you don’t misspell them when you’re using them. Every developer using git from the command line has seen this message at least once in their lives:
$ git psuh
  
git: 'psuh' is not a git command. See 'git --help'.
  
Did you mean this?
  
  push
The most infuriating thing about this gotcha message is that git clearly knows there is only one possible command you could have meant, what if there was a way for git to just catch those spelling mistakes and resubmit the command correctly? Turns out there is with git help.autocorrect setting.
git config --global help.autocorrect 10
This command enables a git autocorrect setting with a timeout. After x milliseconds of inactivity after a git command with only one correct suggestion, git will correct and retry the command. The timeout is case you disagree with git’s logic. In this case you have 0.1 seconds to cancel the command with control+C.
$ git psuh
WARNING: You called a Git command named 'psuh', which does not exist.

Continuing in 0.1 seconds, assuming that you meant 'push'.

Everything up-to-date

More Impressive Tricks – pickaxe, scripts, and hooks

Let’s say for example after a long day of coding & committing you are ready to push changes to a branch, but remembered that you accidentally committed  your production credentials to the branch!
let myEmail = '[email protected]'

let password = 'N0tMyR3alP4ssword!
A simple git blame on the line could work, if the line was still there, but let’s say that the line was already deleted and that change committed. The password is now floating somewhere inside your git history. This is no small problem, thousands of API and cryptographic keys leak to GitHub every day by accident. A crude solution would be to roll back everything, remove the offensive line, recommit, and force push. But force push is destructive, and should not be treated lightly. But what if there are commits you want to keep, or what if someone already pushed these changes? Why can’t you just rebase the single offensive commit and keep everything else? Well you’d need to find the commit first, and if you had a lot of spare time on your hands then hunting for it might be fun. But if it’s urgent or late on a Friday afternoon or both, sometimes it’s better to find these things quickly. The git pickaxe is an advanced feature for those very annoying (and hopefully rare) scenarios of source control; It digs through the entire commit history for a string (or regex pattern), returning all matching commit hashes.
$ git log -p -S "the thing you want to find"
This allows you to find a change in your codebase without knowing when (or even where) it occurred:
$ git log -p -S "const password = 'N0tMyR3alP4ssword!"

commit 41658be12110473f85014dc47c7d691758407213

Author: hopefully_not_you <[email protected]>

Date: ...the rest of the commit, including a diff of the file
Of course, sometimes the pickaxe is rather too effective in finding what you ask it for, (finding common things like generic variable names could give you more results than anticipated). For best use, try to ensure that your search query is unique across all of time and space. Or try the other flags in git log for limiting your results to a set time.
$ git log -p -S "const password = 'N0tMyR3alP4ssword!" --since="2019-09-01" # limits the results to a safe time in the past
Now let’ s go back to the common events in your daily programming routine. Let’s say that you do the same basic things when starting a new feature branch; you pull & checkout master, you create a branch name (with a certain naming scheme) with an empty commit, and then you push it:
  git pull
  git checkout master
  git branch feature/EXAMPLE-100-my-new-feature
  git checkout feature/EXAMPLE-100-my-new-feature
  git commit -m '[EXAMPLE-100] starting work' --allow-empty
  git push --set-upstream origin feature/EXAMPLE-100-my-new-feature
While aliases might cut down on some of the typing, it’s still the same old commands, time after time, that we need to keep making. That’s a lot of repetitive work! A bash function would be perfect for this kind of scenario. Add the following to your bash_profile
    # $/.bash_profile 
    createEmptyBranch() {
      git checkout master && \
      git pull && \
      git branch $1 && \
      git checkout $1 && \
      git commit -m 'starting work' --allow-empty && \
      git push --set-upstream origin $1    
    }
And now in a fresh terminal the following function should be available to you:
$ createEmptyBranch my-test-branch
  Switched to branch 'master'
  Your branch is up to date with 'origin/master'.
  Already up to date.
  Switched to branch 'my-test-branch'
  [my-test-branch f58c0e6] starting work
  Enumerating objects: 1, done.
  ...
   * [new branch]      my-test-branch -> my-test-branch
  Branch 'my-test-branch' set up to track remote branch 'my-test-branch' from 'origin'.
If you’ve never herd of a bash script, they’re really worth their own blog post to dive into, but for our current purposes you can think of them as aliases for multiple command line commands. They are called, in order, halting if any of the intermittent commands fail (which is the purpose of the && symbols). They are especially useful when you need to conform to certain naming conventions for automation. For example, if you’re using a tracking system like Jira and all branch names and commits require a ticket number the following scripts might be quite useful:
  # $/.bash_profile 

  # creates a new branch a jira url and additional description, for example:  
  # $ createEmptyJiraFeatureBranch https://example.jira.com/EXAMPLE-100 my-example-feature
  # creates a branch named:'feature/EXAMPLE-100-my-example-feature' with empty commit: '[EXAMPLE-100] starting work'
  createEmptyJiraFeatureBranch(){
    local JIRA_TICKET=$(echo $1| cut -d'/' -f 5)
    local BRANCH_NAME="feature/$JIRA_TICKET-$2"
    git pull
    git checkout master
    git branch $BRANCH_NAME
    git checkout $BRANCH_NAME
    git commit -m '[$JIRA_TICKET] starting work' --allow-empty
    git push --set-upstream origin $BRANCH_NAME   
  }
  
  # updates master and starts a merge from an arbitrary branch 
  mergeMaster(){
    local BRANCH=$(git rev-parse --abbrev-ref HEAD)
    git checkout master
    git pull
    git checkout $BRANCH
    git merge master
  }  

  # functions can be composed of existing functions, here is an arbitrary example:  
  # same parameters as createEmptyJiraFeatureBranch
  createBranchAndImmediatelyMerge(){
    createEmptyJiraFeatureBranch $1 $2
    mergeMaster
  }

git hooks are one productive step further in that they are scripts that run automatically when you’re doing something else in git. Inside every git repository is a directory called “.git/hooks”:
    $ ls .git/hooks/
    applypatch-msg.sample
    commit-msg.sample
    fsmonitor-watchman.sample
    post-checkout
    post-commit
    post-merge
    post-update.sample
    pre-applypatch.sample
    pre-commit.sample
    pre-push
    pre-push.sample
    pre-rebase.sample
    pre-receive.sample
    prepare-commit-msg.sample
    update.sample
Containted in this directory are all customizable behaviors that run during certain times in your workflows, along with several sample files to give you ideas. For example, if we wanted a to add a jira ticket number to the beginning of our commit message we could create the folowing file named .git/hooks/prepare-commit-msg
  #!/bin/bash
  # Include any branches for which you wish to disable this script
  if [ -z "$BRANCHES_TO_SKIP" ]; then
    BRANCHES_TO_SKIP=(master develop staging test)
  fi
  # Get the current branch name and check if it is excluded
  BRANCH_NAME=$(git symbolic-ref --short HEAD)
  BRANCH_EXCLUDED=$(printf "%s\n" "${BRANCHES_TO_SKIP[@]}" | grep -c "^$BRANCH_NAME$")
  # Trim it down to get the parts we're interested in
  # only currently works with branches of acertain structure: e.g. feature/EXAMPLE-##-foo or bugfix/EXAMPLE-####-bar  
  TRIMMED=$(echo $BRANCH_NAME | sed -e 's:^.*\/\([^-]*-[^-]*\)-.*:\1:' -e \
      'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/')
  # If it isn't excluded, preprend the trimmed branch identifier to the given message
  if [ -n "$BRANCH_NAME" ] &&  ! [[ $BRANCH_EXCLUDED -eq 1 ]]; then
    sed -i.bak -e "1s/^/[$TRIMMED] /" $1
  fi
(Don’t forget to run “git init” before expecting to see your new hooks.) This will allow us to skip writing each commit with the same jira ticket that’s in our branch.
git checkout some/EXAMPLE-100-branch-name
git commit -m 'I'm going to be prepended'
git log -1
commit ... (HEAD -> some/EXAMPLE-100-branch-name, some/EXAMPLE-100-branch-name)
Author: ...
Date:   ...

  [EXAMPLE-100] I'm going to be prepended

These hooks even occur inside other source control tools in other applications, such as the source control inside VSCode: Development workflows have gotten more diverse and customized over the years, and here at Rightpoint we know how much they can vary from project to project. It’s challenging to give general advice on such a complicated topic, and we don’t expect every one of these tools will be useful to you immediately. We hope that your development experience is slightly improved by at least one of them, that maybe it piques your interest in digging into the documentation yourself. Do you have a git secret? Or maybe another way to use the tools we described? Share it with us in the comments! Here are some new ones from our readers that we really like:
Delete all local git branches: Useful after a long day of closing PRs
clean-git-branch() {
  echo Cleaning branches...
  git for-each-ref --format '%(refname:short)' refs/heads | grep -v master | xargs git branch -D
  echo Done.
}
Submitted by Joe Meyer (originally from this StackOverflow post)

Leave a Reply

Your email address will not be published. Required fields are marked *