Introduction Segment:
A well-paced introduction to most of the Git commands
Git Commit Introduction
Git Branching
Git Merging
Introduction to Git Rebase
Getting Up to Speed
The following dose of pure Git awesomeness. Hope you have an appetite.
Detaching Your HEAD
Relative References (^)
Relative References #2 (~)
Reversing Changes in Git
Shuffling Work Around
Getting cozy with modifying the source tree using Git
Introduction to Cherry-picking
Introduction to Interactive Rebase
A Variety Pack
A collection of diverse Git techniques, hacks, and suggestions
Selectively Grabbing a Single Commit
Manipulating Commits
Manipulating Commits #2
Git Tags
Git Describe
Advanced Subjects
For the exceptionally daring!
Rebasing over 9000 times
Multiple Parents
Branch Spaghetti
Introduction Segment:
Git Commit Introduction
A commit in a git repository captures a snapshot of all the tracked files in your directory. It functions like a significant copy and paste operation, but with even more advantages!
However, Git aims to keep commits as lightweight as possible. Therefore, instead of blindly duplicating the entire directory every time you commit, Git can compress a commit as a set of changes, known as a "delta," representing the transition from one version of the repository to the next.
In addition, Git maintains a chronological history of when commits were made. This is why most commits have preceding ancestor commits, visually depicted with arrows in our visualization. Preserving this history is highly beneficial for all project collaborators.
It may seem overwhelming, but for now, you can perceive commits as snapshots of the project. Commits possess remarkable efficiency, and transitioning between them is incredibly swift!
Let's examine this concept in action. On the bottom side, we have a visual representation of a (small) git repository. Currently, there are two commits
present. The first commit, labeled as C0, represents the initial commit, while the second commit, labeled as C1, possibly contains some significant modifications.
Made a new commit to the repository with
git commit -m "C2"
Great! That's fantastic progress. We have successfully made changes to the repository and saved them as a commit. The commit we just created, let's call it "C2," now has a parent commit, C1, which indicates the commit it was built upon or derived from. This parent-child relationship helps maintain a clear lineage of commits within the repository's history.
Git Branching
Branches in Git are extremely lightweight. They act as pointers to specific commits and nothing more. That's why Git enthusiasts often emphasize the mantra:
"Branch early, and branch often."
Since creating branches doesn't consume extra storage or memory, it's easier to logically organize your work into smaller, manageable units rather than having large, cumbersome branches.
As we delve into working with branches and commits together, we'll witness how these two features complement each other. But for now, remember that a branch essentially signifies, "I want to include the changes from this commit and all its preceding commits."
Let's see what branches look like in practice.
$ git branch newImage
Here we will create a new branch named
newImage
.And just like that, branching is as simple as that! The branch named "newImage" now points to commit C1.
Let's attempt to make some progress on this new branch.
git commit -m "C2"
Uh-oh! The main branch has moved forward, but the
newImage
branch hasn't! This happened because we weren't "on" the new branch, which is why the asterisk (*) indicated the main branch as the active one.To switch to the desired branch before committing our changes, we can use the command:
$ git checkout <branch_name>
By executing this command, we inform Git that we want to switch to the specified branch (in this case, the "
newImage
" branch) and make it our current working branch. This ensures that our subsequent commits will be associated with the correct branch.$ git checkout newImage
$ git commit -m "C2"
Perfect! By checking out the new branch before committing our changes, we have successfully recorded our modifications on the new branch. Well done!
Branches and Merging
Fantastic! We have now learned how to commit and branch in Git. The next important concept we need to explore is combining the work from different branches. This capability allows us to branch off, develop new features independently, and later merge them back together.
The first method we will discuss for combining work is called "git merge." In Git, merging involves creating a special commit that has two distinct parents. This type of commit signifies that we want to incorporate all the changes from one parent branch and another parent branch, including their respective ancestry.
To better understand this concept visually, let's proceed to the next view where we'll examine a practical example. This will help elucidate the merging process for beginners.
In the current scenario, we have two branches, each containing a unique commit. This implies that neither branch incorporates the complete set of changes made in the repository so far. To address this, we can utilize the merge operation.
Our goal is to merge the "bugfix" branch into the "main" branch. This merging process will combine the changes from the "bugfix" branch into the "main" branch, ensuring that the final result includes all the modifications made in both branches.
$ git merge bugFix
Indeed, there are some remarkable observations here! Firstly, the "main" branch now points to a commit that has two parents. If you follow the arrows upwards from the "main" branch along the commit tree, you will traverse through every commit leading back to the root. This indicates that the "main" branch now encompasses all the work in the repository.
Additionally, you may notice that the colors of the commits have changed. As a helpful visual aid for learning, each branch has a unique color assigned to it. Furthermore, each commit takes on a color that represents a blend of all the branches that contain that particular commit.
In the current scenario, we can see that the "main" branch color is blended into all the commits, while the "bugFix" color is not. To rectify this and include the "bugFix" color into the commits, let's proceed with the necessary steps.
Let's merge the changes from the "main" branch into the "bugFix" branch.
$ git checkout bugFix
$ git merge main
Since the "bugFix" branch was already an ancestor of the "main" branch, Git didn't have to perform any actual merging work. It simply moved the "bugFix" branch pointer to the same commit where the "main" branch was pointing.
As a result, all the commits now share the same color, indicating that each branch now incorporates the entirety of the work in the repository. This is definitely a cause for celebration! Well done!
4. Introduction to Git Rebase
Another method of combining work between branches is through rebasing. Rebasing involves taking a series of commits, essentially "copying" them, and placing them in a different location.
Although this may initially seem perplexing, the benefit of rebasing is that it can create a clean and linear sequence of commits. By exclusively using rebasing, the commit log and history of the repository can be maintained in a more organized manner.
Now, let's witness the process in action to gain a better understanding.
In the current scenario, we once again have two branches, with the "bugFix" branch currently selected (indicated by the asterisk).
Our objective is to incorporate the work from the "bugFix" branch onto the work from the "main" branch. This way, it would create an illusion that these two features were developed sequentially, even though they were actually developed in parallel.
To achieve this, we can use the git rebase
command. This command will help us reapply the commits from the "bugFix" branch onto the latest commit of the "main" branch. By doing so, we can create a more linear and cohesive sequence of commits.
$ git rebase main
That's great to hear! Now the work from our "bugFix" branch is seamlessly integrated on top of the "main" branch, resulting in a clean and linear sequence of commits.
Please note that the original commit C3 still exists in the repository (although it appears faded in the commit tree), and C3' represents the "copy" of the commit that we rebased onto the "main" branch.
However, you rightly pointed out that the "main" branch hasn't been updated to reflect the latest changes. To address this, let's update the "main" branch accordingly.
Now we are checked out on the main branch. Let's go ahead and rebase onto bugfix...
$ git rebase bugFix
Great! Since the "main" branch was an ancestor of the "bugFix" branch, Git simply moved the reference of the "main" branch forward in the commit history. This action effectively incorporates the commits from the "bugFix" branch onto the updated "main" branch, resulting in a more streamlined commit history.
Moving around in Git:
Before delving into the more advanced features of Git, it's crucial to grasp the various methods available for navigating through the commit tree that represents your project.
Becoming proficient in moving around the commit tree will greatly enhance your capabilities with other Git commands and operations. It enables you to efficiently explore different branches, commits, and versions of your project, facilitating seamless collaboration and development.
HEAD
HEAD is a critical concept in Git that represents the currently checked out commit in your repository. It serves as a symbolic name for the commit you are actively working on.
In most cases, HEAD points to a branch name, such as "bugFix." This means that the branch you have checked out is the one referenced by HEAD. When you make a commit, the status of the branch (e.g., "bugFix") is updated, and this change is reflected through HEAD.
As you continue to make changes to the working tree, HEAD will be updated accordingly to reflect the most recent commit. It essentially acts as a pointer to the tip of the current branch, providing a convenient way to keep track of your progress and navigate through different stages of your project's development.
Let's see this in action. Here we will reveal HEAD before and after a commit.
$ git checkout C1
$ git checkout main
$ git commit -m "C2"
$ git checkout C2
Indeed! In the visualization, we can observe that HEAD was metaphorically hiding underneath the "main" branch. This implies that the currently checked out commit, represented by HEAD, was aligned with the latest commit on the "main" branch.
By revealing HEAD, we can clearly see its position within the commit tree. It serves as a reference point for the commit you're currently working on, allowing you to easily track your progress and navigate through different branches and commits.
Understanding the role of HEAD and its relationship with the active branch is crucial for effectively managing your project and maintaining a clear understanding of your current working state.
Detaching HEAD
Detaching HEAD refers to the act of attaching it directly to a specific commit instead of a branch. Let's examine the state before detaching HEAD:
Before detaching, HEAD is typically pointing to a branch, such as "main," which in turn points to a specific commit, denoted as C1. This configuration allows for easy navigation and management of the project's history through the branch.
However, when we detach HEAD, it will be directly attached to a commit, breaking the link to any branch. This state of detached HEAD has its own implications and usage, which we will explore further.
HEAD -> main -> C1
$ git checkout C1
And now it's
HEAD -> C1
Relative Refs
When navigating through the commit history in Git, specifying commit hashes can become tedious, especially when working without a visual representation of the commit tree. In real-world scenarios, commit hashes tend to be longer and less memorable.
However, Git offers a convenient solution known as "Relative Refs" to simplify moving around without relying solely on commit hashes. Relative Refs allow you to reference commits using relative positions and relationships instead of their complete hashes.
For example, instead of typing the entire lengthy hash like xea3da64c0efc6293610ded892f82a58e8cbc5d8, you can use a shorter, unique identifier such as "fed2" to identify the commit. Git is intelligent enough to recognize the unique identifier based on the available commits in the repository.
Using Relative Refs reduces the burden of remembering and typing lengthy commit hashes, providing a more intuitive and efficient way to navigate the commit history, even in real-world Git scenarios where the commit tree visualization may not be readily available.
Git's relative refs, indeed, offer a more convenient way to specify commits without relying on their complete hash. They enable you to start from a memorable reference point, such as a branch like "bugFix" or the HEAD, and navigate from there.
Two commonly used relative refs provide flexibility in moving through commit history:
Moving upwards one commit at a time with
^
: By appending the caret symbol^
to a reference, you can access the parent commit. For example,bugFix^
refers to the parent commit of the "bugFix" branch.Moving upwards a specific number of times with
~<num>
: Using the tilde symbol~
followed by a number allows you to move up the commit history by the specified number of times. For instance,HEAD~3
represents the commit three steps above the current HEAD.
By leveraging these relative refs, you can navigate through the commit tree in a more intuitive manner, making it easier to track changes, compare versions, and perform various Git operations.
Let's examine the commit history and access the commit above the "main" branch using the caret (^) operator. This operator allows us to reference the parent commit of a specified commit.
To check out the commit above "main," you can use the following command:
$ git checkout main^
Great job! That was much simpler than having to type the entire commit hash. Using the caret (^) operator allows you to effortlessly navigate to the parent commit of a given reference.
You can also reference HEAD
as a relative ref. Let's use that a couple of times to move upwards in the commit tree.
$ git checkout C3
$ git checkout HEAD^ $ git checkout HEAD^
$ git checkout HEAD^
Easy! We can travel backwards in time with HEAD^
The "~" operator
Say you want to move a lot of levels up in the commit tree. It might be tedious to type ^ several times, so Git also has the tilde (~) operator.
The tilde operator (optionally) takes in a trailing number that specifies the number of parents you would like to ascend. Let's see it in action.
To use the tilde operator, simply append it with a number indicating the desired level of ascent. Let's see an example:
$ git checkout HEAD~4
Boom! So concise -- relative refs are great.
Branch forcing
Now that you're familiar with relative refs, let's put them to use in a practical scenario. One common application is to move branches around using the -f
option.
To move a branch directly to a specific commit, you can use the -f
option followed by the branch name and the desired relative ref. For example, the command:
$ git branch -f main HEAD~3
will forcefully move the main
branch to three parents behind the current commit pointed to by HEAD
.
This operation can be useful when you need to adjust branch references, align them with specific commits, or reorganize the commit history. However, please exercise caution when using branch forcing, as it can overwrite and discard existing commits if not used carefully.
Let's see that previous command in action.
$ git branch -f main HEAD~3
Perfect! With the help of relative references, we now have a succinct method of referencing C1, and by utilizing branch forcing (-f), we have efficiently relocated a branch to that specific position.
Reversing Changes in Git
There are many ways to reverse changes in Git. And just like committing, reversing changes in Git has both a low-level component (staging individual files or chunks) and a high-level component (how the changes are actually reversed). Our application will focus on the latter.
There are two primary ways to undo changes in Git -- one is using
git reset
and the other is usinggit revert
. We will look at each of these in the next dialog
Git Reset
When it comes to reversing changes in Git, the git reset
command is a powerful tool. It allows you to move a branch reference backward in time to an earlier commit, effectively undoing the changes made in subsequent commits. It can be thought of as "rewriting history" since it alters the commit timeline as if the specified commit never existed.
Let's take a closer look at how git reset
works:
$ git reset HEAD~1
Nice! Git moved the main branch reference back to C1
; now our local repository is in a state as if C2
had never happened.
Git Revert
When it comes to reverting changes in Git, the git revert
command provides a safe and collaborative approach. Unlike git reset
, which modifies the commit history and can cause issues with shared branches, git revert
creates a new commit that undoes the changes made in a previous commit. This allows you to reverse changes and share those reversed changes with others.
Here's how git revert
works:
$ git revert HEAD
Weird, a new commit plopped down below the commit we wanted to reverse. That's because this new commit C2'
introduces changes -- it just happens to introduce changes that exactly reverse the commit of C2
.
With reverting, you can push out your changes to share with others.
Part -II will be Coming Soon...
Like and Subscribe for more blogs