Skip to content

Lab 07

In this lab session you will practice advanced command line features of git, notably strategies to combine work of multiple features on the main branch. You do not need any specific code to follow this lab session.

Preparation

You'll be working with a playground repository for the remainder of the lab. Before you proceed to the actual exercises, here are some preliminary preparing steps.

Create a playground repo

  1. Create a new empty folder on your Desktop. (Or wherever else you prefer)
  2. Open a terminal, navigate inside the newly created folder
  3. Initialize a new repository with git init

Create a markdown file

  • You will not be working with actual code, the remainder of the lab session requires only a short poem in markdown format.
    • Tracking changes of the sample poem allows you to simulate version control, e.g using branches.
  • Inside your playground repository, create a new text file: "keksli.md"
  • Paste the following the poem file, and save the file:

    # Keksli, the Little Piece of Cake
    
    1) Oh Keksli, sweet as frosting’s kiss,
    2) A tiny fluff of gentle bliss.
    3) With eyes that sparkle, soft and bright,
    4) You bring the world pure joy and light.
    
    5) A little piece of cake, they say,
    6) But cuter than the sweetest tray.
    7) With every purr and playful leap,
    8) You melt our hearts and softly keep—
    
    9) That sprinkle of sweetness, warm and true,
    10) A Keksli-sized delight in all you do.
    

  • Add the file to your repository and create an initial commit:

    git add keksli.md
    git commit -m "Added initial poem" 
    

  • Backup the playground folder in its current state, e.g. by making a copy, or by zipping it.

  • I'll regularly ask you to reset to the initial state. In that case, remove the original folder and restore from your backup on disk.

Creating sample commits

Throughout the remainder of the lab I'll frequently ask you to modify the poem and add the changed version to a new commit.

  • If I do not further specify which line to edit, simply add a new line at the end with "...". Otherwise change the specified line (1), 2), ...).
  • Finally, make sure your poem edit manifests to a new commit, with:
git add keksli.md
git commit -m "Modified poem" 
  • You can use a custom commit message if you wish, for better traceability.

Dog logging

  • The standard git log command neither prints the entire graph, nor does it neatly visualize how commits connect.
  • In class, you've seen the ADOG acronym, as little memory-help for the longer command:
    git log --all --decorate --oneline --graph
  • Create an alias for the ADOG acronym, with:
    git config --global alias.adog "log --all --decorate --oneline --graph"
  • Test the new alias command. Go into your sample repo and type: git adog

New branches

In this first exercise you'll be training the creation of new branches, and how to navigate branches

Creating a new branch

  • Branches are usually created per feature. We'll now assume that you want to create an extended version of the poem with some extra lines.

Your turn

  1. Create a new branch extended-poem, using git checkout -b
    • Note: the checkout command automatically places your HEAD on the new branch. No need to manually switch.
  2. Modify the poem and create a new commit
  3. Inspect the full commit graph with git adog
    • Carefully read the output. Why is the graph visualized graph still a line?

Switching branches

  • The universal command for navigating the graph is git checkout
  • git checkout expects either a commit hash, or a branch name.
    • commit hashes detach the HEAD and place it in the requested commit.
    • branch names attach the HEAD to the last commit of the specified branch
Is there a difference between checkout of the last commit on a branch, and checkout by branch name ?

Yes, there is a difference! In both cases, the HEAD ends up on the last commit of a line. But checkout by commit hash cannot reliably result in an attached HEAD, because commits can be shared by multiple branches.
Illustration:

*   059ba92 (HEAD, main, feature) fixed merge conflict
|\
| * e0a2244 feature XYZ implemented
* | ecf8311 little bugfix
|/
* 1a31ca2 initial commit
Commit 059ba92 is shared by main and feature branch. A git checkout 059ba92 cannot be resolved to a unique branch name. HEAD will be detached.
(In fact, checkout by hash always detaches HEAD, just to avoid the above ambiguity !)

  • git switch is a bit securer than git checkout, because it is a variant of checkout that only accepts branch names.
    • Using git switch you can never end up in detached HEAD state.
    • Navigating between branches with git switch is preferable for novice users.

