========================= Team Development With Git ========================= :Author: Nick Efford :Contact: N.D.Efford@leeds.ac.uk :Status: Final :Revised: 2017-09-15 The aim of this worksheet is to give you further experience of working with Git_, focusing on those aspects relating to developing software in teams. We assume you are familiar with basic use of Git from the work done in Semester 2 last year. Useful background information can be found in Eric Sink's `Version Control By Example`_ (especially `Chapter 8`_), Chacon & Straub's `Pro Git`_ and Westby's `Git For Teams`_. .. _Git: https://git-scm.com .. _Version Control By Example: http://www.ericsink.com/vcbe/html/ .. _Chapter 8: http://www.ericsink.com/vcbe/html/git_example.html .. _Pro Git: http://www.git-scm.com/book/en/v2 .. _Git For Teams: http://shop.oreilly.com/product/0636920034520.do .. warning:: This worksheet is a detailed simulation of how a pair of developers can collaborate using Git, by means of a shared remote repository. It assumes that you have a functioning GitLab_ account and that you have configured an SSH key pair for it, as instructed in the worksheets for COMP1721 last year. **Make sure you read the instructions carefully and follow them exactly, as things will become very confusing if you miss a step!** .. _GitLab: https://gitlab.com Preparation =========== #. Before doing anything else, check whether you need to configure Git with your preferred text editor. In a terminal window, enter .. code-block:: none git config --list If you don't see an entry for the ``core.editor`` setting, or if you see an entry but it isn't for the editor you prefer to use, then you need to give ``core.editor`` a suitable value. Below are commands that will set it properly for Atom, Emacs and Vim. .. code-block:: none git config --global core.editor "atom --wait" git config --global core.editor emacs git config --global core.editor vim Use one of these, or modify as needed if you prefer a different text editor. #. Log in to your GitLab account and create a new private repository named ``teamgit``. This repository will be the means by which two imaginary developers, Alice and Bob, collaborate. Each of them will work on a local clone of this repository. Each will need to push their changes up to ``teamgit`` so that these changes become visible to the other. Each will need to pull changes they don't yet have from ``teamgit`` down into their local clone. #. On a Linux machine, create a directory for this worksheet, also named ``teamgit``. Download the shell script :download:`set-up` into this directory. #. Open *two* terminal windows. In each one, ``cd`` into the newly created ``teamgit`` directory. These terminal windows will be used to represent the two developers, Alice and Bob. In the first terminal window, enter a command like this: .. code-block:: none source set-up Alice USERNAME/teamgit Substitute your actual GitLab username (which will normally be the same as your University username) for ``USERNAME`` in the above. This command will clone the GitLab repository and then configure the clone and the terminal window to simulate Alice's working environment. You should see that the shell prompt and window title have both changed to ``Alice``. #. Leave the Alice terminal window open, then switch to the second terminal window and enter .. code-block:: none source set-up Bob USERNAME/teamgit (again, substituting your GitLab username for ``USERNAME``) This will create the equivalent simulated working environment for Bob. .. note:: For the remainder of this worksheet, stay logged in to GitLab so that you can monitor the ``teamgit`` repository, and keep the terminal windows for Alice and Bob open. Pushing Changes to a Remote Repository ====================================== Let us imagine that Alice contributes to the project first. #. Go to the ``Alice`` terminal window. Use a text editor to create a file called ``Hello.java`` in the ``Alice`` directory, containing this code: .. code-block:: java class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } } Stage and commit these changes using the following commands: .. code-block:: none git add Hello.java git commit -m "First version of program." #. Change the greeting string from "Hello World!" to "Hello Everyone!", then stage and commit your change in a single step like so: .. code-block:: none git commit -a -m "Changed greeting." #. Make a third change to the program, so that it can issue a personalised greating to the user. The final result should look like this: .. code-block:: java class Hello { public static void main(String[] args) { if (args.length > 0) { System.out.println("Hello " + args[0] + "!"); } else { System.out.println("Hello Everyone!"); } } } Stage and commit this change with .. code-block:: none git commit -a -m "Issue a personalised greeting or a default." You should now have three commits in the ``Alice`` repository. #. The ``Alice`` repository retains an association with the remote repository from which it was cloned. To see this, enter .. code-block:: none git remote This should display ``origin`` - the default 'nickname' that Git gives to the original repository from which this one was cloned. If you now enter .. code-block:: none git remote show origin You will see further details about ``origin`` - in particular, the URLs used for fetch and push operations. #. To push Alice's changes up to the shared repository, do this: .. code-block:: none git push origin master This means 'push changes to the ``master`` branch of the ``origin`` remote.' #. Enter ``git log --graph`` in the ``Alice`` terminal window. (The reason for using the ``--graph`` option will become clear in due course.) You should see something like this: .. figure:: alice1.png :scale: 65 % :align: center Alice's repository, after her commits If you try the same thing in the ``Bob`` terminal window, you'll get an error, as Bob has made no commits of his own yet and doesn't have any of Alice's either. #. Back in GitLab, click on the :guilabel:`Repository` link from the menu at the top of the page. Click :guilabel:`Commits` to view the commits as a simple list, then click :guilabel:`Graph` to view them as a graph. The latter should look like this: .. figure:: shared1.png :scale: 80 % :align: center Graph of Alice's commits viewed in GitLab Retrieving Changes From a Remote Repository =========================================== Typically, this involves two operations. First, the ``fetch`` command is used to bring in changes from the remote repository. These fetched changes are stored in a special **branch** of your local repository. Then the ``merge`` command is used to merge those changes with another branch - e.g., the 'master' branch that Git uses by default. Git also provides a ``pull`` command, which essentially performs a ``fetch`` followed by a ``merge``. If you don't wish to inspect fetched changes and are reasonably confident of the merge succeeding, using ``pull`` will save you a little bit of typing. #. In the ``Bob`` terminal window, fetch the changes from ``origin`` like so: .. code-block:: none git fetch origin You can see in the output from Git that it has created a new branch called ``origin/master``, containing the changes. It you enter ``git log`` at this point, it will still give an error, because the changes are not yet merged into the ``master`` branch of the local repository. #. Merge the fetched changes with the local repository like so: .. code-block:: none git merge origin/master This means 'merge changes from branch ``origin/master`` into the branch I am currently in' (which happens to be ``master``). Now try ``git log --graph`` again. You should see that the ``Bob`` repository now has the same three commits as ``Alice`` and GitLab. Non-Conflicting Merges ====================== What happens if both Alice and Bob are working simultaneously? #. In the ``Alice`` repository, edit ``Hello.java`` and change the default greeting in the ``else`` block from "Hello Everyone!" to "Hey Everyone!". Commit your change with a message of "Changed default greeting slightly". Switch to the ``Bob`` repository, edit ``Hello.java`` and change the first part of the personalised greeting in the ``if`` block from ``Hello`` to ``Greetings``. Commit your change with a message of "Changed personalised greeting." Enter ``git log --graph`` in the ``Alice`` and ``Bob`` terminal windows. You should see that the repositories now have **divergent histories**: the latest commits in each are different. These differences are purely local, however. Neither of the commits are present in GitLab yet. .. figure:: alice2.png :scale: 65 % :align: center .. figure:: bob1.png :scale: 65 % :align: center #. Use ``git push origin master`` to push Alice's changes up to GitLab. Then try performing the same operation for Bob. You should find that the Alice's push succeeds, but Bob's fails. Bob isn't allowed to push because Alice has already pushed changes that he doesn't yet have. It doesn't matter that the changes are non-conflicting ones; Git requires Bob to fetch and merge those missing commits locally before allowing him to push. .. figure:: bob2.png :scale: 65 % :align: center Bob's failed push #. To resolve the problem, start by fetching the changes that Bob doesn't have. Enter the following in the ``Bob`` terminal window: .. code-block:: none git fetch origin This succeeds. (Remember that the changes are fetched into a *different* branch from the master branch in which Bob is working, so fetching alone won't cause problems.) Then attempt to merge the fetched changes with Bob's changes, using .. code-block:: none git merge origin/master At this point, Git will probably start up your chosen text editor so that you can provide a message describing the merge. Leave the suggested text as-is, then save and quit the editor. This will cause a **merge commit** to take place. Git will report the result something like this: .. code-block:: none Auto-merging Hello.java Merge made by the 'recursive' strategy. Hello.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) **Git can handle the merge automatically because the changes made by Alice and Bob do not conflict** (they are made on separate lines of the file). .. note:: If you don't want to compose the merge commit message in a text editor, you can instead supply it on the command line using the ``-m`` option, just as for regular commits. #. Try ``git log --graph`` in ``Bob``. This will give you an 'ASCII art' representation of how the merge has combined the diverging histories resulting from Alice's and Bob's changes. .. figure:: bob3.png :scale: 65 % :align: center Result of merging in Bob's repository #. Try the ``git status`` command in ``Bob``. It will report that "your branch is ahead of 'origin/master' by 2 commits" (corresponding to Bob's changes and the merge). This is a useful reminder that you need to push the changes back to origin. Do this now with .. code-block:: none git push origin master This time, the command will succeed. #. To bring Alice up to date, do this in the ``Alice`` repository: .. code-block:: none git pull origin master Note the use of a single pull command here, rather than separate fetch and merge commands. We happen to know that Alice hasn't made any changes since Bob's, so there is no need to inspect what has been fetched before attempting to merge. Notice the output from Git: .. code-block:: none Fast-forward Hello.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 'Fast-forward' is Git's way of telling you that it has simply replayed Bob's commits on top of Alice's; there is no need to actually merge anything. If you now run ``git log --graph`` in the ``Alice`` window, you should find that it looks exactly the same as the output for ``Bob``. Both logs show identical histories of commits, each including the merge. If you refresh the Graph page on GitLab, it should now look something like this: .. figure:: shared2.png :scale: 80 % :align: center ``Alice``, ``Bob`` and the remote repository are all in sync. Conflicting Merges ================== What happens to the merging process if Alice and Bob make changes that conflict - e.g., if they each change the same line of code? #. In ``Bob``, modify ``Hello.java`` so that it looks like this: .. code-block:: java import javax.swing.JOptionPane; class Hello { public static void main(String[] args) { if (args.length > 0) { JOptionPane.showMessageDialog(null, "Greetings " + args[0] + "!"); } else { JOptionPane.showMessageDialog(null, "Hey Everyone!"); } } } Commit and push Bob's changes using these commands: .. code-block:: none git commit -a -m "Use a dialog box for greetings." git push origin master #. In ``Alice``, modify the line printing the personalised greeting so that it looks like this: .. code-block:: java System.out.println("Hey " + args[0] + "!"); Commit this change with .. code-block:: none git commit -a -m "Use a different personalised greeting." #. A push from ``Alice`` will, of course, fail - because Bob has already pushed his changes to GitLab. So you need to ``fetch`` from ``origin`` and merge locally in ``Alice``, like so: .. code-block:: none git fetch origin git merge origin/master You should find that the ``fetch`` command succeeds but automatic merging fails, with an error like the following: .. code-block:: none Auto-merging Hello.java CONFLICT (content): Merge conflict in Hello.java Automatic merge failed; fix conflicts and then commit the result. #. Edit ``Hello.java`` and you will see that Git has added **conflict markers** to highlight conflicting lines of code. Alice's line appears between ``<<<<<<< HEAD`` and ``=======``, whereas Bob's line appears between ``=======`` and ``>>>>>>> origin/master``. If you are using version 1.18 or newer of the Atom editor, it will augment these with colour highlighting. .. figure:: conflict.png :scale: 70 % :align: center File containing conflict markers, as seen in Atom At this point in our scenario, Alice must decide how to resolve the conflict. She can keep her changes and discard Bob's, do the opposite, or somehow combine the two sets of changes. Some discussion with Bob may be advisable! #. Change ``Hello.java`` so that it combines Alice's and Bob's changes, and remove the conflict markers. If you are editing in Atom 1.18 or newer, you can simply use the buttons that it provides to accept one or other set of changes, or some combination of the two. You should end up with something like this: .. code-block:: java import javax.swing.JOptionPane; class Hello { public static void main(String[] args) { if (args.length > 0) { JOptionPane.showMessageDialog(null, "Hey " + args[0] + "!"); } else { JOptionPane.showMessageDialog(null, "Hey Everyone!"); } } } Save the file, then use the ``add`` command to tell Git that you have resolved the conflict: .. code-block:: none git add Hello.java Then run ``git status`` again. Git will tell you that the conflict has been fixed but the merge is still in process. #. Now commit the changes using ``git commit``, *without* supplying a message. Your preferred text editor will start up, containing text like the following: .. code-block:: none Merge remote branch 'origin/master' Conflicts: Hello.java # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. # Lines beginning with ``#`` are treated as comments and are ignored by Git. The text before the ``#`` is Git's suggested commit message. Change it or leave it as it stands, as you wish, then save and quit the editor [#]_. #. Do ``git log --graph`` in ``Alice`` to inspect the history and see the merge that has just been committed. Then push the changes back up to GitLab with .. code-block:: none git push origin master #. Finally, use ``git pull origin master`` to update Bob's repository. If you use ``git log --graph`` in both terminal windows, you will see that Alice and Bob are consistent once again. The final commit graph in GitLab will look like this: .. figure:: shared3.png :scale: 80 % :align: center Next Steps ========== If you aren't using Atom or IntelliJ and therefore aren't benefiting from the support they provide for merging, you might want to investigate some of the specialised merge tools that can be used with Git. Atlassian's `conflict resolution tips page`_ provides some pointers. **Note that this worksheet has covered only the most basic aspects of collaboration using Git.** You need to learn more about branches, rebasing and the various Git workflows to become a fully capable user of Git. You'll find the books mentioned at the start of the worksheet useful in this regard. Also useful are the following online articles: * Rein Heinrich's 2009 blog post on `a Git workflow for agile teams`_ * Vincent Driessen's 2010 blog post on `a successful Git branching model`_ * The `GitLab Flow`_ best practices documentation Additionally, the `Learn Git Branching`_ interactive tutorial is highly recommended. .. _conflict resolution tips page: https://developer.atlassian.com/blog/2015/12/tips-tools-to-solve-git-conflicts/ .. _a Git workflow for agile teams: http://reinh.com/blog/2009/03/02/a-git-workflow-for-agile-teams.html .. _a successful Git branching model: http://nvie.com/posts/a-successful-git-branching-model/ .. _GitLab Flow: https://gitlab.com/help/workflow/gitlab_flow.md .. _Learn Git Branching: http://learngitbranching.js.org ---- .. [#] If you wish to abort the commit, simply remove all of the suggested text appearing before the comments, then save and quit the editor.