A note on my learning journey with Git from this great source https://learngitbranching.js.org/

Basics of Git

These following sections are the basics of Git that is enough for a majority of all workflows. It includes concepts like components of a working tree (commit, branch, HEAD), branching and merging, and how to reverse changes in Git.

Git Commits

Git commits: snapshots of all the tracked files in your working directory.

  • Git can (when possible) compress a commit as a set of changes, or a “delta”, from one commit to the next.
  • Git can maintain a history of these changes.
  • Git can synchronize changes across multiple people.

Demonstration:

git init
echo "Hello World" > file.txt
git add file.txt
git commit -m "Initial commit"
echo "How are you?" >> file.txt
git commit -am "Second commit"

Git Branches

Branches in Git are incredibly lightweight as they are just pointers to a specific commit. A branch essentially says: “I want to include the work of this commit and all parent commits”.

Demonstration:

git branch bugFix # create a new branch called bugFix
git checkout bugFix # switch to the bugFix branch
echo "Bug fixed" >> file.txt # make changes to the file
git commit -am "Bug fixed" # commit the changes in the bugFix branch
git checkout main # switch to the main branch
echo "New feature" >> file.txt
git commit -am "New feature"

In the above example, bugFix and main are two branches. bugFix branch has a commit that fixes a bug, while main branch has a commit that adds a new feature.

Demonstration 2:

git init
echo "Hello World" > file.txt
git add file.txt
git commit -m "Initial commit"
git branch bugFix
echo "Bug fixed" >> file.txt
git commit -am "Bug fixed"
  • git checkout bugFix: switch to the bugFix branch
  • git checkout -b bugFix: create a new branch called bugFix and switch to it
  • git checkout main: switch to the main branch

Braches and Merging

Merging in Git creates a special commit that has two unique parents. A merge commit is necessary because a repository can have multiple branches with different commits. Essentially, a merge commit is to unify the changes of two branches.

In the above example, C4 is a merge commit of C2 - branch bugFix and C3 - branch main. Therefore, C4 has two parents: C2 and C3, which means C4 contains all the changes/works of both C2 and C3. Moreover, in the above example, we merge bugFix into main branch (illustrated by the same color). However, we can also merge main into bugFix branch.

Demonstration:

git init
git commit -m "C1"
git checkout -b bugFix; git commit -am "C2" # create a new branch bugFix and switch to it and then commit
git checkout main; git commit -am "C3" # switch to main and commit
git merge bugFix # merge bugFix into main

Rebase

Rebasing is the process of moving or combining a sequence of commits to a new base commit. Rebasing essentially takes a set of commits, “copies” them, and plops them down somewhere else. The advantage of rebasing is that it can be used to make a nice linear sequence of commits.

  • git rebase <branchA> <branchB>: rebase branchA onto branchB. This means that all the commits in branchA will be “copied” and “pasted” onto branchB. HEAD will be at the tip of branchA after the rebase.
  • git rebase <branch>: rebase the current branch onto branch. This is equivalent to git rebase HEAD <branch>.
Left: Before rebasing. Middle: After rebasing. The `bugFix` branch is rebased onto the `main` branch. Right: `git rebase bugFix`.

Demonstration:

git init
git commit -m "C1"
git branch bugFix
git checkout bugFix
git commit -m "C2"
git checkout main
git commit -m "C3"
git checkout bugFix
git rebase main # rebase bugFix onto main

HEAD is the symbolic name for the currently checked out commit. It is essentially the “current branch”. When you switch branches with git checkout, the HEAD revision changes to point to the tip of the new branch.

Detaching HEAD just means attaching it to a commit instead of a branch. For example, git checkout C1 will detach HEAD and attach it to commit C1.

Demonstration:

git init
git commit -m "C1" # HEAD -> main -> C1
git branch bugFix # HEAD -> main -> C1
git checkout bugFix # HEAD -> bugFix -> C1
git commit -m "C2" # HEAD -> bugFix -> C2
git checkout main # HEAD -> main -> C1
git commit -m "C3" # HEAD -> main -> C3
git merge bugFix # HEAD -> main -> C3 -> C2
git commit -m "C4" # HEAD -> main -> C4 -> C3 -> C2
git checkout C4 # HEAD -> C4

Relative Refs

