Get Git!

Yet Another Git Post for Beginners

This blog is written with assumption that you have a local Git repository and you’ve tried a few basic Git commands. You’re kinda liking git and you want to make the most of it. This post is going to focus on how to use Git productively.

Let’s refresh the basics quickly, shall we?

Git repository(repo) is basically a Directed Acyclic Graph (DAG) where each commit is a node in the graph. There are a bunch of branches in this DAG and default one is usually called master.

Some very important words to remember: In Git, branches are pointers to commit and branch name itself resolves to latest commit in it.

Let’s go through some terminology -

  • Working Tree
    This is essentially whatever you have locally in your repo.
  • Index (or Stage)
    This is a nasty, yet awesome data structure in Git. It holds files which you’re going to commit next. When you run command git add <path|--all>, you essentially update the index in git. Index is stored in a binary file .git/index, and to see what files are part of index, run command git ls-files -s. Also try git ls-files --help.
    Once you do git add on any file, an entry for that file is added in the index and that file is called the tracked file.
    Index is also commonly referred to as the ‘staging area’.
  • HEAD
    HEAD points to the latest commit in your current checked-out branch. This is also called attached HEAD. You might have also seen detached HEAD sometimes (either by mistake, or intentionally or during merge/rebase). This happens when you checkout a commit (not branch) or do a rebase interactively, or resolve conflicts. detached HEAD means that you’re working on an anonymous branch and HEAD is pointing to latest commit in that state. If this is all too confusing, fret not, we’ll cover this in the future blog posts on advanced Git.

Now let’s have a look at some of the most frequently used git commands:
If you’re seeing any of these commands for the first time, I’ll strongly suggest to try them on your own. I have however, for ease of discussion, included the output for a few commands right here -

  • git add <paths|--all> -> Adds files/directories to index.
    $ git add abc.txt def/
    # This will add file abc.txt and all files in directory 'def' to staging
    
  • git commit -m "Osum commit" -> Commits changes to local repo.
  • git push | git push origin master -> Pushes your local commits to remote repo.
  • git checkout -b feature_osum -> Creates a local branch “feature_osum” and switches to it.
  • git checkout <branch> -> Switches to branch <branch>.
  • git branch -d <branch> -> Deletes local branch <branch> if branch is pushed to remote or merged to HEAD. This is a safe delete and your work won’t be lost by mistake.
    $ git checkout -b temp
    $ echo "Hello" > abc.txt
    $ git add abc.txt
    $ git commit -m "Created abc.txt"
    $ git checkout master
    $ git branch -d temp
    error: The branch 'temp' is not fully merged.
    If you are sure you want to delete it, run 'git branch -D temp'.
    
  • git branch -D <branch> -> Forcefully deletes local branch.
  • git push origin --delete <branch> | git push origin :<branch> -> Deletes the remote branch.
  • git merge <branch> -> Merges changes from <branch> to your currently checked-out branch.
  • git fetch origin | git fetch <remotes> -> Fetches changes from remote(s) to your local repo. This doesn’t change your working tree.
  • git pull -> This does a git fetch followed by git merge on your local branch with remote branch.
  • git diff -> It shows Difference between current index and working tree relative to index i.e. it will show diff between files which were added to index previously and then changed on working tree. So if you do git diff after adding a modified file to index with git add then that file won’t be shown in diff. Similarly newly created files will not be shown in diff, but deleted files will be shown.
  • git diff --cached [commit] -> It shows diff between current index and commit. If commit is not given then it takes HEAD by default
  • git diff <commit> -> Shows changes in working directory relative to <commit>. Typically HEAD is passed, but you can pass any commit. Remember branch names are resolved to commit!
  • git diff <commit1> <commit2> -> Shows changes between commits commit1 and commit2. It says what changes commit2 made compared to commit1. Again, remember branch names are resolved to commit!
  • git stash -> Creates stash of changes which are tracked (previously added to index)
    $ git stash
    Saved working directory and index state WIP on master: 086e01c Osum
    
  • git stash -u -> Creates stash of all changes (tracked and untracked)
  • git stash --all -> Creates stash with tracked, untracked and ignored files.
  • git stash save <message> -> Creates stash with a custom message
    $ git stash save "Temp changes in abc.txt"
    Saved working directory and index state On master: Temp changes in abc.txt
    
  • git stash list -> Shows stashed changes. Stashes are stored as stack and latest stash is on the top of list.
    $ git stash list
    stash@{0}: On master: Temp changes in abc.txt
    stash@{1}: WIP on master: d7fbcd1 Added new file
    stash@{2}: WIP on master: 086e01c Osum
    
  • git stash pop -> Applies latest stashed change to working tree and deletes that stash.
  • git stash pop stash@{<revision>} -> Applies specific stashed changes to working tree and then deletes that stash.
    $ git stash pop stash@{2}
    On branch master...
      modified:   abc.txt
      modified:   def.txt
    no changes added to commit (use "git add" and/or "git commit -a")
    Dropped stash@{2} (91a9d61f5f64a5014f149a7f8e1ef52d4cd0bc02)
    
  • git stash drop stash@{<revision>} -> Deletes stashed changes without applying to working tree. If no revision is given then latest one is used
  • git stash clear -> Clears stash queue by dropping all.
  • git rebase <branch> -> Brings all commits on your current branch to top of branch. E.g. your current branch structure is:
                       A---B---C feature_osum
                      /
                 D---E---F---G master
    

    And you’re on feature_osum branch currently. Then running git rebase master will result into tree

                               A'--B'--C' feature_osum
                              /
                 D---E---F---G master
    

    All of your commits A, B, C are re-written depending on whether same changes were present in F, G.

