Java / Git

Exercises

Java / Git

1. First Java Project

Let’s start by creating a project in IntelliJ IDEA:

The steps above will create a ”.idea” directory and a “helloworld.iml” file. These are where IntelliJ keeps all the settings for your project. Notice that there is also a “src” directory where all the source code is stored.

On the Project tool window (the frame on the left), open the “helloworld” folder, right-click the “src” directory, and choose “New > Java Class”. Name your class “Application”, and include a “main” method, so that the code will look like the sample below.

public class Application {
   public static void main(String args[]) {
       System.out.println("Hello World");
   }
}

Tip: an easier way to create the main method is to just write psvm (short for public static void main) and select the first auto-complete suggestion. Try it.

Right-click the “Application” class on the Project tool window and choose “Run ‘Application.main()’”. This will compile and run your application. You should see “Hello World” displayed on the console.

Congratulations, you have created your first Java application!

Take a moment to check the “out” directory. This is where IntelliJ generates the result of the compilation process. You should be able to find there an Application.class file. This file contains the bytecode, generated from the source code, that can be run using any Java Virtual Machine (JVM).

2. Initialize Git Repository

Git is the version control system that we will use to manage the different versions of our files. It can be used locally (on our machine only) or with a server (to share code and collaborate with other team members). We will start by using it locally.

Tip: For now, the command line will be used for all our interactions with Git. The use of a *nix OS is advised, but otherwise you may find the command line as Git Bash on Windows and Terminal on MacOS.

Start by changing directory to the root directory of our newly created IntelliJ project and initialize a new Git repository.

cd directory/of/my/project/helloworld # change to the directory
git init                              # initialize the git repository

This will create a ”.git” directory, where the version history of our project will be locally kept. You can check the current directory contents by doing:

ls -la       # list directory contents

Now let’s see the status of your files by running the command below. Try it with and without the -s flag, which gives us a more concise output.

git status
git status -s

As you can see by the output, all files are reported as untracked by Git. You will create the first Git version of the project using some of these files, but it would be best to simply ignore some of the others:

To do this, execute the command below. Notice that, after this, git status no longer reports the ignored files as untracked.

echo "out/" >> .gitignore     # this appends out to the .gitignore file
echo ".idea/" >> .gitignore   # this appends .idea to the .gitignore file
echo "*.iml" >> .gitignore   # this appends *.iml* to the .gitignore file
git status

Now it is time to create the first version of our project. Let’s start by adding the files to the staging area and then committing them using the commands below. Execute the git status command before and after each one of these steps to see how the files change state.

git status
git add .gitignore src             # add files to the staging area
git status
git commit -m "Initial commit"     # commit the new version
git status

3. Making Another Commit

Now that we know how to add files to the staging area and committing them, follow the steps below:

4. Inspecting the log

To see the list of commits that we have so far (only two at this point) execute the command below (press “q” to exit).

git log

You can also provide the –oneline flag to the git log command to get a cleaner list of commits and the -<number> flag to limit the number of entries to be shown.

git log --oneline -1

The -p flag shows the difference (the patch output) introduced in each commit.

git log -1 -p

5. Adding and Removing

A single commit may change multiple files in different ways:

Tip: Deleting “first.txt” and adding that change to the staging area could have been done with a single command: “git rm first.txt”.

6. Partially Staged Files and Diff

7. Branching and Merging

A branch in Git is a pointer to a particular commit. As we start making commits, we are given a default “master” branch. Every time we create a new commit, the current branch pointer moves forward automatically.

git log --oneline      # shows commits pointed by existing branches
git branch             # shows branch that we're currently working on

When executing the commands above we may also notice a reference to “HEAD”. This is a special pointer used by Git that defines which is the current branch. The asterisk in the output of the second command also represents the HEAD.

Let’s now create a new branch with the name “testing” and switch to it.

git branch testing    # create the new branch
git branch            # check that we're still on the master branch
git checkout testing  # switch to the testing branch
git branch            # check that we're really on the testing branch now

Tip: The commands “git branch testing” and “git checkout testing” could be done in a single step with the equivalent command: “git checkout -b testing”.

Now, let’s make a few changes to the “third.txt” file and add a new commit to the “testing” branch:

Let’s look at the effect that this has on our commit log. Run the command below:

git log --oneline

Notice that the “testing” branch points to the latest commit, but the “master” branch is still pointing to the previous one, where the branch was created.

Let’s switch back to the “master” branch and see the difference. Run the commands below, and notice how there’s no trace of the commit that we made in the “testing” branch.

git checkout master                # switch back to the master branch
git log --oneline

Now that we are back in our “master” branch, let’s do some more changes:

At this point we have two divergent histories that can keep evolving independently but that will eventually be merged back together. We will now merge the testing branch back into the master.

Notice: This is done from the “master” branch, which will receive all the changes from the testing branch.

Git uses two main strategies to merge branches: a) Fast-forward merge (when there is no divergent work) and b) Three-way merge (when there is divergent work).

In this example, there was divergent work, so a three-way merge is needed. This implies that a new commit with the merged files will be created. Execute the following command to merge the two branches and Git will ask you to enter a message for the new commit.

git merge testing

