A Git Workflow

One of the things that many tutorials and guides never teach you about git is how to use it for your day-to-day work. Over time you learn what works and what doesn't, but it isn't always obvious and certainly isn't obvious how you should use it from the start.

So let's explore a possible typical workflow.

Starting off

Using an existing repo

git clone some-repo.git - This is the usual starting point if you are aiming to work with an existing repo. You can append a directory name to the end which is where the repository will be cloned to. Some people also prefer to create a new directory, cd in to it, and run git clone some-repo.git .

Creating a new repo

Otherwise, run git init within the directory you wish to use for your repo will initialise it. You can have existing files within this directory. If you use this method, you will need to also add the files you want git to keep track of, you can use the following methods:

  • Add a .gitignore file to the root of the directory containing a list of files/types you do not want included. You can list explicit files and/or wildcard matches like *.so which will exclude all files ending in .so, or random* which will exclude anything with 'random' as the the first part of the file name. A .gitignore with a single * wildcard in it will exclude everything.
  • git add . - This will add everything in the working directory, ideally you want to have the .gitignore done before doing this.
  • git add path/filename - Add a file explicitly. If you set up a .gitignore exclude everything then you will need to use git add -f path/filename to force tracking of that file.
  • git add -u - If you already have your files tracked with git, then using this command will add only updated/changed files.
  • Note: You need to git add any time you have modified files and are going to do a commit, generally git will warn you when changes have been made which haven't been tracked.

If you intend to keep your newly created repo in a remote such as BitBucket or GitHub, go to those sites and create a repository using their interfaces, they will then give you a detailed set of commands to run to add the remote to your repo. Generally speaking it will follow this format;

  • git remote add origin git@git-repo-url/reponame.git - to add the remote as origin
  • git push -u origin master - to push everything upstream, assuming you've initialised and added files to your repo of course.

Tracking changes

Before performing any work, and assuming you're working with a project which is being stored remotely, you should ideally git pull to check upstream for changes before starting and ensure your local repo is up to date.

If you're working with someone else's project (or in a team) then it is always recommended that you work in a new branch to avoid contaminating the master branch. git checkout -b newbranchname will create and checkout a new branch with the name specified by newbranchname, this is how you will perform the bulk of your work.

You can have multiple branches, for example you may have a branch of master called dev_vector where you are writing new code for some vector calculations, and then you can have another branch from master named documentation where you're adding docs to the source. You can also branch a branch and get messy via git checkout -b new_vector dev_vector - this is a new branch off of dev_vector

Now, do some work in that branch, and add your changes:

  • git add -u to add only updated files, and/or git add files-explicitly, or wildcard git add . which will add everything not blocked by .gitignore

At this stage git is tracking the changes you've made, but they haven't actually been added to the repo proper, essentially you've just prepared git for a commit; so either

  • git commit to commit the files, this will then pop-up a commit message in your preferred text editor where you should ideally describe what changes have been made (as a whole)
  • git commit -m "Your message here" if you want to do a quick commit without adding a fancy message.

A small note on commit messages

Commit messages are an important guide to you when looking at the history of a project, and a well written commit message is invaluable. We've all been guilty of writing such gems as "asdasdasd" (much like how we probably mashed the keyboard for a game save name at least once), and when trying to find a breaking change, this is just plain unhelpful. Rather than extoll the virtues of commit messages here, please read this excellent post by Chris Beams.

Now! If you're working with a repo that is tracked upstream (I really hope you are, or at least keep regular backups), run git push. This is optional, you can just keep it all local if you want, and sometimes this is the case when you're just experimenting with code in a random branch. You can of course just keep the master branch upstream, and keep all your branches local.

In the case that you are working on someone elses, or an organizations project, please do follow their guidelines for commits if they have a guide.

Merge to the Master branch

Assuming you've made all your changes and committed them to the hallows of git, you're now in a position to get them in to the master branch which is where the main code is developed.

The first thing you should do is make sure master is up to date, so quickly do;

  • git checkout master
  • git pull

Now checkout back to your working branch, git checkout branchname. You should then git rebase master your changes on the current master, this is the fun part. What this does is rewinds your changes back to the point where you branched, then replays your changes on to the master from the point where you branched off. If things go smoothly, then there won't be anything for you to do apart from git add -u and git commit, before the next step.

If for some reason you had issues then git will tell you all about it. Likely you will need to edit the problem files where git will have inserted lines which include the lines to be changed, and the changes.

So now that your branch is updated to the current master, it's time to actually merge your changes.

  • git checkout master
  • git merge branchname

Since we prepared by first getting the branch updated to master, this should have been completely pain free (this is why we update the branch first). So go ahead and perform the holy trinity;

  • git add -u
  • git commit
  • git push

Notes: Some extra little helpful tidbits

Instead of doing lots of small commits and pushes, you can amend the last commit and add to it with git commit --amend. For example, you forgot to rename something, or added a new file, rather than adding yet another commit, just amend the last one. But be aware, don't make this your primary mode of committing code and be sure to create a regular commit - the last --amend you do will be the last full commit before the next.

To simplify the above, git commit --amend will append changes to the last commit, a git commit is a new commit.

If you really screwed up with your changes, you can run git reset --hard or git reset --hard *some commit*, this will take your current branch back to the last commit or commit specified and change all staged (git add) files back to that point. Use with caution!

You can checkout a commit, run git log to see a list of commits, and then git checkout *change-hash* where change-hash looks similar to commit 917854a9760fc662b341efe1c0b51671535ab423 under git log.

Want to know what changed between commits? Run git log to view the history, then run git diff 14b8..b410 where 14b8..b410 is the first few characters of the commits you want to compare. Order matters! so if you compare most-recent..least-recent then you will get changes listed in reverse order to the actual commit timeline. To view changes in timeline order you will need to compare least-recent to most-recent.

You can also run git diff path/to/filename to see only what has changed with that file, likewise you can also use git log path/to/file to see its history.

There is also git log -p path/to/filename, this will give you a change log which also contains a diff between each change. This and the above commands can be used to compare branches as well.