Some cool stuff to boost your speed (and productivity)

  1. Git integration on command prompt
    Here is an example of this: My Bash Prompt Here with a quick glance I can see which branch I’m on, what is the branch status (color coded) and how many extra commit I have or I’m missing relative to remote (arrow direction). Check-it out at Github Gist. As a bonus, it also includes Python virtualenv and virtualgo on prompt.
    If you’re on windows, I’ll highly recommend posh-git which provides a nice prompt to PowerShell.
    [KEITH1] C:\Users\Keith\GitHub\posh-git [master ≡ +0 ~1 -0 !]>
    
  2. Use mergetool for quick merging
    When you get merge conflicts, you can either manually resolve them of take help of intelligent tools. In git you can configure any of your favourite tool to help you in merging. To configure a mergetool run following commands:
    # Here I'm using beyondcompare as mergetool. You can pick any of your tool
    $ git config --global merge.tool bcomp
    $ git config --global mergetool.bcomp.trustExitCode true
    $ git config --global mergetool.bcomp.cmd 'BComp.exe "$LOCAL" "$REMOTE" "$BASE" "$MERGED"'
    

    Now to resolve merge/rebase conflicts, just run git mergetool and git will automatically open mergetool.

  3. Aliases
    Aliases are one of the most awesome thing in git. Many times you type long commands and think whether you could have typed them quickly. Aliases help you there. To add an alias run command:
    git config --global alias.cm "checkout master"
    

    Now when you run git cm, it will switch to master branch. I personally keep following aliases in all of my dev machines:

    # This gives you pretty single line log history of current HEAD
    lol = log --graph --decorate --pretty=oneline --abbrev-commit
    # Same as lol, but for all objects
    lola = log --graph --decorate --pretty=oneline --abbrev-commit --all
    cane = commit --amend --no-edit
    ca = commit --amend
    
    $ git lol
    * 2deb5cdf (HEAD -> master, origin/master, origin/HEAD) Updated recoveryservices backup tests
    * 97064ed9 (tag: azure-mgmt-cosmosdb_0.2.1, test) azure-mgmt-cosmosdb 0.2.1
    *   39ab3a57 Merge pull request #1547 from lmazuel/multiapi_fix
    |\  
    | * 0489ff93 Remove subscription_id as a str requirement in multiapi package
    * |   967e4bc2 (tag: azure-mgmt-machinelearningcompute_0.2.0) Merge pull request #1539 from AutorestCI/RestAPI-PR1868
    |\ \  
    | * | fe7874eb Update history and bump version
    | * | 5b673df8 Generated from 4b65060d24c05de5c8776722618050fa85fc0363
    * | | 0e7d0a2c azure-mgmt-cosmosdb 0.2.1
    * | |   0e4a9383 Merge pull request #1549 from AutorestCI/RestAPI-PR1875
    
  4. Start using relative commit refs
    Remember in git HEAD and branch names resolve to a commit. There might be cases when you have to refer commits like parent of HEAD, grandparent of HEAD etc.
    • HEAD -> Resolved to HEAD
    • HEAD~1 -> Parent of HEAD
    • HEAD~<n> -> nth parent of HEAD. E.g. HEAD~3 points to parent of grandparent of HEAD.

    This works for branch names as well. So you can refer commits like master, master~1, master~2 etc.
    Merge commits have 2 parents and ~ will resolve to first parent. To resolve to other parent in merge commits use ^ instead of ~. For non-merge commits ^ will also resolve to 1st parent.
    TIP: HEAD~ is shorthand for HEAD~1 and similarly HEAD^ is shorthand for HEAD^1

  5. Reflog FTW
    There will be instances when you’ll do reset --hard by mistake or do similar changes which will result into loss of data. Git has an awesome failsafe for that and it’s called reflog.
    When you run commands like reset --hard, git doesn’t actually delete commit objects from .git/objects directory. It just moves HEAD to specific commit. So unless you run git gc with expiration of reflog, you’re pretty much safe!
    reflog is just a log of whatever you did on local repo. So if you want to go for old state, you can use reflog to do that.
    $ git lol
    * 7af7c9e (HEAD -> master) epsum
    * 41538db Lorem
    * e0fa777 def
    * a3c7262 init
    

    Now let’s assume a ghost ran git reset --hard HEAD~2 and now first 2 commits are gone and HEAD is moved to third commit (surprisingly ghost has no clue about how to clear reflogs!).
    Let’s see what reflog has now:

    $ git reflog
    e0fa777 (HEAD -> master) HEAD@{0}: reset: moving to HEAD~2
    7af7c9e HEAD@{1}: commit: epsum
    41538db HEAD@{2}: commit: Lorem
    e0fa777 (HEAD -> master) HEAD@{3}: commit: def
    a3c7262 HEAD@{4}: commit (initial): init
    

    Note that reflog still has reference to deleted commits i.e. 7af7c9e HEAD@{1}: commit: epsum. So to bring back this commit just run git reset 7af7c9e, this will bring back all commits till 7af7c9e in history and show changes in git status. From here you can manually fix/checkout files. You can also do reset --hard while recovering, but that will delete all of your local changes as well.
    By default reflog expires in 90days. So recover your data in 3 months or it will be permanently deleted!

  6. 3-Way Diff
    git config --global merge.conflictStyle diff3
    

    Git by default uses 3-way merge to run merge, but it shows conflicts in old RCS style i.e. it shows only your changes and changes to be merged. In 3-way diff, it shows your changes, changes to be merged and original content in conflict.
    E.g.

    • 2-way diff
      Here are lines that are either unchanged from the common
      ancestor, or cleanly resolved because only one side changed.
      <<<<<<< yours:sample.txt
      Conflict resolution is hard;
      let's go shopping.
      =======
      Git makes conflict resolution easy.
      >>>>>>> theirs:sample.txt
      And here is another line that is cleanly resolved or unmodified.
      
    • 3-way diff
      Here are lines that are either unchanged from the common
      ancestor, or cleanly resolved because only one side changed.
      <<<<<<< yours:sample.txt
      Conflict resolution is hard;
      let's go shopping.
      |||||||
      Conflict resolution is hard.
      =======
      Git makes conflict resolution easy.
      >>>>>>> theirs:sample.txt
      And here is another line that is cleanly resolved or unmodified.
      

      Here text between ||||||| and ======= is the original content changed by both you and someone else in same place. Having a view of original content makes merge decisions easy.
      Both examples taken from https://git-scm.com/docs/git-merge

