GIT Source Control

GIT is a software configuration management (SCM) tool. This page offers notes on some of the more useful GIT commands and workflows such as merging, rebasing, hunks and patches.

Page Contents

References

  1. GIT SCM.
  2. Pro Git eBook.
  3. Ry's Git Tutorial, Ryan Hodson.
  4. Git Basics - Undoing Things, Git Docs.
  5. Undoing Changes, Atlassian Git Tutorial Page.
  6. How to undo the last commits in Git?, SO Thread.
  7. Awesome page... just found this and it looks great: Visualizing Git Concepts with D3. Visit this page, it is really cool!

To Read / To Do

https://stackoverflow.com/questions/35941566/delete-remote-branch-via-git
https://stackoverflow.com/questions/1889559/git-diff-to-ignore-m
https://stackoverflow.com/questions/1822849/what-are-these-ms-that-keep-showing-up-in-my-files-in-emacs
https://stackoverflow.com/questions/1510798/trying-to-fix-line-endings-with-git-filter-branch-but-having-no-luck/1511273
https://stackoverflow.com/questions/20106712/what-are-the-differences-between-git-remote-prune-git-prune-git-fetch-prune

Where Git Finds Your SSH File

I had a little problem on Windows. When I connected from my office location Git would fail to SSH into GitHub. When I connected from home Git would connect to GitHub with no problem whatsoever.

The reason for this is that when connecting from the office my HOME DIRECTORY changed and git was looking for my key files relative to the home directory!

The solution was found in this SO thread answer. The solution is to create a shell script that will involke the ssh client with the a parameter to tell it where to look for key files: give this parameter an obsolute path that is not relative to HOME.

#!/bin/sh
ssh -i /absolute/path/to/key/folder/id_rsa $*

In the above script id_rsa is the key filename. You might have saved your private key with a different file name. Save the script somewhere and give it executable permissions (chmod +x filename).

Then set the environment variable GIT_SSH to the absolute path to the script you just created.

export GIT_SSH=/path/to/your/script.sh

Now your git push operations etc should work just fine.

Create A Public/Private Keypair And Add To Your Keyring

References:

Check your home directory (if on a corporate network it might not be what you expect).

echo $HOME
echo ~

To create SSH key pair:

ssh-keygen

To see your public key type:

cat ~/.ssh/id_rsa.pub

To check that SSH is working...

ssh -T git@bitbucket.org
# or...
ssh -T git@github.org

To see if the key has been added to the client keyring...

ssh-list -l
# or...
ssh -vT address-of-server

Followed the GitHub instructions to create the private/public RSA key pair. Having obtained this key it can be added to the keyring using the following, but this addition is not permanent.

ssh-add ~/.ssh/some_key_name

So to make the addition permanent I consulted this SO thread. When you created the OpenSSH key pair you probably stored it as ~/.ssh/some_key_name (the private key) and ~/.ssh/some_key_name.pub (the public key). To add these permanently to your keyring edit the file ~/.ssh/config (and if it is empty just create it). Add the following line (replicate once for each key you wish to permenantly add):

IdentityFile ~/.ssh/some_key_name

Git Workspace v.s. Index v.s. Repository

Diagram of Git worspace vs index vs respository

Configuring Git

Setup your user name and email globally:

git config --global user.name "name"
git config --global user.email "email"

You can override the global settings for specific repos by chaning to the repo's root and running:

git config user.name "name"
git config user.email "email"

Setup your editor and diff tool:

git config --global core.editor gvim
git config --global merge.tool vimdiff

To see your config setup:

git config --list

To see a specific key’s config:

git config key-name

For example...

git config user.name

Search (Grep) Your Repo

Using git grep ignores git interal files and generated files, which can make your grepping life a lot easier!

Cheat Sheet Of Basic Git Commands

Creating Git Repos

To create a repo in the CWD:

git init

Cloning Git Repos

To clone a repo:

git clone https://repo_addr [dirname]
git clone git://repo_addr [dirname]

This pulls down data for the repo into dirname, if provided, or to an automatically decided dirname in the CWD otherwise. Note you get the entire repo including all the history copied locally. A remote called "origin" will be automatically added to point to https://repo_addr.

Checking Out Repos

Checking out a commit makes the entire working directory match that commit. This can be used to view an old state of your project without altering your current state in any way. Checking out a file lets you see an old version of that particular file, leaving the rest of your working directory untouched.

git checkout commit-hash | tag-name | branch-name