Relative refs are another powerful way to specify commits. They are almost exclusively used for moving around in history and are great for “walking back in time”.

  • ^: indicates the parent commit. ^^ indicates the grandparent commit.
  • ~: indicates the first parent commit. ~2 indicates the first parent’s parent commit. ~n indicates the nth parent commit.
  • ^2: indicates the second parent commit. This is useful for merge commits where one commit has two parents.
  • You can also use HEAD as a relative ref. For example, HEAD^ is the parent commit of the current commit.
Illustration of before/after branching using relative refs. Left: start. Middle: `git branch -f main C6`. Right: `git checkout HEAD~1`. Far right: `git branch -f bugFix HEAD~1`

Demonstration:

git branch -f main C6 # move the main branch to commit C6
git checkout HEAD~1 # move HEAD to the parent commit of the current commit
git branch -f bugFix HEAD~1 # move the bugFix branch to the parent commit of the current commit

In the above example, git branch -f source des_commit forces the source branch to point to a specific destination commit des_commit, while git checkout HEAD~1 moves HEAD to the parent commit of the current commit.

Comparing to git reset, git branch -f is used to move branches around, while git reset is used to move HEAD and the current branch around.

Advanced: We can also combine relative refs such as HEAD~^2~2 which moves HEAD to the grandparent of the second parent of the parent of the parent of the current commit as shown in the illustration below.

Illustration of combining relative refs. Left: start. Right: `git checkout HEAD~^2~2` moves HEAD to the grandparent of the second parent of the parent of the parent of the current commit.

Reversing Changes in Git

There are many ways to reverse changes in Git. Low-level approach like staging individual files or high-level approach like git revert or git reset.

  • git reset reverts changes by moving a branch reference backward in time to an older commit. In this way, it can be used to “undo” commits or “rewriting history”. All commits after the reset commit will be lost.
  • git revert creates a new commit that reverts the changes of a previous commit. It is a safe way to undo and can be shared with others unlike git reset.
Illustration of reverting changes. Left: starting point. There are three branches: main, pushed, and local. The current HEAD is at local branch and commit C3. Middle: `git reset HEAD~1`. Right: `git checkout pushed; git revert HEAD`.

Demonstration: Given the current working directory, reverse the most recent commit on both local and pushed (one per branch).

git reset HEAD~1 # reverse the most recent commit on the current branch
git checkout pushed # switch to the pushed branch
git revert HEAD # reverse the most recent commit on the pushed branch

Moving Work Around

This section covers more advanced topics in Git such as cherry-picking, interactive rebase, and tags.

Cherry-pick

Cherry-picking in Git means to choose a specific commit from one branch and apply it onto another. This is in contrast with other ways such as merge or rebase which apply many commits onto another branch.

For example, git cherry-pick C3 C4 will apply the changes of commits C3 and C4 onto the current branch.

Interactive Rebase

Interactive rebase is a powerful tool to re-write history. It allows you to change the order of commits, combine multiple commits into one, or even delete commits.

Situations where interactive rebase is useful:

  • Cleaning up a messy history before merging a feature branch.
  • Squashing commits together to make them more readable.
  • Splitting a commit into smaller commits.
  • Removing a commit that introduced a bug.
  • Reordering commits to make them more logical.
  • Changing commit messages.

Practical scenario: You have some changes (newImage) and another set of changes (caption) that are related, so they are stacked on top of each other in your repository. And you need to make a small modification to an earlier commit (newImage).

There are several approaches to solve this problem:

  • Approach 1: Use git rebase -i to reorder the commits and then use git commit --amend to modify the commit. Then use git rebase -i to reorder the commits back to the original order.
  • Approach 2: Use git cherry-pick to apply the changes of the commit to a new commit. Then use git rebase -i to delete the original commit and reorder the new commit to the original position.
