List of git tips

Generally, I take it upon myself to push git onto other developers wherever I go. It is a wonderous, joyful world where your work is never impeded by a network connection and everything is far more powerful and flexible. Often you hear "why would I ever want to move away from Subversion?", which is a bit like saying "why would I ever want to stop riding a horse to work", but that's a topic for another post. This page is a first-stop manual for those new to working in teams or working with git, and lays out some good practises you'll probably want to follow early on until you become a more advanced user.

Useful links

You should have a basic understanding of git before reading this - I recommend the visual git guide.

For further reading, there is a more indepth description of how branching works in git, and how your simpler commands affect them here.

Best practises for working in a small team

These are ordered by desirability and complexity. Being on top of things and pragmatic in your approach should mean things stay simple and easy, but when leaving commits unpushed for a day or so these things do happen. Hopefully, you will never need to encounter later measures in small projects. With each, I've tried to show graphically the state of the git network in each form of process.

1. Pulling and merging BEFORE committing will avoid the need to merge or rebase:

$ git pull
$ git add ...
$ git commit ...
$ git push ...

YOU       >>    --------------------------⇒
                    /  \
ORIGIN    >>    ---/    \       ----------⇒
                         \     /
OTHERS    >>    ----------\---/----------⇒

Now conflicts only happen when 2 people make a commit at the same time.

2. Sometimes, remote changes won't pull if your local changes conflict with them. To resolve this, you simply store your local changes to the working copy in a temporary 'bucket', pull changes down and then retrieve them:

$ git stash save
$ git pull ...
$ git stash pop

There are many other commands for manipulating the stash - you can store multiple changesets in here and move them around easily. Calling git stash help shows some help on this command.

3. Commiting before pulling can result in others making commits in front of you, but so long as no merge conflicts occur pulling results in an automatic three-way merge:

$ git add ...
$ git commit ...
$ git pull

                        ___________
YOU       >>    -------/- - - - - -\-------------⇒
                    /  \           /     \
ORIGIN    >>    ---/    \---------/       \------⇒
                             /
OTHERS    >>    ------------/--------------------⇒

This often results in empty, meaningless commits in your history. A better way to pull in changes when there are new commits both locally and upstream is by rebasing:

$ git pull --rebase

YOU       >>    ----------------------------⇒
                    /  \           /  \
ORIGIN    >>    ---/    \---------/    \----⇒
                             /
OTHERS    >>    ------------/---------------⇒

This causes local history to be rewound to the last common point in time, the remote changes to be applied and then yours to be re-added on top. You could do all of this manually of course, but it would involve several commands and a lot of scripting. The result is that history remains linear and needless branches are avoided.

4. Sometimes conflicts between remote history and local history or the working copy are too great to be merged automatically. If you're too different from your origin repository and are unable to push or pull (remember, pull = fetch & merge), git will notify you when you attempt to pull or merge:
                      _________________
YOU       >>    -----/ (local commits) \----------------⇒
                    /                            x
ORIGIN    >>    ---/- - ------------------------/-------⇒
                       / (far-reaching changes)
OTHERS    >>    ------/---------------------------------⇒

$ git stash save
$ git pull ... (this will merge the remote changes cumulatively with all of your own local commits)
(if conflicts occur you will be notified now)
$ git stash pop (this will apply your old working copy changes & delete the stash)
(if conflicts occur you will be notified now)

Conflicts can be resolved using the builtin merge resolution tool for your system with the command git mergetool, or manually edited. You can stage conflicts resolved yourself for commiting using git add -u.

Your working copy is now updated with the latest changes from origin - when committed this will look very similar to an automatic merge in your commit history, except that it will serve some purpose. Proceed to commit locally or push to update origin from here as normal.

You could also achieve this result by manually applying the remote version:

   $ `git fetch ...`
   $ `git merge .../master`        (NOTE: git branch -r to list out all remote branches. you can also see differences between remote branches using git diff)
   $ `git mergetool`                            (to fix any conflicts, if there are some present)

Which is basically doing the same thing in reverse, with a bit more control over it.

5. If you have existing commits AND a dirty working copy, git will attempt to create a merge for you when you pull as with point #2, and usually fails at this when the working copy differs too much from any remote changes. Combine best practises from all things above to keep it clean:

$ git stash save (only needed if you have working directory changes)
$ git pull --rebase
$ git stash pop (again, only needed where local changes exist)

Now, your local commits are again moved to the TOP of the latest HEAD from the server, resulting in a linear tree; and you can continue to commit your dirty working copy into the head of the tree as well.

Submodules explained

Submodules work much the same as SVN:externals - they provide an ability to insert a repository inside another.

The key to understanding submodules is to remember that they are totally independent of the parent repository. Each is still its own fully-fledged git repository, and so you can navigate into the submodule folder and work with it just the same as if it was checked out on its own.