Git Branches

A branch is lightweight moveable pointer to a commit and the default branch name is "master". HEAD points to current branch head, so changes when you change branch.

To list all the branches in the repo type:

git branch

You can filter the list with the --merged and --no-merged options to see branches that are not merged back in etc.

To create and checkout a new branch use:

git branch new-branch-name
git checkout new-branch-name

The shorthand for the above is:

git checkout -b new-branch-name

To "delete" a branch (branch history still exists and is recoverable):

git branch -d branch-name

To switch branches use:

git checkout branch-name

Now, HEAD will point to new branch and NOT prev branch.

Note that switching branches changes files in your working directory. When you switch branches, Git resets your working directory to look like it did the last time you committed on that branch.

Adding Files To Your Git Repo

You must add files from your workspace into the Git index so that Git knows what to track...
git add .  # adds all files (tracked or not) in cwd
git add -u # puts all changed tracked files into staging area (i.e., wont add new files)
git add -A # does both of the above

Note that if you modify a file after staging, the modifications after the stage will not be committed in this commit unless added again.

Committing Files

Each commit is pointer to snapshot plus author/committer details, msg etc and pointer to prev commit (or commits if this is a result of merge).

To commit all staged changes:

git commit -m "your msg here"

To commit all tracked files, staged or not, use:

git commit -a -m "..."

Any tracked file that is not staged will be automatically staged.

To ammed the last commit, adding in all currently staged files use:

git commit --amend

This will give you the chance to modify the last commit (launches text editor) and will also add any currently staged files into the last commit. For example:

git commit -m "a commit where I forgot to add a file"
git add forgotten_file
git commit --amend

Git Logs: Viewing Your History

To get a summary of changes made with author, date, full commit hash and description:

git log

To get a one-line-per-commit summary (short hash and description):

git log --online

To get a log between versions/tags/commits/nranches etc:

git log chng..chng

where chng can be a tag name, a commit's hash, a branch name etc.

To limit the log to the last 5 entries:

git log -5

To view the differences introduced in each commit:

git log -p

To view the logs for the last two weeks:

git log --since=2.weeks

Use "<number>.<time-period-type>". So, for example, the time period could also be "days" or "hours".

To grep the log for commits that introduces a change to the code that added or removed a search string:

git log -S<search-string>

To really pretty-print your logs:

git log --oneline --decorate --graph --all

To show the log with full path names and status of changed files:

git log --name-status

To show the log with abbreviated pathnames and a diffstat of changed files:

git log --stat

Tagging Files With Git

Tags are "nice" names by which you can identify snapshots of a repo. For example, you will likely tag releases.

git tag -a <tag-name> -m "Description of tag"
        ^^
        -a means create an annotated tag (stores name, date and msg)

To list all the available tags in your repo:

git tag

To search for tags with names matching a given pattern:

get tag -l <pattern>

An important point to note is that git push does NOT transfer tags to remote servers! You must do this explicity using git push origin <tagname>

Undoing Things

This section remains a little cheat sheety in style. I'm going to expand on it in a later section...

Revert unstanged changes:

Kinda like reverting a file in the SVN sense

git checkout -- filename

By refering to the diagram describing the working directory vs the index we can understand what this is doing. We have modified a local file but not staged the changes yet. By checking out the file we're just overwriting the changes in the working directory, which Git, because we haven't staged these changes, knows nothing about.

To unstage a file:

git reset HEAD -- filename

This resets the index entry for "filename" to its state at HEAD, therefore removing it from the index. Note this changes the index entry, which is distinct from your working directory. The file in the working directory is not touched.

To undo all uncomitted changes to tracked files:

git reset --hard

The option --hard resets (to the most recent commit) the index and the working tree.

A Note About Reset:

A git reset actually alters your history and can only work backwards from the current commit. By altering the history you could potentially loose history. Also once a change is pushed to a shared repository, resetting the change afterwards can become troublesome!

If git revert is a "safe" way to undo changes, you can think of git reset as the dangerous method. When you undo with git reset(and the commits are no longer referenced by any ref or the reflog), there is no way to retrieve the original copy — it is a permanent undo. Care must be taken when using this tool, as it’s one of the only Git commands that has the potential to lose your work.

To remove a file from repo withOUT deleting it locally:

git rm --cached myfile.name

To remove untracked files use:

git clean -f

To undo a commit use:

git revert commit-id