Best practices to make every life easier with git

If working in large team, better work on your private feature branch

When you’re working in large team, avoid working on main branches. As everyone will merge their changes to main branches and that will create problem for you during pull, rebase etc.

Keep commits small, readable and to the point.

Avoid making commits for fixing typos, fixing small stuff that you missed in last commit etc. It is better to amend your changes in last commit using git commit --amend command or squash commits into single useful commit.

  1. Amending Commit
    $ git commit --amend      # Add changes to last commit. This will prompt for commit message re-writing
    # If there are no changes then commit --amend will prompt for commit message re-writing 
    $ git commit --amend --no-edit        # Add changes to last commit without re-writing commit message
    # If you've configured aliases, you can use following short notations
    $ git ca
    $ git cane
    
  2. Squashing Commits
    $ git lol
    * a8ac30e (HEAD -> master) Fixed a typo
    * f969ca9 Added completed Get-Git blog
    * a517835 [WIP] Get Git
    * 1872550 (origin/master) Added all assets of athena
    # Here I want to squash first 3 commits into single one
    
    $ git rebase -i HEAD~3
    # The output will be following in default text editor
    pick a517835 [WIP] Get Git
    pick f969ca9 Added completed Get-Git blog
    pick de97cb9 Fixed a typo
    # Rebase 1872550..de97cb9 onto 1872550 (3 commands)
    #
    # Commands:
    # p, pick = use commit
    # r, reword = use commit, but edit the commit message
    # e, edit = use commit, but stop for amending
    # s, squash = use commit, but meld into previous commit
    # f, fixup = like "squash", but discard this commit's log message
    # x, exec = run command (the rest of the line) using shell
    # d, drop = remove commit
    #
    # These lines can be re-ordered; they are executed from top to bottom.
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    #
    # However, if you remove everything, the rebase will be aborted.
    #
    # Note that empty commits are commented out
    

    This command is very powerful. Here you’ve various options to play with commits, unfortunately we’re interested in squash only. To squash change pick to squash or s. You can’t squash a commit without previous commit, so here you can’t squash commit a517835 as it doesn’t has any previous commit. We’ll edit this in prompted editor as per following:

    pick a517835 [WIP] Get Git
    s f969ca9 Added completed Get-Git blog
    s de97cb9 Fixed a typo
    # I've removed comments for readability
    

    After this, save the file and close it, and then you’ll be prompted with another message in editor like:

    # This is a combination of 3 commits.
    # This is the 1st commit message:
    [WIP] Get Git
    # This is the commit message #2:
    Added completed Get-Git blog
    # This is the commit message #3:
    Fixed a typo
    

    Whatever you write here, will be used as commit message for squashed commits. You should provide a readable and useful commit message. After editing this file, save it and close it. And this is how you squash commits. Try a git lol to validate squash:

    $ git lol
    * aef36a9 (HEAD -> master) [WIP] Get Git
    * 1872550 (origin/master) Added all assets of athena
    

    Notice 2 commits are gone and hash of first one is changed!

Checkout my github projects at DheerendraRathor