Objectives

Expand on git branching & merging features with more advanced git commands and concepts

(Recap) Saving Changes and Branching

Saving

Saving changes is a two-step process in Git:

# Stages changes to the staging area
git add <files>

# Commits a snapshot of changes to the local repo
git commit

Adding

  • Adds changes to the staging area
  • Does not really affect the working tree
  • Changes are not recorded until committed
# Add just these file(s)
git add app.js

# Add all changed files in this directory and sub-dirs
git add . 

# Glob and stage a pattern of files
git add **/*.js

# Unstage the changes to app.js
git reset app.js

Committing

  • Commits the staged snapshot to the local repository
  • Include a meaningful commit message
  • Logically group your changes into separate commits
# Commits the staged snapshot - will open editor for a message
git commit

# Commits with a message
git commit -m "Make a meaningful change"

Twofer - commit and add in one command:

# Stage & commit all changed files with a message
git commit -am "FH-12345 - My files on disk are perfect"

Branching

A branch in git is simply a reference to a commit

# List all local branches
git branch

# Create a new branch locally
git branch mynewbranch [myoptionalbasebranch]

# What commit is a branch pointing at?
git rev-parse mynewbranch

# What commits are on a branch?
git log mynewbranch [--pretty --graph --oneline --decorate]

# What branches point at a particular commit?
git branch --points-at e29b7ee

# Delete a branch
git branch -d mynewbranch

# Really delete a branch, even if it has changes that aren't on any other branches
git branch -D mynewbranch

There are also "remote-tracking" branches These are read-only copies of what is on a "remote", at the time that we last "fetched" (updated the information we store locally about branches on the remote).

# Get new information from all remotes
git remote update [--prune] # or: git fetch --all [--prune]

# List all remote-tracking branches
git branch -r

# List all branches (local and remote-tracking)
git branch -a

# Track a remote branch
git branch [branchname] -u origin/anybranch

# Now status will show the relationship with
git status [-sb]
stdout: "Your branch is ahead of 'origin/anybranch' by 3 commits"

# Delete a branch from a remote
git push origin --delete mybranchname

Rewriting History

Prerequisites

# Clone the forked repository
git clone git@github.com:<github_username>/git-tutorial.git

Overview

Why rewrite history?

  • Git is not only a source control tool, it is also a communication tool
    • Order of commits, logical grouping, good commit messages help to communicate changes
    • Think of the person reviewing your PRs as a potential axe-murderer. Axe murderers hate fix-up commits.
  • Golden rule: Never rewrite history on shared/public trees

Rebasing

Rebasing and Merging are two methods that achieve the same goal - the integration of changes from one branch into another branch

rebase on master rebase on feature

Practical

cd git-tutorial # From earlier
git fetch
git checkout rebase

# The rebase branch was branched from master a while ago
# It's out of date (master has moved on)
# And there's bad commits in our history that we're going to fixup
git log --pretty=oneline

# First, lets interactively rebase our last 4 commits
git rebase -i HEAD~4 # Note the lack of a branch

# Fix up the commits - think about re-ordering or (s)quashing
# the version bumps and (r)eword the commit messages. 
# Also, delete commits that are of no value
git rebase origin/master

# Alternatively:
# We can do both a history rewrite and a rebase via:
git rebase -i origin/master

Stashing

Move changes in a dirty workspace away to the side

# You have a dirty workspace
$ git status -s
 D .eslintrc.json
?? .eslintrc.yaml

# "stash" those changes
$ git stash save "Experimental work with eslint"
Saved working directory and index state On master: Experimental work with eslint
HEAD is now at 645ba47 Merge pull request #884 from...

# See what stashes you have in this repository
$ git stash list
stash@{0}: On master: Experimental work with eslint
stash@{1}: kubernetes client test stuff
stash@{2}: WIP on master: d09a007 Merge pull request #768 from...

# Your workspace is now clean...is it?
$ git status -s
?? .eslintrc.yaml # this file isn't tracked by git yet, so not stashed by default

# Restore top/first stash (stash@{0}) & remove it from the list
$ git stash pop # == apply && drop

# Back to normal
$ git status -s
 D .eslintrc.json
?? .eslintrc.yaml

# 'save' is the default stash action -- needed if you want to include a message
$ git stash save -u "Stash all including untracked"

# Bring it (or another stash) back
$ git stash apply stash@{0}

# Remove the stash if it's no longer needed
git stash drop stash@{0}

# Or make a new branch & restore your changes there
git stash branch <branchname> [stash@<revision>]

Tagging

  • There are two types of git tags - annotated and lightweight
  • We’re going to only cover lightweight tags today (you’ll see these more often)
  • A lightweight tag is like a branch that does not change
    • Pointer to a specific git-ref.
    • Unlike annotated tags, it is not a full object in the Git DB (not annotated, not-checksummed, not signed, no metadata about the creator)
  • Lightweight tags - under the covers
# LW tags are files on disk with a pointer to a commit
ls .git/refs/tags/

# Cat to reveal pointer to a commit
cat git/refs/tags/v99.0.0

# Show this commit
git show 659220

Creating a tag

# On a branch or any kind of git-ref
git tag v1.2.3-your-name

# Check filesystem (ls .git/refs/tags)
# Git does not push tags by default to a remote
# Or `git push --tags`
git push origin v1.2.3-your-name

# Checkout a tag
git checkout v99.0.0

# Create a branch from a tag
git checkout -b version-nine-nine v99.0.0

# Delete tags /!\ Careful now  /!\
# Local only
git tag -d v99.0.0

# Delete from remote
git push origin :refs/tags/v99.0.0

# A little bit about annotated tags
git tag -s v0.99.0-annotated -m "my tag v0.99.0-annotated"
git show v0.99.0-annotated

Cherry Picking

Apply the changes introduced by some existing commits

Our main use-case for git-cherry-pick is for taking some bugfix commits from one branch, and applying them to another (e.g. a release branch and master)

# Ensure that you're on the branch you want to copy commits to
# and the working directory is clean
$ git status
On branch cp-example
nothing to commit, working tree clean

# Bring a commit from a different branch over
git cherry-pick dea2da

# Notice that the changes, author details, and commit message are the same
# But the commit sha1 is different (and maybe the tree object)
$ git log -1 # or git show
commit b2d900443cb628f8d5c6accc271e7b66aaadb9ca
Author: Jason Madigan <jason@jasonmadigan.com>
Date:   Thu Dec 1 16:42:17 2016 +0000

    name change

Hooks

Do something before or after a particular action

  • Server-side hooks: we can't use them because of Github. Use webhooks instead
  • Client-side hooks:
    • 'git am' hooks: we don't use 'am', so we don't care
    • Commit:
      • post-commit
      • commit-msg
      • prepare-commit-msg
      • pre-commit
    • Others
      • pre-rebase
      • post-checkout
      • post-merge
      • pre-push
      • pre-auto-gc
      • post-rewrite
# everything you need to know about each hook
man githooks
  • Hooks are stored in .git/hooks/ in your repo
  • Hooks can be scripts in any language, and can run any programs available on the system

Example pre-commit hook to lint code, stored in .git/hooks/pre-commit

#!/usr/bin/bash --login

eslint ./lib

Config

Config is layered: system, global (user), local (repo)

# System
cat /etc/gitconfig

# Global (user)
cat ~/.gitconfig # or ~/.git/config

# repo-specific
cat ./.git/config

Set defaults for all your projects in ~/.gitconfig, either by editing the file directly, or by using the config subcommand. E.g.:

# Set a sane editor (otherwise uses value from $EDITOR)
$ git config --global core.editor emacs

# Set a custom commit message template
git config --global commit.template ~/.git-commit-template

Gerard's current ~/.git-commit-template file: https://git.io/vQJzx

Most interesting is the user's global config

[user]
    name = Gerard Ryan
    email = gerard@ryan.lt
    signingkey = 8A617903604095DC

[core]
    excludesfile = ~/.gitignore
    quotepath = false
    autocrlf = input
    safecrlf = warn
    editor = emacsclient -t -a emacs

[hub]
    protocol = ssh

[commit]
    template = /home/grdryn/.git-commit-template
    gpgsign = true

[gpg]
    program = gpg2

Aliases are really useful

[alias]
    br = branch
    co = checkout
    ci = commit -a
    d = diff --color-words
    st = status
    lol = log --graph --decorate --all --abbrev-commit --pretty=oneline
        lg = log --graph --all --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative
    scrub = !git reset --hard && git clean -fd

Lots of other useful aliases and config customization examples here:

https://github.com/matthewmccullough/dotfiles/blob/master/gitconfig

Bisecting

  • git bisect helps you figure out when breaking changes were introduced
  • Uses a binary-search between one changeset and another to quickly narrow breakage to a single commit

Practical

In the git-tutorial repo:

git checkout bisect
# Our bisect branch is a little ahead of of master (500 commits)
# At some point, a commit slipped in that means our app doesn't start
# and our tests fail.
# Lets use git bisect to find the broken commit
git bisect start
# Step 1 - find a commit where things are working
# I have no idea, so lets cast a wide net (across 500 commits)
git bisect good cddd186 # update to 3.0.0
git bisect bad bf0c5b1  # update to 502.0.0
# Run `npm test` to see if our tests fail
# If they fail - run `git bisect bad`
# If they pass - run `git bisect good`
# When done, reset via `git bisect reset` & patch the bug

Under the Covers

$ tree -L 1 ./.git
./.git
├── branches
├── COMMIT_EDITMSG # Text of most recent commit message
├── config         # Local configuration specific to this repo
├── description
├── FETCH_HEAD     # Info about what was fetched from remotes
├── HEAD           # Special ref that points to current ref in ./git/refs
├── hooks          # Dir containing scripts that run at specific events
├── index          # Binary representation of the index/cache/staging area
├── info           
├── logs           # Dir containing info used by reflog
├── objects        # Dir containing all "objects" (loose or packed)
├── ORIG_HEAD
├── packed-refs    # (compressed)
├── refs           # Branch, tag, remote, stash info
└── rr-cache
  • The stuff outside the .git directory is the checked-out working directory.
  • Only .git contents (expanded) exist on server: bare repo.

COMMIT_EDITMSG

COMMIT_EDITMSG can be useful with the hub command to create a pull-request on github with a single command

# Create a PR against master branch of the repo in the feedhenry org
# with the contents of the most recent commit message as the description
git pull-request -b feedhenry:master -F ./.git/COMMIT_EDITMSG

Git objects - what are they?

  • Blob - Think of blobs as file contents (deflated) with a size & type header.
  • Tree - Trees refer to blobs (and other trees), similar to how a directory points to files (and child directories).
  • Commit - Commits consist of a tree, parent commits, metadata (message, author details, time, etc).
  • Tag - An annotated tag is an object type that points to another object, and contains metadata such as timestamp, tag name, description, tagger and gpgsig. Most commonly refers to a commit.

Inspecting Objects with git cat-file

# What type of object is it?
$ git cat-file -t $(git rev-parse HEAD)
commit

# What size is it (in bytes)?
$ git cat-file -s e29b7eef219e38d005b1360214322a336a56729e
1079

# Pretty-print it
$ git cat-file -p $(git rev-parse HEAD)^
tree 2320f5bb8cf059571ed69d705ba792c7939296f5
parent e67aeb6f2a1c521f62106ede0b53f3a30ce57a36
parent d4c13babe4fb19550455d96cd067a19f745d999e
author Jason Madigan <jmadigan@redhat.com> 1480413570 +0000
committer GitHub <noreply@github.com> 1480413570 +0000

Merge pull request #2 from fheng/readme-updates

README updates

Crazy example with no files or porcelain commands

Porcelain provides a more user-friendly interface to the plumbing https://git.io/v1Bt5

# Create a new branch from master & checkout
git co -b ungit master

# Make a new blob object
blob_sha1=$(echo "Let's ungit" | git hash-object -w --stdin)

# Add the blob as a file in the index
git update-index --add --cacheinfo 100644 ${blob_sha1} ungit.txt

# Make a tree object from the current index
tree_sha1=$(git write-tree)

# Make a commit object with the newly-created tree
commit_sha1=$(echo "Ungit all the things" | git commit-tree -p HEAD ${tree_sha1})

# Update the current branch with that new commit
git merge ${commit_sha1}

# WTF, where's my ungit.txt?!
ls && git status

Reflog - git's safety net

Git commands that change the state of the local repo will write an entry to the reflog

git reflog # git log -g --abbrev-commit --pretty=oneline
a21f4c4 HEAD@{0}: merge a21f4c412fa194021faa859193380a26b54a024d: Fast-forward
c838b24 HEAD@{1}: merge c838b2447de706f53eaabed16c371abbb6d82b03: Fast-forward
e29b7ee HEAD@{2}: checkout: moving from cp-conflict-example to ungit
bf196ea HEAD@{3}: commit (cherry-pick): Bumping version to 2.0.8
e29b7ee HEAD@{4}: checkout: moving from cp-example to cp-conflict-example
b2d9004 HEAD@{5}: checkout: moving from master to cp-example
e29b7ee HEAD@{6}: checkout: moving from cp-example to master
b2d9004 HEAD@{7}: cherry-pick: name change
e29b7ee HEAD@{8}: checkout: moving from master to cp-example
e29b7ee HEAD@{9}: clone: from git@github.com:fheng/git-tutorial.git

# Checkout a previous state
git checkout HEAD@{2} # detached head, consider creating a branch here if important

# Reset current branch to an earlier state
git reset [--hard] HEAD@{4}