The git revert command undoes a committed snapshot. But, instead of removing the commit from the project history, it figures out how to undo the changes introduced by the commit and appends a new commit with the resulting content. This prevents Git from losing history, which is important for the integrity of your revision history and for reliable collaboration.

Listing Files In The Git Repo

To list files currently being tracked on branch master:

git ls-tree -r master [--name-only]

The above will list all files and directories in master, recursively, that are being tracked. Each line output is for one file and consists of the file's latest comment hash and the file path. If --name-only is used then the commit hash is ommitted.

To do the same, but list on the current branch:

git ls-tree -r HEAD --name-only

Diffing With Git

git diff: What you have changed but NOT staged
git diff --staged: What will go into next commit
git diff --cached: Synonym for --staged
git difftool:      To use external diff tool

Git Parlance & Internal Structure

What Are Commits And How Do They Store Snapshots?

The Git Book on the internals of Git (Git Objects) has a good explanation of how Git repos are organised [Ref] [Ref].

To summarise a little:

  1. Commit - stores meta data (author, committer, comment) and pointer to the top level tree snapshot as well as the previous commit (or commits if a merge commit). The commit object itself is identified by a sha1 pointer. Note that we said "top level": the commit is a snapshot of your entire repo.
  2. Tree - corresponds to a directory - collection of pointers (pair of sha1 hash and file name) to file blobs and trees.
  3. Block - a binary blob representing a file with an associated sha1 hash.

Diagram of GIT commits, trees and blobs

Commit-ish & Tree-ish?!

Two terms you might see in the documentation are "commit-ish" and "tree-ish" [Ref].

"Commit-ish" means an identifier that references a commit object in the repo. For example a tag is an identifer that references a commit, as is the sha1 hash for that commit.

"Tree-ish" means an identified that references part of the repo's tree. Again, a tag can be "tree-ish" because it references a commit, from which we can access the files and folders that the commit is a snapshot of.

What is HEAD?

TODO:
    https://stackoverflow.com/questions/5772192/how-can-i-reconcile-detached-head-with-master-origin
    https://git-scm.com/book/tr/v2/Git-Internals-Git-References

The HEAD file is a symbolic reference to the branch you’re currently on. By symbolic reference, we mean that unlike a normal reference, it doesn’t generally contain a SHA-1 value but rather a pointer to another reference

Revert & Reset

As we have seen in previous quotes above, revert is a safer version of reset. The former creates a new commit which represents the changes needed to to revert the former HEAD to a previous commit. It does not change any of the previous commits.

A reset on the other hand has the potention to obiliterate information.

Resetting The Index (aka. The Staging Area)

One of the cheat-sheet snippets we saw earlier was git reset HEAD -- filename, which unstaged a file. From the Git help we get the following explanation:

git reset [-q] [<tree-ish>] [--] <paths>...

This form resets the index entries for all <paths> to their state at <tree-ish>. (It does not affect the working tree or the current branch.)

This means that git reset <paths> is the opposite of git add <paths>.

Output of "git reset --help"

The effect can be visualised as shown in the diagram below:

Diagram showing effect of git reset HEAD -- filename

Moving Your Branch HEAD Around & Maybe Modify The Working Tree + Index

Another form of the git reset command can be used to move the HEAD of the current branch around. In all forms the HEAD of the branch is moved to a specific commit. What happens to the index and woking tree depends on the mode in which it is used...

git reset [<mode>] [<commit>]

This form resets the current branch head to <commit> and possibly updates the index (resetting it to the tree of <commit>) and the working tree depending on <mode>. If <mode> is omitted, defaults to "--mixed". The <mode> must be one of the following:

  • --soft:

    Does not touch the index file or the working tree at all (but resets the head to <commit>, just like all modes do). This leaves all your changed files "Changes to be committed", as git status would put it.

  • --mixed

    Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) and reports what has not been updated. This is the default action.

    If -N is specified, removed paths are marked as intent-to-add.

  • --hard

    Resets the index and working tree. Any changes to tracked files in the working tree since <commit> are discarded.

  • --merge

    Resets the index and updates the files in the working tree that are different between <commit> and HEAD, but keeps those which are different between the index and working tree (i.e. which have changes which have not been added). If a file that is different between <commit> and the index has unstaged changes, reset is aborted.

    In other words, --merge does something like a git read-tree -u -m <commit>, but carries forward unmerged index entries.

  • --keep

    Resets index entries and updates files in the working tree that are different between <commit> and HEAD. If a file that is different between <commit> and HEAD has local changes, reset is aborted.

