Using Git as Version Control

What’s the point of Version Control? 

The point of version control is to have a clear history of changes that you and other developers have made on an ever-growing project or codebase. In this class, we use Git for similar reasons. We use it so that you can maintain the history of your projects easily, as well as to track your progress if you ever need to revert your changes.

How Git Works - Overview

At its core, Git works by keeping a controlled record of changes and revisions you make to a codebase. A codebase is simply a collection of source files (*.h, *.c, Makefile, etc.) stored inside of a folder. Let’s look at a brief example of how Git can be used to manage a project!

Imagine you have a very simple project where you’re told to add the following features:

Your instructor has provided you with a little bit of starter code. It looks like this:

/* DriverLib Includes */

#include <ti/devices/msp432p4xx/driverlib/driverlib.h>


/* Standard Includes */

#include <stdint.h>

#include <stdbool.h>


int main(void)

{

    /* Stop Watchdog */

    WDT_A_holdTimer();


    while(1)

    {

    

    }

}

At its core, Git works as an intermediate step which effectively enforces you to define good checkpoints of progress as you program. As we walk through this example, we hope to outline a style of thinking which evokes this kind of mentality for integrating Git as a part of your programming process.

To add each feature, you’re going to need to add code which does the following:

In your previous programming classes, you may have simply just “gone for it” and pasted all your code in at once. You probably would have done something like this (the changes you made to the starter code are bold):

/* DriverLib Includes */

#include <ti/devices/msp432p4xx/driverlib/driverlib.h>


/* Standard Includes */

#include <stdint.h>

#include <stdbool.h>


/* Predefined values for LEDs */

#define LED2_GREEN_PORT     GPIO_PORT_P2

#define LED2_GREEN_PIN      GPIO_PIN1


#define LED2_BLUE_PORT      GPIO_PORT_P2

#define LED2_BLUE_PIN       GPIO_PIN2


/* Predefined values for Buttons */

#define S1_PORT             GPIO_PORT_P1

#define S1_PIN              GPIO_PIN1


#define S2_PORT             GPIO_PORT_P1

#define S2_PIN              GPIO_PIN4


#define PRESSED             0

#define RELEASED            1


int main(void)

{

    /* Stop Watchdog */

    WDT_A_holdTimer();


    // Initialize LEDs

    GPIO_setAsOutputPin(LED2_GREEN_PORT, LED2_GREEN_PIN);

    GPIO_setAsOutputPin(LED2_BLUE_PORT, LED2_BLUE_PIN);


    // Initialize buttons

    GPIO_setAsInputPinWithPullUpResistor(S1_PORT, S1_PIN);

    GPIO_setAsInputPinWithPullUpResistor(S2_PORT, S2_PIN);


    while(1)

    {

        uint8_t rawS1 = GPIO_getInputPinValue(S1_PORT, S1_PIN);

        uint8_t rawS2 = GPIO_getInputPinValue(S2_PORT, S2_PIN);

        

        // Temporarily dim both LEDs

        GPIO_setOutputLowOnPin(LED2_GREEN_PORT, LED2_GREEN_PIN);

        GPIO_setOutputLowOnPin(LED2_BLUE_PORT, LED2_BLUE_PIN);


        // If S1 is pressed, turn on the Green LED

        if (rawS1 == PRESSED)

        {

            GPIO_setOutputHighOnPin(LED2_GREEN_PORT, LED2_GREEN_PIN);

        }


        // If S2 is pressed, turn on the Blue LED

        if (rawS2 == PRESSED)

        {

            GPIO_setOutputHighOnPin(LED2_BLUE_PORT, LED2_BLUE_PIN);

        }

    }

}

This example is really simple, so most of you reading this could have just written all of this in one shot and moved on with your life. However, imagine how much more complicated your modifications would become if this project were to become significantly larger. It’s hard to discern what your changes actually did to your system if you haphazardly upload thousands of lines of code at once. Git’s usefulness really starts to shine when you need to start organizing medium to large-scale changes to your project.