As we do not need the “testing” branch any longer, we can delete it. This will leave all commits intact, it will only delete the pointer.

git branch -d testing     # this cannot be done from inside the testing branch

8. Handling Merge Conflicts

Not all merges work as cleanly as in the previous example. If you change the same part of the same file in the two different branches, Git won’t be able to merge them cleanly. Let’s try to create this scenario.

Start by creating (but not switching to) a “testing” branch again.

git branch testing

Now let’s change our “Application.java” file so that it contains the following code:

public class Application {
    public static void main(String[] args) {
        System.out.println("Thank you for using the Hello World App");
    }
}

Stage and commit this change with the message “Added a welcoming message”.

Now change into the “testing” branch and notice that we still have our previous version of the “Application.java” file. Change it so it looks like this:

public class Application {
    public static void main(String[] args) {
        System.out.println("Hello World");
        System.out.println("Version 1.0");
    }
}

Stage and commit this change with the message “Added the version number”.

And finally, try to merge the changes of the “testing” branch back into the “master” branch.

git checkout master     # go back to the master branch … 
git merge testing       # … and try to merge testing

The result of this should be the following message from Git.

Auto-merging src/Application.java
CONFLICT (content): Merge conflict in src/Application.java
Automatic merge failed; fix conflicts and then commit the result.

Note that the merge is still in progress, and to complete it we must first resolve the conflict. More information can be obtained by running git status, which provides the output below.

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

	both modified:   src/Application.java

no changes added to commit (use "git add" and/or "git commit -a")

So, as the output explains, our options are: * Fix the conflicts and manually do the merge commit * Abort the merge, thus reverting to how things were before running git merge testing.

Let’s go with the first option. Open the Application.java file and you will find it annotated with conflict markers, like in the example below.

public class Application {
    public static void main(String[] args) {
<<<<<<< HEAD
        System.out.println("Thank you for using the Hello World App");
=======
        System.out.println("Hello World");
        System.out.println("Version 1.0");
>>>>>>> testing
    }
}

Git is telling us that the first fragment of text annotated with conflict markers came from the branch pointed to by “HEAD” (i.e., the master branch), and that the second part came from the “testing” branch.

We must modify this file to be like the result that we intend for the merge. Assuming that we want to keep the changes from the two divergent branches, we could simply delete the conflict markers and the line outputting Hello World to make the code look like the one below:

public class Application {
    public static void main(String[] args) {
        System.out.println("Thank you for using the Hello World App");
        System.out.println("Version 1.0");
    }
}

We can now proceed with the merge by manually doing the commit. Execute “git status” between each operation to see how Git reports the state of the merge between each operation.

git status
git add src/Application.java
git status
git commit -m "Merge branch 'testing'"   # commit resolved conflicts and end the merge
git status
git log --oneline

As we do not need the “testing” branch any longer, we can delete it again:

git branch -d testing

9. Using Remotes

A remote is a Git repository that is hosted elsewhere (another folder, the local network, the internet, …). You can push and fetch data to and from remotes.

We already have a local repository. Let’s now create an empty repository on GitHub where we can push our local repository to:

git remote add origin git@github.com:yourusername/helloworld.git

We can now list the remotes of the project by executing the command below. Try it out and check that the origin remote is setup for both fetch and push.

git remote -v

Now it is time to push our local repository to the remote that we have just configured. We can do this by executing the command below, but make sure that you are on the “master” branch locally before you do.

Important: In order to push to your repository, you have to authenticate with GitHub first. If we are using SSH (using a remote that starts with git@github.com), we need to setup some SSH keys first (see https://help.github.com/articles/connecting-to-github-with-ssh/). An easier, but not recommended, way would be to use an HTTPS remote https://github.com/yourusername/helloworld.git).

git branch
git push origin master

In the command above we had to explicitly say what remote (“origin”) and remote branch (“master”) we want to push to. However, it is more convenient if we do not have to type this every time. For this, we can use the notion of tracking branches. These are local branches that have a direct relationship to a specific remote branch. We can set up a tracking branch by running the command below, which will connect our local current branch (“master”) to the master branch on the origin remote.

git branch -u origin/master 

And next time we can just do:

git push

When we are collaborating with others, often there may be changes in the remote that we do not yet have in our local repository. Let’s simulate this scenario by adding a new file to the remote repository using GitHub’s web interface:

git fetch

This only fetches the new commit from the remote, it does not merge them. The remote branch “origin/master” is also updated in your local repository and is now pointing to the fetched commit. So we can do the following to merge:

git merge origin/master

Tip: We could have combined these two operations (fetch and merge) in a single command: “git pull”.

Conflicts may occur when we pull files that have also been modified and committed locally, in which case we will need to resolve them as we did with when merging a branch.

10. Git Clone and Import

What if we lose all our data and we have to recover our entire project? Let’s test that:

Now, clone your project from the command line:

git clone git@github.com:arestivo/helloworld.git
cd helloworld
ls -la # checking if everything is still there

Now, because we did not add the ”.idea” directory to our repository, we have to import the project again into IntelliJ using the “Import Project” option.

11. Collaborating

Try adding other students as collaborators on your GitHub project and work with them.

12. Improving your Git Branching

Now it is time to test your brand new branching skills using Learn Git Branching.