Git Gud Git Guide

Intermediate level git tutorial


Welcome to the git adventure! It is time for you to embark on your journey through the many wonders of git.

This tutorial assumes you have installed git on your computer, and preferably tried it out a little. During the course of this adventure you will create a small, rather useless python program. You don't need any previous python experience to follow, in fact this might even teach you some basic (probably bad) python. Make sure to read the instructions so you don't miss anything.

I try to not explain too much about what's going on behind the scenes, since I think the reason a lot of people don't use many of git's functions is that they're all hidden inside pages and pages of backstory in tutorials or documentation. Thus, this tutorial mostly explains how, not why. I'd recommend going though this tutorial for a good overview of what can be done, and then reading up on the more confusing things after.

Let's start!

Commits and status checking

Start by setting up a repo and cloning it to your computer. I called mine favorites. Open up the repo in your terminal. Begin by creating an empty file called main.py.

Let's take a look at the status of the repo using

> git status

Our new file is under "Untracked files". This basically means that the repo doesn't know and doesn't care about this file at the moment. To start tracking the file, we type

> git add main.py

main.py is now ready to be commited! Type

> git commit

This will open your text editor and allow you to type a commit message. Write something like "Add main.py", save it and exit. You can now check git status again and see that everything is commited.

Let's add some content to main.py,

def print_favorites():
   print('Hello world!')

Time to commit again. When committing, I like to use the flag -m (for message) to write a commit message directly on the command line. So I would write

> git commit -m "Add a print function"

This is nice if you want to write short messages for small changes, maybe in your private repo. It will be very useful during this tutorial. Otherwise, a good convention is writing multiple-lined commit messages, where the first line stands on its own and the following lines expand on the contents of the commit. Always remember to write descriptive commit messages! I may or may not follow that advice during this tutorial.

Our changes are now saved, but only locally. To send the commit to GitHub, making it backed up and available anywhere, we use git push.

> git push

We added a function, but we're not calling it yet. Add a call to print_favorites:

def print_favorites():
  print('Hello world!')

print_favorites()