Your turn

  1. Navigate back to the main branch
  2. Modify the poem and create a new commit
  3. Inspect the graph again with git adog. What has changed ?

Detached HEAD commits

  • Usually you want your branches to diverge from an attached HEAD, i.e. you want branches to spawn from the end of a branch.
  • Yet, it is possible to create implicit branches, by committing while in detached HEAD.
    Illustration:
    • In a series of two commit, checkout of the initial commit results in detached HEAD.
      git adog
       * adce8cb (main) second commit
       * 3af628f (HEAD) initial commit
      
    • Creating a new commit in detached HEAD would be in conflict with second commit
    • Git therefore implicitly creates a new unnamed branch. (HEAD does not point to a branch name !)
      git adog
       * 310e6b7 (HEAD) commit from detached head
       | * adce8cb (main) second commit
       |/
       * 3af628f initial commit
      
  • Commits from detached HEAD (as the above 310e6b7) are dangerous, because they are not associated with any branch, and hard to find again later. Work committed from detached HEAD is easily lost.
  • Luckily it is possible to belatedly associate the commits to a branch name!

Your turn

  1. Reset the playground repo to it's original state.
  2. Add a new commit.
    • Inspect the repo graph with git adog. You should only see two consecutive commits, with HEAD attached.
  3. Detach the HEAD, by checkout of the initial commit by ID.
    • Inspect again with git adog, the output should match the first of the above listings.
  4. With your HEAD detached, create a new commit.
    • Inspect your graph again with git adog. The output should now match the second of the above listings.
    • Write down the ID of the commit created in detached HEAD mode.
  5. Attempt to abandon your last commit, by checkout of the main branch.
    • Git will reattach your HEAD.
    • Git will also issue a warning that you are leaving a commit behind. Git will also tell you how to belatedly associate the commit left behind to a new branch.
  6. Switch back to the commit left behind, by checkout (of the ID you noted down)
  7. Use the command previously proposed by git, to associate the commit left behind with a new branch name, e.g. implicit-branch
    • Inspect the repo graph once more with git adog
Is your HEAD now attached?

No. git adog shows that HEAD is on the last commit of implicit-branch. But there is no -> symbol. The HEAD remains detached until you attach it with git checkout implicit-branch (or git switch implicit-branch).
Before attaching HEAD:

 * 310e6b7 (HEAD, implicit-branch) commit from detached head
 | * adce8cb (main) second commit
 |/
 * 3af628f initial commit
After attaching HEAD: (with git switch implicit-branch)
 * 310e6b7 (HEAD -> implicit-branch) commit from detached head
 | * adce8cb (main) second commit
 |/
 * 3af628f initial commit

Merging

  • So far you've learned how to create new branches
  • Branches are like diverging paths. With every branch created, you create a new bifurcation.
  • Next you'll train how to unite them again. This process is called merging.

Merging without conflict

  • In the easiest case the changes made on two branches are not in conflict, e.g. because the changes made affect entirely disjoint files.
  • The standard procedure for merging is: 1) Place your HEAD on the branch where you want to receive / integrate changes: git checkout receiving-branch 2) Merge form the branch containing the changes you want to integrate: git merge branch-with-commits-to-integrate

Your turn

  1. Restore the playground project to its original state.
  2. Add a new branch feature
  3. Add a new commit to the feature branch.
  4. Place your HEAD back on main
  5. Create a new file toto.md with content # Hello and store it in a new commit on main.
  6. Merge feature into main
  7. Verify the merge was successful:

  8. You should still have the toto.md file.

  9. You should also see the extended poem version.
  10. git adog should visualize the merge.

Merging with auto-conflict resolving

Merging with auto-conflict resolving happens when two branches modified the same file, but in a non-conflicting way, i.e. not the same line or code element.