Output of "git reset --help"

Soft Reset: Move HEAD, Touch Nothing!

Mixed Reset: Move HEAD, Touch Only The Index

Diagram of a git reset --mixed command

Hard Reset: Move HEAD, Touch The Index And The Working Tree

Diagram of a git reset --hard command

Branches And Commits (v.s. SVN-like SCMs)

Creating branches in Git vs SVN

Add & Commit A Subset Of Changes: Hunks

I enjoyed learning about this because it is quite often that I find I've fixed two bugs or more in the same file. This can be because the two bugs are so tightly coupled that fixing one depended on another, or it was a quick fix for something I noticed on the fly and was so simple it wasn't worth creating a branch just for this fix. Either way, I end up with a file that contains changes that address multiple bugs/tickets.

Note: you can also accomplish the same thing using an interactive rebase.

Lets test it out. Create a new repo and add a test file...

$ git init learngit
Initialized empty Git repository in C:/Users/jh/Documents/Sandbox/learngit/.git/

$ cd learngit

$ echo "#include <stdio.h>
        return 0;
>
> int main(int argc)
> {
>         return 0;
> }
> " > test.c

$ git commit -m "Create a test file.

Create a test file so that in a future commit I can test patch adds"
[master (root-commit) f573b91] Create a test file.
 1 file changed, 6 insertions(+)
 create mode 100644 test.c

We've created a little git repository and added one file called test/c to it. Very very simple. Now let's spoof making two bug fixes by adding the following comments just above the return 0 statement:

/* This line fixes bug A*/
/* This line fixes bug B*/
/* This line fixes bug A*/

Okay, now I want to make two seperate commits as I'd like to have one commit that deals soley with the changes I've made to fix bug A, and another commit that deals soley with the changes I've made to fix bug B. This way, any one looking back through the logs will be able to easily understand which changes fix which bug.

To do this I use the following command:

git add -p test.c

This launches an interactive command line utility:

$git add -p test.c<
diff --git a/test.c b/test.c
index 312d3cd..36da841 100644
--- a/test.c
+++ b/test.c
@@ -2,5 +2,8 @@

 int main(int argc)
 {
+       /* This line fixes bug A*/
+       /* This line fixes bug B*/
+       /* This line fixes bug A*/
        return 0;
 }
Stage this hunk [y,n,q,a,d,/,e,?]? e

The GIT command line askes me whether I want to stage this hunk. What is a "hunk"? A hunk is just a set of changes. Currently, we can see above that all the changes in the file are grouped together because they app have a plus sign next to them, indicating that they will be added in one group.

I don't want the line for bug B to be added in this group. So I must split the current hunk into smaller hunks. I do this by entering "e", as shown. This will start the configured text editor.

To avoid committing the lines associated with bug B, I relpace the + symbol at the line start, with a # symbol as so (partial file in editor shown):

int main(int argc)
 {
+       /* This line fixes bug A*/
#       /* This line fixes bug B*/
+       /* This line fixes bug A*/
        return 0;
 }

Save and exit from the editor. The command ends. Now if I look at the GIT status, I see the following:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   test.c