Illustration of interactive rebase. Left: starting point. There are three branches: main, newImage, and caption*. The current HEAD is at caption branch and commit C3. Right: The goal is to modify the commit C3 twice and the commit C2 three times, then reorder the commits back to the original order (C1, C2''', C3'') on the main branch.

Demonstration: Given the current working directory, modify the commit C3 twice and the commit C2 three times, then reorder the commits back to the original order (C1, C2’’’, C3’’) on the main branch.

git rebase -i HEAD~2 # rebasing the last two commits. Assuming the modifications are made. Reorder the commits as HEAD -> C2' -> C3' -> C1
git rebase -i HEAD~1 # rebasing the last commit. Assuming the modifications are made. HEAD -> C2'' -> C3' -> C1
git rebase -i HEAD~2 # reorder the commits back to the original order. HEAD -> C3'' -> C2''' -> C1
git checkout main # switch to the main branch
git merge caption # merge the caption branch into the main branch, fast-forward
Illustration of interactive rebase. From left to right. 1st: starting point. 2nd: `git rebase -i HEAD~2`. 3rd: `git rebase -i HEAD~1`. 4th: `git rebase -i HEAD~2`. 5th: `git checkout main; git merge caption`.

Example with git cherry-pick:

Illustration of interactive rebase. Left: starting point. There are three branches: main, newImage, and caption*. The current HEAD is at caption branch and commit C3. Right: The goal is to modify the commit C3 one and the commit C2 twice, then reorder the commits back to the original order (C1, C2'', C3') on the main branch.
git checkout C1
git cherry-pick C2 # apply the changes of commit C2 onto the current branch
git checkout main # switch to the main branch
git cherry-pick C2^ # apply the changes of commit C2' onto the main branch
git cherry-pick C3 # apply the changes of commit C3 onto the main branch

Git Tags

Git tags are a way to mark specific points in history as being important. They are often used to mark release points (v1.0, and so on).

  • git tag <tagname>: create a new tag
  • git tag <tagname> <commit>: create a new tag that points to a specific commit
  • git tag -a <tagname> -m "message": create an annotated tag
  • git tag -d <tagname>: delete a tag
git tag v1 side~1 # create a tag v1 that points to the commit that is one before recent commit of the branch `side`
git tag v0 main~2 # create a tag v0 that points to the commit that is two before recent commit of the branch `main`

Git Describe

Git describe is a way to get a human-readable description of a commit. It is often used to provide a more user-friendly way to identify a commit.

git describe <ref>: describe the most recent commit reachable from the ref. If the ref is omitted, it defaults to HEAD.

The output of git describe is in the format <tag>_<numCommits>_g<hash>. For example, v1_2_ga6f4 means that the most recent tag is v1, there are 2 commits since the tag, and the hash of the commit being described is a6f4.

Illustration of git describe. `git describe main` will output `v1_2_gC2` because the most recent tag is `v1`, there are 2 commits since the tag, and the hash of the commit being described is `C2`. `git describe side` will output `v2_1_gC4` because the most recent tag is `v2`, there is 1 commit since the tag, and the hash of the commit being described is `C4`.

Exercises

Example 1: Given the current working history, arrange the commits as shown in the goal.

Solution:

git rebase main bugFix # rebase bugFix onto main. HEAD -> bugFix
git rebase bugFix side # rebase side onto bugFix, HEAD -> side
git rebase side another # rebase another onto side, HEAD -> another
git rebase another main # rebase main onto another, HEAD -> main
From left to right. 1st: starting point. 2nd: `git rebase main bugFix`. 3rd: `git rebase bugFix side`. 4th: `git rebase side another`. 5th: `git rebase another main`.

Example 2: given the current working history, create a new branch bugWork at the specified destination as shown in the goal. It can simply be done by git branch bugWork C2. However, the goal is to use relative refs to specify the destination commit.

Solution:

git branch bugWork HEAD~^2~ # Current HEAD at C7. HEAD~ is C6. HEAD~^2 is the second parent of C6 which is C5. HEAD~^2~ is the parent of C5 which is C2. 

Example 3: given the current working history, where the main branch is some commits ahead of the three branches one, two, and three. The goal is to modify the three branches as shown in the goal.

Analysis: The simplest task is for the branch three where it is just move forward one commit to the commit C2. The branch one can be done by cherry-pick or rebase while branch two is the copy of branch one with an additional commit C5.

Solution:

git checkout three; git reset C2 # move the three branch to the commit C2
git checkout one; git cherry-pick C4 C3 C2 # apply the changes of commit C4, C3, and C2 onto the one branch
git checkout two; git cherry-pick C5; # apply the changes of commit C5 onto the two branch
git cherry-pick C4^ C3^ C2^ # apply the changes of the parent commit of C4, C3, and C2 onto the two branch
  • git checkout three; git reset C2: move the branch three to the commit C2. Alternatively, git branch -f three C2 can be used.

Working Remotely

This section covers the basics of working with remote repositories in Git. It includes concepts like cloning, fetching, pulling, pushing, and remotes.