Your turn

  1. Restore the playground project to its original state.
  2. Add a new branch feature
  3. Add a new commit to the feature branch, where the first line of the poem is changed to "1) Oh Keksli, sweet as a nutella kiss,"
  4. Place your HEAD back on main
  5. Add a new commit to the main branch, where the last line of the poem is changed to "10) Oh Keksli-sized delight, we all love you. <3"
  6. Merge feature into main

  7. At this point you might see a weird editor opening up.

  8. That's VIM, a powerful, but unintuitive text editor.
  9. To just quit the editor, type this sequence: ESC : w q ENTER (for write and quit the editor)

  10. Verify the merge was successful:

  11. Inspect the git output for a success message.

  12. Inspect the keksli.md content and verify first and last line of the poem have been modified.
  13. git adog should visualize the merge.

Merging with conflict

  1. Restore the playground project to its original state.
  2. Add a new branch feature
  3. Add a new commit to the feature branch, where the first line of the poem is changed to "1) Oh Keksli, sweet as a nutella kiss,"
  4. Place your HEAD back on main
  5. Add a new commit to the main branch, where the first line of the poem is changed to "1) Oh Keksli, sweet as a maple syrup kiss,"
  6. Merge feature into main
  7. Verify you get a CONFLICT message:
    Auto-merging keksli.md
    CONFLICT (content): Merge conflict in keksli.md
    Automatic merge failed; fix conflicts and then commit the result.
    
  8. Inspect the keksli.md file. You should see version markers:
    # Keksli, the Little Piece of Cake
    
    <<<<<<< HEAD
    1) Oh Keksli, sweet as a maple syrup kiss,
    =======
    1) Oh Keksli, sweet as a nutella kiss,
    >>>>>>> feature
    2) A tiny fluff of gentle bliss.
    3) With eyes that sparkle, soft and bright,
    4) You bring the world pure joy and light.
    ...
    
  9. Resolve the merge conflict by removing the version markers and deciding for one version to remain. Then safe the file and create a final "resolve" commit.

When one of the branches is main, always first merge into the other. Then merge back.

  • The interest of branches is usually to develop a feature without damaging main while work is in progress.
  • When you're ready to merge, always first merge main into the feature branch, merging can fail and leave your repo in a problematic state. A safer procedure is:
    1. Merge main into feature.
    2. If needed, resolve conflicts and create a new "solved" commit.
    3. Merge feature back into main.
      ---
      title: "Merging first main into feature, then merging back"
      ---
      gitGraph:
          commit id: "1"
          commit id: "2"
          branch "feature"
          checkout "feature"
          commit id: "3"
          checkout "main"
          commit id: "4"
          checkout "feature"
          merge "main" type: REVERSE
          commit id: "manual resolve"
          checkout "main"
          merge "feature" type: HIGHLIGHT

Deleting branches

  • If you only create branches and never delete them, you git graph will over time end up pretty cluttered.
  • Ideally branches are removed from the graph once they are no longer needed.
  • To definitely remove a branch, you have to remove it locally and remotely.

Local

  • Removing a branch locally translates to removing a branch from the local graph.
  • Revise the course content and look up how to delete a local branch.

Your turn

  1. Add another commit to your feature branch
  2. Remove the last feature branch from your repo.
    • First attempt the command git branch --delete feature
    • Why is the command rejected. Identify two strategies to delete the branch anyway.
  3. Use git adog to verify it has disappeared.
  4. Use git branch -a to verify it is no longer listed.

Remote

  • Removing a branch remotely translates to removing a branch from the remote (server) graph.
  • Removing a remote branch only makes sense if there is a remote repository associated to your local repository.