Changes not staged for commit:
  (use "git add >file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   test.c

Oh la la! My file is both staged and unstaged. So have we staged only the changes for bug A? Lets find out by looking at what will go into the next commit:

$ git diff --staged
diff --git a/test.c b/test.c
index 312d3cd..67cc02e 100644
--- a/test.c
+++ b/test.c
@@ -2,5 +2,7 @@

 int main(int argc)
 {
+       /* This line fixes bug A*/
+       /* This line fixes bug A*/
        return 0;
 }

Sweet! So now I can commit these changes, then add the remaining bug B changes and commit them in a seperate commit... happy days!

BUT before I do this, I should really make sure that the changes that I have staged are good: they produce a compilable set of changes that don't break the build and the changes do in fact do what the commit message associated with them says. To do this I would like to save my unstaged changes and take them out of the workspace, whilst leaving the staged changes in the workspace:

$ git stash --keep-index

Now I can review/test etc the staged changes in isolation. Once I'm happy I can run a git commit, and then to restore the remaining unstaged stuff I can run:

$ git stash pop

I can now repeat these steps for the bug B changes and so one...

Integrating

Fast forward

Fast forward merge of branches in Git

From the master directory you would merge in the new feature. But because the history has not divereged Git can just fast forward the master's HEAD to point to the latest commit.

Would be done using something like this:

# create a new branch of master
git checkout master
git checkout -b new_feature

# do some development in the new branch and then commit
git commit -m "Some new development, a fix, whatever"

# Go back to master and merge in the "new_feature" branch...
git checkout master
git merge new_feature

# Cleanup feature branch
git branch -d new_feature

Merging

Git will do 3-way merge using the common ancestor. In this case, because the history between the master and feature branches has diverged, a simple fast-forward integration can not be performed. Instead a new commit in the master branch is created that exists soley to identify that a merge has taken place.

Merging branches in Git

Would be done using something like this:

# create a new branch of master
git checkout master
git checkout -b new_feature

# do some development in the new branch and then commit
git commit -m "Some new development, a fix, whatever"

# !! Meanwhile someone else creates extra commits on the master branch !!

# Go back to master and merge in the "new_feature" branch...
git checkout master
git merge new_feature

# Cleanup feature branch
git branch -d new_feature

A lot of people (including me) like these kinds of integration. Its the typical way merging is done in other revision control systems like SVN, for example. All the history is preserved and it is clear that this commit represents a merge.

However, a counter argument is that we might not want to "pollute" our master branch with commits that only represent a merge. The idea is that the master line should show a story where each commit is some extra feature or bug-fix. We shouldn't see a ton of merge-commits (could be a ton in a busy master branch!). We only want to see the story, and that story should course-grained (i.e, for a bug fix only one extra commit rather than the many commits that may have been made in the development branch). This is why a lot of Git users prefer rebasing (see below).

Rebasing

The Git world offers something that CVS, SVN, Perforce etc users won't be as familiar with... rebasing. It gives you a cleaner (no merge commits polluting the project history) and a more linear project history by taking changes you made in your development branch, possibly combining several dev changes into one single commit on the fly, and "appending" them to another branch as-if the changes we made on that branch.

The Vanilla Rebase

Rebase branches in Git

We have used the following syntax:

git rebase <target-branch> <src-branch>

Which is equivalent to doing...

git checkout <src-branch> && \
git rebase <target-branch> 

I like the first variant because it makes sure you have the correct branch checked out before you do the rebase. When you use rebase with only the "target-branch" branch, you are rebasing currently checked-out branch onto the target.

Use git rebase <target-branch> to rebased the current branch onto "target-branch".
Use git rebase <target-branch> <src-branch> to rebase the "src-branch" onto the "target-branch" by checking out "src-branch" first and then doing the rebase.

Can investigate this using this simple little shell script that tries to replicate the above. It has two command line uses: either "test_vanilla_rebase.sh master onto new" or "test_vanilla_rebase.sh new onto master".

Interative Rebasing: Squashing Commits

The following notes should be used with a ton of caution! You might even consider making temporary backups before starting this work!

The notes in this section cover commands that can completely obliterate files from your repository. Use with absolute caution! Make backups first!

In the example above where the new feature is rebased onto the master branch, 3 commits are added to the master for the one new feature. We might like to tidy this up a little and only have one new commit in the master branch that is the aggregation of all of the commits done during development. This way, the master branch is kept even neater with only one change (as we probaly don't care to much about the intermediate development steps - the detail - in the main master branch story... it just wants to capture that a new feature was added, not the nitty gritty detail of its development stages).

Ideally we would do this:

Interactive rebase of branches in Git with commit squashing

And this is what we can achieve using interactive rebasing in Git! Whoop whoop...

We can do the interactive rebase using:

git rebase -i master new

Which, remember, is the same as doing:

git checkout new
git rebase master

When we do this Git will launch an editor that will allow you to pick the commits from new that are going to be replaying on top of master. If we take our simple little shell script and add a "-i" to the rebase command and rebase new onto master interactively we will get the following in a text editor:

pick <hash> First new feature
pick <hash> 2 new feature
pick <hash> 3 new feature
pick <hash> 4 new feature

If we want to combine everything into one commit, simply change the text to the following and save and exit:

pick <hash> First new feature
squash <hash> 2 new feature
squash <hash> 3 new feature
squash <hash> 4 new feature

Or lets say, for example if we want to combine the commits "First new feature" and "2 new feature" as a single commit but leave the others alone we would do:

pick <hash> First new feature
squash <hash> 2 new feature
pick <hash> 3 new feature
pick <hash> 4 new feature

Basically the "squash" command will combine the commit on that line with every commit above it, until it hits a "pick" line.

If you delete a line from the rebase listing its contents is lost forever... eek!

Interative Rebasing: Editing Commits

The following notes should be used with a ton of caution! You might even consider making temporary backups before starting this work!

The notes in this section cover commands that can completely obliterate files from your repository. Use with absolute caution! Make backups first!

Lets say we run this script to setup a sandbox repo in which to have a little play. We would end up with the following commit log:

de5fcb1 Third commit about something
466318b Second commit about something, with an error... oops
fca8f1a First commit about something

We have traced our problem to the second commit and now we want to ammend it. Maybe it was something simple like changing the commit message or the author. But maybe it is something more complicated like a code change that will be required. Either was it doesn't matter. We can rebase our own branch like so:

git rebase -i HEAD~2
              ^^^^^^
              Rebase starting after the second commit before HEAD.
              In this case it is that same as typinh
                 git rebase -i fca8f1a

Note that with this command we cannot change the first commit. If we wanted to do that we could use the option --root, but I'm leaving that one out as I can't see myself ever using it.

So let's rebase. Git will launch our editor and give us the following:

pick 466318b Second commit about something, with an error... oops
pick de5fcb1 Third commit about something

Change the first "pick" to "edit", and exit the editor saving changes. Observe the Git message:

warning: stopped at 466318b... Second commit about something, with an error... oops

Git is replaying the history of our branch. It's as-if it rewound the branch to its state at commit fca8f1a (the first commit) and then applied the changes made in commit 466318b (the second commit), but before commiting those changes, it is giving us a chance to make further changes. Any changes we make will be written as part of this commit, as-if we had actually made them back in time when we actually did the second commit!

We're now in a kind-of "temporary world" where we can edit this commit before it is applied. If we cat the file we see:

First line
Second line with an error

So we know that the change for the commit we are "paused" on has been applied.

If we look at the log, we also see our log message:

$ git log --oneline
466318b Second commit about something, with an error... oops
fca8f1a First commit about something

So, we can see Git has "paused" the rebase on this commit. Although the commit message is in the log, it hasn't actually "gone through" yet! We have this time to modify the file or the log message as we see fit.

Let's edit the file to remove our error and change the second line from "Second line with an error" to just "Second line". To do this you must git add the file and then do a git commit --amend as follows:

git add file.txt
git commit --amend

Git launches our text editor. In the editor we see our commit message which we can now change to just "Second correct commit" and save.

Now you must remember to do git rebase --continue to make the changes permanent.

UH OH! We get a conflict:

First line
<<<<<<< HEAD
Second line
=======
Second line with an error
Third line
>>>>>>> de5fcb1... Third commit about something

Ah, the change we made conflicts with the change made in the final commit on our branch. At this point the rebase has again "paused", but this time the branch is in the state it would be when the third commit had been done. But as we have seen, Git cannot replay this commit automatically for us because a conflict has been introduced.

This is quite easy to fix. We just edit the file so it looks like this and save:

First line
Second line
Third line

Having resolved the conflict we need to git add file.txt, commit it and then remember to run git rebase --continue.

At this point we are told that our rebase has been successfull. If we cat our file we see the contents are as we have modified them to be and the log reflects this:

caeb871 Third commit about something
2867a5a Second CORRECT commit
fca8f1a First commit about something

You'll notice that the second and third commits now have diffent commit hashes. This is to be expected as the file contents have changed. Any files that we had been branched from any of the changed files, will be updated correctly to "point" back to the equivalent modified file.

Undoing A Rebase

So, here's a mistake I made doing a rebase to squash some commits on my branch. It's a nice example of using git reset --hard and git reflog. I had the following set of commits, which I've labeled #44 through #49. I wanted to keep commit #45 and squash #46 to #49:

Image for part 1 of description of how to undo a git rebase where files were squashed

I.e., I wanted to end up with the following:

Image for part 1 of description of how to undo a git rebase where files were squashed

But, I was a little careless with my rebase, and ended up with this:

Image for part 1 of description of how to undo a git rebase where files were squashed

Instead of doing a git rebase -i 45 I had done a git rebase -i 44, thus including commit 45 in my squash. As the figure above shows, HEAD now points to my newly created commit that squashes up commits 45 through 49. If I look at the log, I can no longer see individual commits 45 through 49. So, where have they gone and can I get them back?

The answer is yes. As long as I have not executed a git prune command, the individual commit chain still exists and is shown above by the grayed out commit nodes. These nodes still exist in the Git repo, its just that there is no "handle" or "named pointer", if you will, that we can use to refer to them by. So, how the, can we refer to them in order to tell Git that we want them back? Answer: git reflog.

"Reflog" is short for "reference log" and it records when the tips of branches were updated in the local repository [Ref]. Great, so this log should contain the history that describes when our branch HEAD moved from commit 49 to commit 45-49. This means we can do a hard reset to make both our working tree, index and HEAD pointer "go back" to commit 49 with all the commit history associated with it, as shown below.

Image for part 1 of description of how to undo a git rebase where files were squashed

Now our repository looks exactly as it did before we started messing around with rebasing. From here I could continue to do the correct rebase... all is well. Phew!

Using Rebase To Edit A Commit

Let's create a silly little repository:

Depiction of a Git repository with 2 commits
> mkdir deleteme
> cd deleteme
> git init
> echo "First commit line" > test.txt
> git add -A
> git commit -m "First commit"
> echo "First line of second" >gt; test.txt
> echo "Second line of second" >gt; test.txt
> git add -u
> git commit -m "Second commit"
> git log --oneline
5156c89 Second commit
e310460 First commit

Great, so we have two commits. The reflog looks like this:

> git reflog
5156c89 HEAD@{0}: commit: Second commit
e310460 HEAD@{1}: commit (initial): First commit

So, when we made our first commit, HEAD was set to point to it. When we made our second commit, HEAD was moved to point to that. What we'd expect...

Let's edit the second commit. We do this by starting a rebase from e310460.

git rebase -i e310460

The editor will pop up and allow us to change "pick" to "edit" for commit 5156c89. When we save and exit the rebase starts:

> git rebase -i e310460
warning: Stopped at 5156c89... Second commit

The reflog now looks like this:

> git reflog
5156c89 HEAD@{0}: rebase -i: fast-forward
e310460 HEAD@{1}: rebase -i (start): checkout e310460
5156c89 HEAD@{2}: commit: Second commit
e310460 HEAD@{3}: commit (initial): First commit

The log looks like this:

> git log --oneline
5156c89 Second commit
e310460 First commit

Okay, so we are now on the second commit. It appears in the log, but we are editing this commit. We can see what the rebase has tried to do by looking at the reflog, where we can see it intially checked out out the first commit

Git Filter Branch

The following notes should be used with a ton of caution! You might even consider making temporary backups before starting this work!

The notes in this section cover commands that can completely obliterate files from your repository. Use with absolute caution! Make backups first!

http://schacon.github.io/git/git-filter-branch.html

Eek! This little fella lets you do a batch modification of many commits!

Cherry Picking

When you want to take a single commit from one branch and apply it to another. Note, this is different from merging or rebasing because you are taking one specific commit and not a range of commits. Notice also that because this isn't a merge you will loose the ability to track where these changes came from as it will look like you just made these edits yourself!

git checkout branch-to-apply-commit-to
git cherry-pick [-e] [-x] <commit-ish>

Note: this does the commit for you!

Use the -e flag to edit the commit message prior to comitting.

The -x flag can be useful, because as said, you loose the ability to track where these changes came from: This flag will append a line that says "(cherry picked from commit ...)" to your commit message.

Hunting Down Bugs: Git Bisect

The following notes should be used with a ton of caution! You might even consider making temporary backups before starting this work!

The notes in this section cover commands that can completely obliterate files from your repository. Use with absolute caution! Make backups first!

TODO

Patches

Generating Patch Files

A patch file represents one single commit diff. Create using:

git format-patch (branch-name|since) -o dir_to_store_patches

This will create a set of files in the directory "dir-to-store-patches" with the following naming convention:

xxx-commit_msg.patch
^^^ ^^^^^^^^^^
^^^ The commit message for the commit that this patch represents
^^^ withs spaces replaced by underscores.
^^^
The commit number

If you substitute you branch name in the format-patch command line it will generate patches for every commit since the start of the branch.

If you specifiy a commit number in the format-patch command line, instead of a branch name, you will get the patches for every single commit in the current branch since that commit.

Applying Patch Files

To apply a patch to the currently checked out branch use:

git am < patch-file

This will create a new commit from the patch file.

Git Tips and Tricks

Many thanks to my colleague Kris Dunning for these. I've added a few over time but he kicked it off...

Checking for white-space

To check for white-space alone:

git diff --check

To check for white-space and tabs in indents:

git -c core.whitespace=tab-in-indent diff --check

Adding an Alias

This can be useful when you want to make a shorthand command for something you do frequently, or for long commands that are hard to remember.

For instance, the white-space and tabs in indent check above can be reduced to:

git whitespace

By adding the alias:

git config --global alias.whitespace 'git -c core.whitespace=tab-in-indent diff --check'

Useful Aliases

Whitespace Check: "git whitespace"

As above:

git config --global alias.whitespace 'git -c core.whitespace=tab-in-indent diff --check'
Status short cut: "git st"
git config --global alias.st status
Gitk Visualisation Window: "git visual"
git config --global alias.visual '!gitk --all HEAD &'
Git log with graph: "git lg"
git config --global alias.lg "log --graph --oneline --decorate"

With colour [Ref]...

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

Logs

To see all the commits that have been for a specific branch that was branched off "parent-branch-name":

# From the branch that was branched from "parent-branch-name"
git log --no-merges parent-branch-name..
                                      ^^
                                      These 2 dots are important!

The flag --no-merges tells Git not to print commits with more than one parent and will make the log stop at the point the branch is merged back to the parent branch.

Stashes

Stashes are for when you don't want to commit your current work, want to get on with something else, but don't, obviously, want to loose the current work in progress.

A stash is basically a snapshot of your uncommitted changes. This is different to a patch, which is a snapshot of you committed changes. Also, unlike a patch, you can not push a stash to a remote.

Picture describing the Git stash processes/usage

Interestingly you can do more than one stash and then re-apply them seperately. Use stash list to see a list of stashes. To apply a specific stash use something like git stash apply stash@{1}, which would re-apply the penultimate stash (the smaller the number the more recent the stash).

Although you can't push a stash to a remote, you can carry it over to another branch using git stash branch-name. This will switch you to "branch-name" and apply the stash to that branch. Sweet!

Hooks

Look at the .sample files in the repo's "hooks" directory in the repo root. Rename a sample without the .sample extensions to "activate" it.

Could use this as a way to auto-backup you git repo to a shared server, for example, or run pre-commit checks like syntax checking, linting etc.

For example, in my repo dir under .git/hooks I see the following files:

-rwxr-xr-x 1 jh 1049089  478 Jul 28 10:27 applypatch-msg.sample*
-rwxr-xr-x 1 jh 1049089  896 Jul 28 10:27 commit-msg.sample*
-rwxr-xr-x 1 jh 1049089  189 Jul 28 10:27 post-update.sample*
-rwxr-xr-x 1 jh 1049089  424 Jul 28 10:27 pre-applypatch.sample*
-rwxr-xr-x 1 jh 1049089 1642 Jul 28 10:27 pre-commit.sample*
-rwxr-xr-x 1 jh 1049089 1348 Jul 28 10:27 pre-push.sample*
-rwxr-xr-x 1 jh 1049089 4951 Jul 28 10:27 pre-rebase.sample*
-rwxr-xr-x 1 jh 1049089  544 Jul 28 10:27 pre-receive.sample*
-rwxr-xr-x 1 jh 1049089 1239 Jul 28 10:27 prepare-commit-msg.sample*
-rwxr-xr-x 1 jh 1049089 3610 Jul 28 10:27 update.sample*

If we look at pre-commit.sample we can read that it is an example hook script to verify what is about to be comitted. We could, if we wanted to, run a linting tool or style checked from this script and deny the commit if it didn't meet our coding standards.

Adding Git Branch To Linux Prompt

See this section of the Linux shell page.

Basic Git Notes

Remotes
   git remote : displays name of remote
                -v shows URLS of each remote

   git remote add [shortname] [url]

   Fetch and pull
      git fetch [remote-name]. Gets modifications from remote

      Cloned repos have been automatically saved as the remote "origin" by default.

      git pull = git fetch + merge

   Push to remotes
      git push [remote-name] [branch-name]
      For example, if the name of the remote it "origin" and the local branch name is "master"...
         git push origin master

      Can use git push -u origin master - The -u tells get to remember parameters so next time
                                          can just type "git push".

When Git Auto-Complete Shows Deleted Branches

Found the answer on this SO thread.

If, when using Git auto-complete, Git displays branches that you have deleted it is likely that they are remote branches, i.e., origin/xxx. Use git branch -a to see if they exist. To get rid of ones that have been deleted on the remote use git fetch --prune origin.