Submodules are stored in their parent repository by reference - and it is the way these references work that results in almost all problems working with them. Here is what you need to know:

  • The parent repository stores only the URL, folder path and commit ID of its submodules
  • The .gitmodules file in the root of the parent repository contains mappings between submodule folders and their repository URLs
    • The actual URLs used in your working copy are those defined in .git/config
    • git submodule init is the command which copies the submodule registrations from your .gitmodules file to your own config
      • if any of the entries are already present, they will not be updated.
    • NOTE: The reason for this split is so that you can develop with local repositories instead of the 'live' ones stored in .gitmodules
  • git submodule update is used to sync the checked out version of your submodules with the current version stored in the parent repository
  • normal git add / git commit workflow is used to actually update submodules to new versions inside their parent

To 'update' a submodule (sync it with the parent repository after an update in the parent)

  • If new submodules have been added to the repository, they won't be handled until you run git submodule init.
  • After doing this, run git submodule update - this will checkout all submodules at the commit ID stored in the parent repository

To 'really update' a submodule (pull down changes after an update in the submodule)

  • Change directory into the submodule's folder
  • Run git pull to bring down any changes
  • Change back up to the parent repository
  • Run git add, add the submodule like you would a normal file change (the diff will show a change between 2 commit hashes) and commit it
  • Push your changes as usual. Now the current commit reference of the submodule has been updated!

To remove a submodule

There is no inbuilt way in git yet to do this - you need to perform all steps manually. Luckily if you have an understanding of how submodules are referenced, this is fairly straightforward.

  1. Remove the submodule's entry in the .gitmodules file - this removes it for others
  2. Remove the submodule entry in .git/config - this removes it for yourself
  3. Remove the actual submodule files by running git rm --cached path/to/submodule
  4. Commit as usual

Other caveats

You may also encounter submodules which give you weird error messages from time to time - this is usually due to the current submodule version stored in the parent being older than the current HEAD version in the submodule's repository. The result is that the parent repository has to checkout the submodule at a revision before HEAD - meaning you can't commit into it, pull, push etc. This is known as "detached HEAD" mode.

The most common side effect of this is that you will change into a submodule's directory, make changes, commit and then it will tell you everything is up to date when you try to push.

To resolve this issue, you need to get back to your master branch inside the submodule. Note that doing this will lose any commits you've already made - so before you do so, you should do a git log and copy down the hashes of the first and last commits you've made. Now checkout your master branch with git checkout master. You can now make changes, commit etc as usual - if you hadn't made any commits yet, this is all you need to do. If you have, this is where you reapply your changes - run git cherry-pick FIRST_COMMIT..LAST_COMMIT. If you only have one commit, just put its hash by itself after the command.

After you've committed and pushed your changes to the submodule, be sure to go back to the parent and add/commit the submodule's version if you want the changes to be checked out by others.

Submodule status messages

  • 'Modified content' - there are local changes within the submodule which need to be committed
  • 'Untracked content' - there are new files inside the submodule which you may or may not want to commit
    • Note: both of these states show as the same hash when diffed, with '-dirty' at the end
  • 'New commits' - the submodule has been modified and updated locally, and needs to be committed
    • Note: this shows as a different hash when diffed

Some neat tricks

Rewind master onto a different branch when you realise something you've done is very experimental:

(get all your commits in and your working directory clean)
git branch newBranchName (makes a new branch at this commit and changes to it)
git checkout master (go back to the master branch)
git reset --hard {commit to go back to} (rewinds the HEAD of the current branch back to this commit)

Now, git pull new changes back onto master, make your commits, whatever, basically proceed as normal.
To switch back to the experimental thing you were working on, git checkout -b newBranchName

Use cherry-picking to rewrite history:

Great for when an important bugfix for the main project is created as part of a development branch. Use the command
git cherry-pick with the commit ID to add that commit to the current history at whichever point you are at.

git grep:

for grepping over only the stuff under version control

Check out git bisect for tracking down hard-to-find bugs

Note that this only really works well when people make atomic commits!

Checkout a remote branch to work on it yourself:

This is more 'good practise' than anything else, basically this creates a local branch based on a remote branch and allows git to track commit differences between your branch and the remote one in the same way that it does for origin/master. If you just do git checkout -b localBranchName, its changes will not be tracked against the 'original' branch (as you saw it) in your remote repository. Instead do:

git checkout --track -b myBranchName origin/myBranchName

The two branch names should, in most cases, be the same.
You can push local branches up to the server using git push with the name of your branch instead of 'master'.

Some useful aliases

Aliases are stored in your ~/.gitconfig file. They are stored under the [alias] section and can be used to automate tasks to great effect. Each line consists of a new command name, equals sign and some git command to execute when called. You can also prefix commands with ! to execute raw shell commands.

Short log format: git lg or git lgd (former shows relative times)

lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
lgd = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative

Perform a diff, but ignore end-of-line whitespace: git dif [args]

dif = diff --ignore-space-at-eol

'Short' commands, showing filenames only: git (sdiff | sshow | slog) [args]

sdiff = diff --name-only
sshow = show --name-only
slog = log --name-only

Ignore local changes to files already under version control: git lignore [file], git unlignore [file] and git lignored

lignore = update-index --assume-unchanged
unlignore = update-index --no-assume-unchanged
lignored = !git ls-files -v | grep "^[[:lower:]]"

Find commits which have been detached (lost stashes, headless commits etc): git findlost

findlost = !git fsck --no-reflog | awk '/dangling commit/ {print $3}'