Your turn

  1. Go to UQAM's Gitlab webpage and link you local repo to a remote server:
    • Login into gitlab.
    • Click the + symbol on the top left, then new project / repository
      1. Select: Create blank project
      2. Select a name for your project, e.g. lab08
      3. Uncheck: Initialize repository with a README
      4. Click: Create project
    • Enter the command to link you local repo to the remote server: (replace max by your username)
      git remote add origin git@gitlab.info.uqam.ca:max/lab08.git
  2. Use git branch -a, inspect the list of local and remote branches.
  3. Create a new local branch feature.
  4. Add at least one commit to the feature branch.
  5. Push the branch to the remote repository, with:
    git push --set-upstream origin main
  6. Figure out how to push the feature branch to the remote repository.
  7. Verify with git branch -a that both branches exist on the remote repository.
    • Additionally log in to the UQAM gitlab website and inspect the project page. Can you spot both branches ?
  8. Delete the remote feature branch using:
    git push -d origin feature
  9. Use git branch -a again to verify the `feature branch has disappeared.

Tagging

Tags are additional human-readable identifiers for commits.

Adding tags

Tags are always first created locally, and then pushed to the server.

Your turn

  1. Reset your playgound project.
  2. Create tag for the initial commit on the main branch. Illustration:
    ---
    title: "Tagged initial commit"
    ---
    gitGraph:
    commit id: "1" type: HIGHLIGHT tag: "v1.0"
  3. Push the tag to the server
  4. Find the tags
    • Inspect git adog. Can you find the tag ?
    • Log into UQÀM's GitLab server, and navigate to your project. Can you find the tag ?

Cherry Picking

Cherry-picking allows you to merge with a specific commit rather than merge with an entire branch.

Cherry pick a single poem line

  • In this exercise you'll be making several modifications to the keksli poem on a feature branch.
  • Afterward, you'll merge exactly one of these changes into main branch.
  • Illustration:
    ---
    title: "Cherry picking a single commit from a feature branch"
    ---
    gitGraph:
        commit id: "1"
        branch "feature"
        checkout "feature"
        commit id: "2"
        commit id: "3"
        commit id: "4"
        checkout "main"
        commit id: "5"
        checkout "main"
        cherry-pick id: "3"

Your turn

  1. Reset the playground project to its original state
  2. Create a feature branch
  3. Make three consecutive commits on the feature branch:
    1. Change the first poem line to 1) Oh Keksli, sweet as a cinnamon kiss,. Make a commit on feature.
    2. Change the second poem line to 2) A tiny fluff of chocolate bliss.. Make a commit on feature.
      Write down the commit id of the second line change.
    3. Change the third poem line to 3) With eyes that glitter, soft and bright,. Make a commit on feature.
  4. Place the HEAD back on main. Make a new commit.
  5. Cherry-pick the second poem line change to main, using the commit ID of the second line-change.
  6. You'll receive a merge conflict. Inspect and solve the conflict. Issue a final commit for the solved conflict.
  7. Inspect the graph with git adog
Is there anything peculiar with the git adog output ?

Yes! The graph representation does not show the chery-picking! Indeed git log never shows cherry-picks, only full branch merges.

Rebase

Rebasing allows you to straighten the git commit graph by hindsight.

Rebasing a feature branch

  • Usually rebase is applied before merging a feature branch back to main
  • By first rebasing main into feature, there is no risk of a broken main branch (just like when merging main into feature)
  • Rebasing creates a replay of every commit on feature, based on the most recent main commit.
  • This transforms the graph, so commits of main and feature no longer occur concurrent, but sequential.
  • The final merge is always guaranteed to work, for any conflicts have already been cleared out during rebase.

Your turn

  1. Reset your playground project
  2. Create a feature branch
  3. Make three consecutive commits on the feature branch
  4. Attach your HEAD to main
  5. Change the poem title line to #Keksli, the Rebased piece of Cake
  6. Attach your HEAD to feature again. Your commit graph should now look like this:
    ---
    title: "Feature branch lacks recent main commit"
    ---
    gitGraph:
        commit id: "1"
        branch "feature"
        checkout "feature"
        commit id: "2"
        commit id: "3"
        commit id: "4"  type: HIGHLIGHT
        checkout "main"
        commit id: "5"
  7. Rebase the feature branch, to spawn from the most recent main commit, using git rebase main
  8. Merge feature into main
  9. Inspect the graph structure with git adog. Verify it corresponds to:
    ---
    title: "Feature branch rebased and merged to main"
    ---
    gitGraph:
        commit id: "1"
        commit id: "5"
        branch "feature"
        checkout "feature"
        commit id: "2R"
        commit id: "3R"
        commit id: "4R"
        checkout "main"
        merge "feature"  type: HIGHLIGHT

Rebase instead of merge

Take the habit of rebasing rather than merging. A graph that does not showcase concurrent progress is easier to maintain and navigate. Rebasing effectively transforms the graph to ressemble a history where every feature was developed instantly, without concurrency to the main branch.