To make your changes easier to parse and understand, we want to organize these changes into commits. A commit is simply a set of line additions, modifications, and removals to one or more files. We want to make commits for each meaningful modification we make to our source code. The motivation behind making meaningful commits (as opposed to just pasting our whole solution) is to ensure that we have an easy-to-read changelog which we can use to revert changes if we ever need to. In this example, we will partition the code written above into three separate commits:

Let’s wind back to the starter code and see how we can organize the same code we had already written into meaningful commits. One commit you could make involves adding all the #defines at the beginning of the file. In this case, your first revision would look like this (commit message: Added predefined values for each port and pin):

Notice how the modifications you’ve made are inserted into the old source file. This is what we call a git diff. A git diff tells us what lines have been added, modified, or deleted between two commits. In this case, we are comparing the commit our instructor made against the new commit we just made.

To view a diff between your uncommitted changes and the latest commit, simply double-click on a file in the Git Staging menu.

Returning back to the example, we can also make separate commits for each specific feature. 

The code modifications are shown below.

First, add Green LED functionality from a previous commit which only has #defined values. (Commit message: Pressing S1 now turns on Launchpad LED2 Green)

Next, add Blue LED functionality from a previous commit which has the Green LED feature implemented. (Commit message: Pressing S2 now turns on Launchpad LED2 Blue

Notice that at this time, we’ve effectively partitioned our changes into discrete features. While your features in your actual projects don’t necessarily need to be this small, it’s important to keep a conscious mind about using Git to measure each of your changes. Using Git is a mindset just as much as it is a tool - it’s a mindset in which we keep conscious of each change we make to our code, in what order we make those changes, and what effect those changes have on the rest of our program. 

At this point, we’ve made multiple changes locally. However, as opposed to the big, one-shot change we would have done in the past without version control, we now also have a detailed history of how exactly our code has evolved. You can now view your commit history by right-clicking on your project and selecting Team, then selecting Show in History. The commit history now looks like this: 

If you ever want to compare changes between any two commits, simply CTRL+click on the two commits you want to compare. Then right-click on one of the selected commits and select Compare with Each Other.

Notice that there are two master history markers - there’s one at origin/master and then there’s another one simply named master. One of Git’s features is actually allowing users to make multiple changes locally and then uploading them all at once. If you’ve ever run into issues where nothing shows up remotely even though you’ve committed all of your work, this is why. The commit history you have created only exists offline on your personal computer, and your remote repository at www.github.com has no notion of any edits you have made locally. The visual below shows what your local commit history looks and what your remote would look like, along with corresponding labels for master and origin/master and to which commits they refer. 

In a previous tutorial, we always tell you to commit and push your code, rather than just committing it. When you push a set of commits, what you’re really doing is copying all of your local commits into the remote repository. Each one of the blue commits gets copied into the remote repository in the same way that they were created locally, like so: 

When we tell you to commit and push, we’re trying to remove this extra step you might accidentally forget. Pushing each commit individually ensures that your remote repository is updated as soon as you commit to your local repository, so you wouldn’t need to worry about this anymore. While there are definitely reasons why you wouldn’t always push after each commit in an actual collaborative setting, you don't really need to worry much about any disadvantages in this course.

The example above explains how we can update our remote repository based on more recent commits we made locally. However, sometimes, we need to deal with the issues the other way around. For example, if we added a file manually through www.github.com and NOT from our local repository, we would then have a commit history that looks like this: 

The main issue that concerns us is that our remote and local repositories no longer have the same commit history. Any commits we make locally are now made with a potentially different starting point, meaning our changes might not actually be applied remotely. Git is not smart - Git is stupid. Git has no notion of what logical changes you wanted to make - it only keeps track of what lines of code you’ve changed in each text file. If you were to make more commits locally, your local branch would now look like the following: 

Both of the commits Added sample PDF and Added Honor Code are running with the assumption that their changes will modify the commit Pressing S2 now turns on Launchpad LED2 Blue. That’s a problem. To create some protection against accidental commits which step over each other, Git will reject your attempts to push your local commits to the remote repository until your local repository’s history exactly resembles the remote history, with local commits applied on top.

To fix this issue, we need to pull any remote commits to your local repository. Pulling remote commits actually creates a merge with your local history. It’s important that you avoid directly editing files remotely, because if you happen to edit the same line in a file remotely and locally, you’ll receive what’s known as a merge conflict. Merge conflict resolution is not something which we will discuss in this article, so you have been warned!

Pulling the remote repository to your local one will allow Git to merge the new commits in both the remote and local branches together in chronological order. The resulting commit history will be placed in your local repository:

All that’s left is to push the resulting merge back to remote, and both the local and remote repositories should look the same! 

How to use Git as an Undo Tool

Before reading any of this, please know that this is the danger zone for your project. If you mess up this process, you could potentially irrevocably erase progress you have fought for in your project. Proceed with care and caution. 

When undoing changes through Git, there's two scenarios you're likely to run into:

Before undoing anything, we highly recommend you commit all files for which you DO want to keep all changes you have made in them.

To revert local changes which you have NOT committed, simply right-click on the file you wish to revert changes in, and then select Replace With, followed by HEAD Revision. Be very careful when doing this, because these local changes CANNOT be recovered once you revert them! Doing this will revert the file you have selected back to the state the file was in upon your most recent commit. You might want to recompile your code at this point just to make sure that the code still compiles!

Note: This series of steps by itself is insufficient if you have already committed your changes. The procedure directly following this one describes how to handle this case.

To revert changes which have ALREADY BEEN COMMITTED, you will need to use Git's reset feature. The procedure you need to follow looks like this:

First, you need to replace your entire project with the latest version stored remotely. BE VERY CAREFUL WITH THIS - YOU WILL LOSE ALL UNCOMMITTED CHANGES IN YOUR ENTIRE PROJECT IN THIS PROCESS. To do this, you invoke the same procedure above, except this time, instead of right-clicking on a file, you right-click on your entire project. Specifically, you right-click on your project, and select Replace With, followed by HEAD Revision.

Afterwards, open the History view for this project. As mentioned above, you can access it by right-clicking on your project and selecting Team (Show in History).

Once you're in the History view, right-click on a specific commit which you want to revert back towards, and then select Reset. Choose Mixed.

At this point, you have two choices. The reason why we choose a Mixed reset is as an extra fail-safe - both the Mixed and Soft options will remove the commits themselves but will keep the contents of the commits on your files. 

If at this point you realize you made a mistake and no longer want to undo your commits, you can commit your entire project again through the same Team (Commit...) option you usually use. While your commit history has been destroyed, you still have your revisions. Push these changes after you commit.

On the other hand, if you really feel like you can discard these changes for real, you can finally discard any changes after the commit you have selected by right-clicking on your project, and select Replace With, followed by HEAD Revision. You should see that the latest HEAD is now reverted back to the old commit, but that when you open your files, the changes have not been discarded for good. 

THIS IS YOUR LAST WARNING. THE NEXT STEP WILL ERASE YOUR CHANGES FOR GOOD. BE SURE THIS IS WHAT YOU ACTUALLY WANT! (You can copy your current project to a new project before this step to have a copy of your changes.)

To discard these changes for good, right-click on your project and select Replace With, followed by HEAD Revision.


As was the case beforehand, since changes you make are only stored locally, you need to push any modifications you made. This includes your reset. Push your changes by right-clicking on your project, then selecting Team (Push to Upstream...). You have now successfully reset your repository to one of your earlier commits!

Further Reading and Considerations

Using Git effectively requires you to keep your mind straight with regards to what and how you’re programming. Equally, if not more important, is your ability to tackle a project and decompose it into manageable components. Determining good milestones which you can implement using commits is part of the challenge! If you’re working on a larger project, making meaningful commits not only keeps your code clean, but also allows others to understand the evolution of your code more easily, which is really important when you want a TA or professor to help with debugging your code!  

There are several things we did not discuss in this article. Among them are branches and merge conflicts. There's plenty of good resources online you can Google, and if there's anything you don't understand in this article, one of the best ways to learn is to Google anything you don't understand and see what links you get back. More often than not, a post from Stack Overflow (stackoverflow.com) has what you need.

If you’re still interested in more information revolving Git, there’s always the official documentation at https://git-scm.com/book/en/v2