You can now run this using > python main.py (if you happen to have Python, otherwise it's not really important.)

git diff

An important thing to do before committing is to check what's being commited. With git status you can check what files are being commited, but to see what lines of code are being commited you can use git diff. Try that now and make sure you are only committing the new funticon call. I often find sneaky debug prints that I forgot to remove this way. You can also use `git commit -v` (for verbose) to see this output while writing your commit message.

> git diff

Check git status. This time main.py is not under "untracked files" but under "tracked files not staged for commit". We could do git add again to stage it, but for files in this category it's enough to add the -a (for all) flag to the commit command to have them all staged and commited.

> git commit -a -m "Add call to print_favorites"

Mutliple flags can be combined, so an equvialent command is

> git commit -am "Add call to print_favorites"

Those are the basics! Do git push and continue on.

Collaborating and conflicting

Merges

Time for some more serious business. We're going to stage some situations that happen as you collaborate, and practice dealing with them.

Let's expand main.py by adding some of your favorite things! I'll be writing mine in the examples but feel free to write whatever you want. Just don't go off the trail since we are doing this to stage some git situations. First, add your favorite animal.

animal = 'cat'

def print_favorites():
  print('Hello world!')

print_favorites()

Check the diff, commit and push.

Introducing the NPC

Actually, this would be easier with some help. Your childhood friend Isabelle knows everything about you so she can add things too. We're gonna pretend to be Isabelle now, by cloning the repo again in a different folder. Do that now.

> cd ../isabelle (create a folder somewhere outside the repo)
isabelle > git clone [your repo]

She adds your favorite movie, commits and pushes.

animal = 'cat'
movie = 'fight club'

def print_favorites():
...
isabelle > git commit -am "Add favorite movie"
isabelle > git push

Meanwhile, you start adding some printing functionality.

animal = 'cat'

def hello()

def print_favorites():
  print('Hello world!')
  print('My favorite animal is', animal)
...

Now, commit and try to push. What happens? You can't, since there are new changes in the remote repository. You will need to pull those changes first, using git pull.

> git pull

Oops, what's this? Suddenly your editor pops up.

You and Isabelle have both made changes in the same file, which can make git a little confused. In this case however, the changes are in different parts of the file, so git can automatically figure out what the file should look like. It does however need to make a new commit for these changes, which is why it is now promptiong you for a merge commit message. You don't really need to write anything since there is a preset commit message, so just save and exit. Some people even set their clients to automatically create the merge commit in these situations.

Now the merge is done, and a new commit has been created. Go ahead and push. This beautiful MS Paint image shows what happened:

Merging

Both the print and the movie commits were based off the animal commit, then a merge commit was created where both changes are present.

So that's fine and dandy and represents the reality of the situation. But it sort of... doesn't look too clean, and all these merge commits eventually start cluttering up the commit history. What if we could, instead of having two commits based off the animal commit, pretend like our print commit was based off the movie commit? Then we would get a clean sequence like this:

Rebasing

Rebasing

This can be done using rebasing. Many people always do a rebase when they pull down new changes. Let's get ourselves into the same situation again to try it out. Do git pull in Isabelle's repo. Isabelle adds your favorite book, commits and pushes:

book = 'en annan gryning'
isabelle > git commit -am "Add favorite book" 
isabelle > git push

And you add a print for the movie:

print('My favorite movie is', movie)

After committing, instead of git pull, let's write git pull -r. (r for rebase)

> git pull -r

Now we have the situation in the image above, and a much cleaner log! Happy times.

Rainy day scenarios

The above were some very nice and friendly situations. Sometimes, git won't be able to figure out how to consolidate different changes in the same file. Let's try a scenario like that!

(Remember to pull in Isabelles repo!) Isabelle is still adding things, she just added your favorite food, commited and pushed:

food = 'pizza'
isabelle > git commit -am "Add favorite food" 
isabelle > git push

Meanwhile, you add your favorite programming language:

language = 'c++'

The changes were made to the same line in the code, so this should cause a conflict. Let's try an ordinary pull first.

> git commit -am "Add favorite programming language"
> git pull

Oh no, it's a merge conflict! Time to panic!!! Or not. This isn't really scary at all. Let's take a look at what happened to the file.

animal = 'cat'
movie = 'fight club'
book = 'en annan gryning'
<<<<<<< HEAD
language = 'c++'
=======
food = 'pizza'
>>>>>>> e330d67ec9a9f1200117e0cdbfc8d2de05371e3a

The conflicting parts are marked out with <<<<<<'s and ======='s. You can see what it looked like before, what your change is, and what the other person(s) changes are. Now it's up to you to decide what to keep and what to discard. In our case, we just keep both lines.

animal = 'cat'
movie = 'fight club'
book = 'en annan gryning'
language = 'c++'
food = 'pizza'

There we go! Now we have to commit this.

> git commit -am "Merge"

That's how you deal with a merge conflict. You may find yourself in tricker situations when more lines of code are involved. Never panic though, just look through all the conflicts, and perhaps reach out to the person who made the conflicting changes if something is unclear.

You can also simplify merging using mergetools, but that's for another guide. Google it!

Rebasing again

Since I introduced rebasing, we should take a look at what happens when you get a merge conflict during a rebase. The process is almost the same but the instructions from the client can look a little confusing.

Recreate a similar scenario by adding new print statements in both Isabelle's and your repo.

isabelle > git pull
isabelle > (add print for book)
isabelle > git commit -am "Print favorite book" 
isabelle > git push
> (add print for language)
> git commit -am "Print favorite programming language"

Now do

> git pull -r

This message looks way scarier than the ordinary merge conflict message. Note that it tells us to do git rebase --continue when we've resolved the conflict. Resolve it by keeping both changes and save the file. Now try this command.

> git rebase --continue

Git tells us that we need to do an add first. Do that.

> git add main.py
> git rebase --continue

And there we go, ready to push!

That's about as scary as things normally get when collaborating with others. Grab a friend and start a project, or go help out with an open source one!

Doing many things at once -

branching and stashing

Stashing

You've started working on something new: a list with your favorite games. You're about halfway done, not ready to commit yet.

games = ['pikmin', 'zelda ocarina of']

Suddenly, Isabelle pings you, saying there is still a print statement missing (for the favorite food) and it's really blocking her work, so you should fix it asap. And you're in the middle of your game list, you're not ready to commit it yet... What to do...

New command - git stash!

> git stash

This takes all your uncommited changes and temporarily hides them in your 'stash'. You now have a clean working directory, and can easily add the print statement, commit and push. Do this. Check with git diff that you have the right changes before and after stashing.

Now that everything is fixed, use git stash apply to go back to where you were.

> git stash apply

Now, finish up the list, commit and push!

games = ['pikmin', 'zelda ocarina of time', 'starcraft 2']

Branching

Sometimes you will make changes that are so large that you want to be able to work on it and make a few commits before actually letting those changes appear in the main project, for example if you're adding a large new feature. Then you may need something called a branch. This gives you something like a copy of the repo, where you can work without disturbing the "master branch", which is where we've been working so far.

You switch between branches using git checkout [branchname], and to create a new branch, you can add the flag -b. It's time for a large refactoring of the favorites program, so let's create a branch for it.

> git checkout -b dictionary

Switching to a new branch can be done at any point before a commit, the unsaved changes don't really belong anywhere until then. So if you start out on something on the master branch and realize midway through that it should have its own branch, just switch before committing.

Our big change is putting all these odd variables we have into a dictionary, a Python structure kind of like a map in Java. First we create the dictionary, replacing the variables:

favorites = {
    'animal' : 'cat',
    'movie' : 'fight club',
    'book' : 'en annan gryning',
    'language' : 'c++',
    'food' : 'pizza'
}

Commit this, just like you normally would. Try switching back to the master branch using git checkout master to see that the changes can't be found there. Switch back using git checkout dictionary. Since we have healthy amounts of paranoia, we want to push this in case our laptop gets stolen on the way home. We can push half-finished work since it's on a branch.

To push a branch, we need a little bit of extra magic. Try a git push and the client will tell you what to do, namely

> git push --set-upstream origin dictionary

This will set the upstream to "origin", which is where the master branch pushes to (GitHub). From now on we can just push and pull normally on this branch.

Pulling down a new branch also needs an incantation. We want Isabelle to rewrite the print function in a smart way, since she knows everything about dictionaries. She will first need to do git pull, or a git fetch, to get the information about the new branch. Git fetch does only the first part of git pull, getting the information from upstream without trying to merge anything. This could be useful if you don't want to touch the master branch at that moment. Now, to pull the branch, she basically needs to create her own branch that tracks the remote branch, which is why the command needed looks a bit complicated. Just copypaste to try it out!

isabelle > git fetch
isabelle > git checkout -b dictionary origin/dictionary

This creates a new branch in her local repository, with the same name as the remote branch. YOu could name it to whatever you want, this is just for convenience.

Isabelle rewrites the print method to make use of the dictionary.

def print_favorites():
  for key, item in favorites.items():
    print('My favorite {} is {}.'.format(key, item))

print_favorites()

She commits this, like a normal commit. She can also push it without issues since the upstream was already set.

Now you can pull this branch, using git pull while having the branch checked out. This change is ready to go into production! We now want to merge it into the master branch.

Check out the master branch.

> git checkout master

To start merging, do

> git merge dictionary

Merge conflicts when merging a branch are fixed in the standard way, resolve the conflicts and commit the results. You can also add the -r flag here to rebase instead of merging. I'm going to leave this as an exercise for the reader.

You can now go ahead and push!

Cleaning up

It can be a good idea to delete the branch, so you don't start stacking them up. This is slightly involved, you need to delete the remote branch once, then the "tracking branches" for everyone that was tracking it (you and Isabelle) and finally the local branches for everyone that had them. In that order, the commands are

> git push origin --delete dictionary
> git fetch -p
> git branch -d dictionary

Deleting the local branches is not super important, you will probably not notice them being around. This concludes branches!

Fixing oopsies and cleaning up the log

Amend

Sometimes you commit something and more or less instantly regret it. It could be because of a typo in the commit message or the you forgot to remove some debugging prints. In those cases, our best friend is amend.

You add a nice welcome message to your program.

def print_favorites():
    print('Welcome to my favorites!')
    ...

You commit using

> git commit -am "Add welcom message"

Oh no, that's a typo. No fear, do

> git commit --amend

and you editor will open, allowing you to change your message.

You decide to try running the program,

python main.py

and realize it would be much nicer if there was an empty line after the welcome message. That should just be added to the commit we had. Make the change, adding an empty print() statement. Add this to the commit using

> git commit -a --amend --no-edit

which lets you skip editing the commit message. Note the -a flag, you need to remember to stage your changes for commit.

You can basically do any change, add new files, remove files, change files, and then just do git commit --amend to add that change to the latest commit. However, once you've pushed it, consider it gone. Changing things that have been made public is almost never a good idea, since other people may have started using them already. Go ahead and push this commit.

Reset

Sometimes you just don't want the commit anymore. That can mean two things: That you want to keep the changes, just not have them in a commit, or that you want to remove all the changes you made.

To only undo the act of committing, use

> git reset --soft HEAD~1

If you also want to remove all the changes you made, and make it like you just pulled the previous commit, use

> git reset --hard HEAD~1

This WILL delete your changes, so only use this if you really want to. In some cases, it's better to reset files one by one to make sure you know what you're resetting. To do this, use

> git checkout --main.py

Make some bogus edits in main.py to try these commands out.

Interactive rebase

Create a new branch called songs, and start adding your favorite tunes. To create a situation, we're going to add one song per commit. Go!

> git checkout -b songs
> (add dictionary with one song)
> git commit -am "Add song dictionary with song x"
> (add dictionary with one song)
> git commit -am "Add song y"
...

The end result should be something like

songs = { 
    'the killers' : 'mr brightside',
    'goldfrapp' : 'rocket',
    'inka marka' : 'loy loy loy'
    }

When you're done, also add some print statements for the songs in print_favorites.

print('My favorite songs are:')
for key, item in songs.items():
  print('{} by {}'.format(key, item))

Commit this. If you check git status now, you'll see that we have 4 commits pending. These are all the commits from the branch, and you can see them using git log. Try it.

> git log

Sometimes you do want the change split up into several commits, and in that case you could just go ahead and push. But sometimes it's cleaner to have just one commit explaining your change, e.g. "Add favorite songs and print them". This can be achieved using interactive rebasing. Do

> git rebase -i 

pick a982c8c Add song dictionary with whats your problem
pick 4482745 Add rocket
pick 6575534 Add loy loy loy
pick 57c845b Print favorite songs

# Rebase 8e77232..0d49872 onto 8e77232
#
# 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
#
# 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.

We want to merge the commits, so we can use either squash or fixup. squash lets us write a new commit message, while fixup uses the top commit's message. Squashing can be nice if you want to reuse parts of different commit messages. Otherwise, you can use fixup to just use the top one, or combine fixup with reword to edit the top message. Let's go with the latter option.

r a982c8c Add song dictionary with whats your problem
f 4482745 Add rocket
f 6575534 Add loy loy loy
f 57c845b Print favorite songs

Set the commit message to something nice like "Add favorite songs and print them". Check git log again. Very cool! NOTE that this should not be done if more people have access to and are using your commits, as in the previous example.

Now you can merge this branch into master and push it.

> git checkout master
> git merge songs
> git push

Pull requests and forks

Pull requests

Using pull requests is a more involved way of merging a branch with master. If you are working on a larger project that may even be in production, it is a good idea to have your teammates look over your code before you merge it. Also, if you wrote some code for a project that you don't have writing rights to, such as an open source project, you will need to do a pull request and have the owners merge your code.

We'll try both these scenarios. First, there is one very obvious thing missing from our project, printing the games. Create a branch and fix that.

> git checkout -b games
print('My favorite games are:)
[print(game) for game in games]

Now, hit up your repo on GitHub. Go to your branches, and find the games branch. (This is where the clutter will really show if you neglect deleting branches.) Here, click "Create Pull Request". This will take you to a page where you can see all the changes you're trying to add, and you can write a description of them. Make sure to make this good, or your change might not even be considered! When you're done, click on "Send pull request". The relevant people should be notified, and now you just gotta wait for them to look it over. Isabelle takes a look, tries the program, and notices that it prints a long block of text. "Add some blank lines to make the output more readable", she comments.

It is very important to remember that comments on your code are always a good opportunity to learn. Respect them, learn from them, but don't take them personally. Sure, five-minutes-ago you may have made a mistake, but from-now-on-you sure won't make that one again (at least not this week), and it's all thanks to the feedback you got.

After receiving feedback, you can discuss it in the comments, and also make additional commits. Make a commit adding blank lines in appropriate places. After this, you could make a comment like "I added blank lines, how does it look?" and Isabelle may respond with "Great, go ahead and merge."

When there are not conflicts, you can merge using the big green "Merge pull request" button on GitHub. Otherwise, you can always merge locally and push, and GitHub will notice that the merge is done. Either way should work for us right now so go pick one and do it. Afterwards, GitHub will offer you a nice button to delete the branch. Good job!

Forks

If you want to work on a repo that's not yours, instead of a branch you do a fork. This creates a copy of the repo that you're free to do what you want with (though you should of course consider licenses). Sometimes you do this just to make a tweak of some project for your own use, but sometimes you want to add an improvement that could benefit other users, so you want it to be added to the original repo. This is often appreciated, and it is done using pull requests just like in the previous section. Time to try it out!

There is a Hall of Fame for the brave souls who completed this tutorial here. The final test you need to pass to belong on this list is adding yourself to it! Go to the repository here. In the top right corner, there should be a "Fork" button. Go ahead, click it!

A fork of the halloffame repo should be created on your GitHub account. Note that its default branch is not master but one called gh-pages, this has to do with how GitHub hosts websites. Just stay on this branch. Clone this to your computer, and open up the file index.html. In there you can find the hall of fame list (search for "ENTER YOUR NAME"). There are some instructions on how to enter your name, follow those and look at previous entries and you should be able to figure it out. Add your name to the list, do an extra check with git diff so everything looks ok, and commit and push.

...

Awesome Humanbeing<br>
<a href="https://github.com/coolgit">Cool Gitlearner</a><br>
YOURNAMEHERE!

...

Now, go to your fork repo on GitHub, and open the tab "Pull requests" found at the top.

Pull request tab location

Click the big green "New pull request" button. You will see the diff between your fork and the original halloffame repo. Click the big green "Create pull request" button (obviously not to be confused with the big green "New pull request" button from the previous page) and you can write a lovely message to me explaining why I should approve this change. (Basically "I made it here, put me on dat list"). Feel free to add small feedback comments here if you want. Press the last big green "Create pull request" button to send it to me for review. I'll accept it as soon as I can, so check back! Every new name makes me happy, so I look forward to adding yours.

The end

That concludes the tutorial! I hope you enjoyed it and/or learned something new. If there are changes you think should be made to this tutorial, go ahead and send me a pull request!