Advanced Git Commit Operations

GitGitBeginner
Practice Now

Introduction

Welcome back, Git adventurer! You've taken your first steps into the world of version control, and now it's time to level up your skills. In this lab, we're going to explore some of Git's more advanced commit operations. These techniques will give you even more control over your project's history, allowing you to clean up mistakes, reorganize your work, and collaborate more effectively.

Think of this lab as upgrading your time machine. Not only can you travel through time, but now you'll learn how to alter the timeline itself! Don't worry if this sounds daunting - we'll guide you through each step, explaining not just how to do these operations, but why they're useful in real-world scenarios.

By the end of this lab, you'll be able to amend commits, revert changes, cherry-pick specific commits, perform interactive rebasing, and squash commits. These are powerful tools that professional developers use every day to maintain clean, organized project histories. Let's dive in and take your Git skills to the next level!


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL git(("`Git`")) -.-> git/SetupandConfigGroup(["`Setup and Config`"]) git(("`Git`")) -.-> git/BasicOperationsGroup(["`Basic Operations`"]) git(("`Git`")) -.-> git/DataManagementGroup(["`Data Management`"]) git(("`Git`")) -.-> git/BranchManagementGroup(["`Branch Management`"]) git/SetupandConfigGroup -.-> git/init("`Initialize Repo`") git/BasicOperationsGroup -.-> git/add("`Stage Files`") git/BasicOperationsGroup -.-> git/commit("`Create Commit`") git/DataManagementGroup -.-> git/reset("`Undo Changes`") git/BranchManagementGroup -.-> git/branch("`Handle Branches`") git/BranchManagementGroup -.-> git/checkout("`Switch Branches`") git/BranchManagementGroup -.-> git/log("`Show Commits`") git/BranchManagementGroup -.-> git/cherry_pick("`Cherry Pick`") git/BranchManagementGroup -.-> git/rebase("`Reapply Commits`") subgraph Lab Skills git/init -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/add -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/commit -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/reset -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/branch -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/checkout -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/log -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/cherry_pick -.-> lab-387471{{"`Advanced Git Commit Operations`"}} git/rebase -.-> lab-387471{{"`Advanced Git Commit Operations`"}} end

Setting Up Your Workspace

Before we start our advanced operations, let's set up a new workspace. We'll create a new directory and initialize a Git repository in it. This will give us a clean space to experiment without affecting any of your existing projects.

Open your terminal and type these commands, pressing Enter after each line:

cd ~/project
mkdir git-advanced-lab
cd git-advanced-lab
git init

Let's break down what these commands do step by step:

  1. cd ~/project: cd stands for "change directory". ~/project is a path that usually points to a "project" folder within your home directory (~). This command navigates your terminal into that directory. If the "project" directory doesn't exist in your home directory, you might encounter an error. If so, create it first using mkdir ~/project and then try cd ~/project again.
  2. mkdir git-advanced-lab: mkdir stands for "make directory". This command creates a new folder named "git-advanced-lab" inside your current directory (which should be ~/project after the previous command). This new directory will be the root of our Git repository for this lab.
  3. cd git-advanced-lab: This command changes your current directory again, this time moving you inside the newly created "git-advanced-lab" directory. Now, any commands you run will be executed within this directory.
  4. git init: This is the crucial Git command for initializing a repository. git init sets up all the necessary Git structures inside the current directory (git-advanced-lab), turning it into a Git repository. You'll see a hidden folder named .git created inside git-advanced-lab. This .git folder is the heart of your Git repository and stores all the version history and configuration.

Now that we have a Git repository initialized, let's create a simple file to work with and make our initial commit:

echo "Hello, Advanced Git" > hello.txt
git add hello.txt
git commit -m "Initial commit"

Here's what's happening in these commands:

  1. echo "Hello, Advanced Git" > hello.txt: echo is a command that displays text. "Hello, Advanced Git" is the text we want to display. > is a redirection operator. It takes the output of the echo command and redirects it to a file named "hello.txt". If "hello.txt" doesn't exist, it will be created. If it does exist, it will be overwritten with the new content. So, this command creates a new file named "hello.txt" with the content "Hello, Advanced Git".
  2. git add hello.txt: Before Git can track changes in a file, you need to explicitly tell Git to start tracking it. git add hello.txt stages the "hello.txt" file. Staging means preparing the file to be included in the next commit. Think of the staging area as a preparation zone for your commit.
  3. git commit -m "Initial commit": git commit takes all the changes that are currently staged (in our case, the addition of "hello.txt") and saves them as a new commit in the repository's history. -m "Initial commit" adds a commit message. "Initial commit" is the message itself, which should be a short description of the changes you're committing. It's good practice to write clear and concise commit messages.

Great! We now have a repository with one commit. Let's check our status to confirm everything is set up correctly:

git status

After running git status, you should see a message in your terminal that looks something like this:

On branch master
nothing to commit, working tree clean

This message indicates that your working tree is clean. "Working tree" refers to the directory where your project files are located. "Clean" means there are no changes in your working directory that are not yet committed. This confirms that we're ready to start our advanced operations with a clean slate!

Amending Your Last Commit

Imagine you've just made a commit, but then you realize you forgot to include a change in a file, or you made a small typo in your commit message. Instead of creating a whole new commit for such a minor fix, Git allows you to fix the most recent commit using the --amend option. This is like going back in time slightly to adjust your last action.

Let's try it out. First, let's modify our hello.txt file by adding another line:

echo "This is an important file." >> hello.txt

This command appends a new line to our "hello.txt" file. Let's understand the >> operator:

  • > would overwrite the entire file with the new content.
  • >> appends the new content to the end of the existing file, preserving the original content.

So, after running this command, "hello.txt" will contain two lines:

Hello, Advanced Git
This is an important file.

Now, let's say we realized we should have included this "important file" note in our initial commit itself. We can amend our previous commit to include this change and update the commit message to reflect the addition.

git add hello.txt
git commit --amend -m "Initial commit with important note"

Here's a breakdown of these commands:

  1. git add hello.txt: We've modified hello.txt, so we need to stage the changes again using git add. This tells Git that we want to include the current state of "hello.txt" in our commit.
  2. git commit --amend -m "Initial commit with important note": This is where the magic happens.
    • git commit --amend: The --amend flag tells Git that instead of creating a new commit, we want to modify the last commit.
    • -m "Initial commit with important note": This provides a new commit message. If you omit the -m option, Git will open your default text editor, allowing you to edit the original commit message.

What just happened?

Instead of creating a new commit in your Git history, Git has effectively replaced the previous "Initial commit" with a new, improved version. The old commit is gone, and the new commit incorporates the changes we staged and the updated commit message.

Let's verify this by checking our commit log:

git log --oneline

You should see only one commit in the output, but the commit message should now be "Initial commit with important note".

<commit_hash> Initial commit with important note

Press q to exit the log view if it doesn't automatically close.

Important Considerations when Amending:

  • Only Amend Unpushed Commits: You should only amend commits that you haven't yet pushed to a shared remote repository (like GitHub, GitLab, etc.). Amending a commit that has already been pushed rewrites history that others might be relying on, which can lead to significant problems and confusion in collaborative environments.
  • Local History Cleanup: Amending is primarily useful for cleaning up your local commit history before sharing your work. It's a great way to refine your commits while you're still working on a feature branch, for example.

Reverting a Commit

Sometimes, you make a commit and later realize it introduced a bug or you simply want to undo those changes. You might think of using git reset to go back in time and remove the commit. However, git reset can be destructive, especially if you've already pushed your changes or if others are working on the same branch. A safer and more collaborative way to undo changes is using git revert.

git revert creates a new commit that undoes the changes introduced by a specific commit. It doesn't erase the original commit from history; instead, it adds a new commit that effectively reverses the effects of the unwanted commit. This preserves the history and is much safer for shared repositories.

Let's create a new commit that we will then revert:

echo "This line will be reverted" >> hello.txt
git add hello.txt
git commit -m "Add line to be reverted"

These commands are similar to what we've done before:

  1. echo "This line will be reverted" >> hello.txt: Adds a new line to our "hello.txt" file.
  2. git add hello.txt: Stages the modified file.
  3. git commit -m "Add line to be reverted": Creates a new commit with the message "Add line to be reverted" that includes the addition of this new line to "hello.txt".

Now, let's say we decide that adding "This line will be reverted" was a mistake, and we want to remove it. We can revert the last commit using:

git revert HEAD

Let's break down this command:

  • git revert: This is the command to revert changes.
  • HEAD: In Git, HEAD is a pointer to the current commit of your current branch. In most cases, HEAD refers to the most recent commit. So, git revert HEAD means "revert the most recent commit". You can also specify a specific commit hash to revert an older commit.

When you run git revert HEAD, Git will do the following:

  1. Analyze the changes: It looks at the changes introduced by the HEAD commit (in our case, "Add line to be reverted").
  2. Create reverse changes: It figures out how to undo those changes (in our case, removing the "This line will be reverted" line from "hello.txt").
  3. Create a new commit: It automatically creates a new commit that applies these reverse changes.
  4. Open Text Editor (Optional): Git will usually open your default text editor (like Vim, Nano, VS Code's editor, etc.) with a pre-filled commit message for the revert commit. The default message is usually something like "Revert 'Add line to be reverted'". You can accept the default message or modify it to provide more context.

Handling the Text Editor during git revert:

If your default editor opens, and you are not familiar with it (especially if it's Vim), don't worry! The process is usually simple:

  • To accept the default message and complete the revert:
    • If it's Vim: Press Esc key (to enter command mode), then type :wq (write and quit), and press Enter.
    • If it's Nano: Press Ctrl + X (to exit), then Y (to save changes), and Enter (to confirm filename).
    • For other editors: Look for options to "Save" and "Close" or "Exit".

After you save and close the editor (if it opened), Git will finalize the revert commit.

Let's check our commit history now:

git log --oneline

You should see three commits in your log, in reverse chronological order (newest first):

  1. A commit starting with "Revert" (the revert commit we just created).
  2. "Add line to be reverted" (the commit we reverted).
  3. "Initial commit with important note" (our original amended commit).
<commit_hash_revert> Revert "Add line to be reverted"
<commit_hash_original> Add line to be reverted
<commit_hash_initial> Initial commit with important note

If you look at the content of hello.txt now, the line "This line will be reverted" should be gone, effectively undoing the changes from the "Add line to be reverted" commit.

Why is git revert preferred over git reset in shared repositories?

  • Non-Destructive History: git revert doesn't rewrite history. It adds a new commit to undo changes, making it safe for collaboration. Everyone can see the original mistake and the correction in the history.
  • Avoids Force Pushing: git reset often requires force pushing (git push --force) to a remote repository if you've already pushed the commits. Force pushing can be very disruptive and overwrite others' work. git revert avoids this issue.
  • Clear Audit Trail: git revert provides a clear record of when and why changes were undone. This is valuable for understanding the project's evolution and debugging issues.

Cherry-picking Commits

Cherry-picking in Git is like picking a cherry from a tree โ€“ you select a specific commit from one branch and apply it to your current branch. This is useful when you want to incorporate a particular feature or fix from another branch without merging the entire branch.

Let's simulate a scenario where we have a feature developed in a separate branch, and we want to bring just that feature into our main branch.

First, we need to create a new branch and make a commit on it:

git checkout -b feature-branch
echo "This is a new feature" >> feature.txt
git add feature.txt
git commit -m "Add new feature"

Let's break down these commands:

  1. git checkout -b feature-branch:
    • git checkout: This command is used to switch branches or create new branches.
    • -b feature-branch: The -b flag tells git checkout to create a new branch named "feature-branch" and immediately switch to it. So, this command creates a new branch and makes it your active branch.
  2. echo "This is a new feature" >> feature.txt: Creates a new file named "feature.txt" with the content "This is a new feature" in our "feature-branch".
  3. git add feature.txt: Stages the newly created "feature.txt" file.
  4. git commit -m "Add new feature": Creates a commit on the "feature-branch" with the message "Add new feature", including the addition of "feature.txt".

Now, let's imagine we're done with this feature for now, and we want to integrate it into our main branch (master). First, we need to switch back to the master branch:

git checkout master

This command simply switches your active branch back to "master".

Now we want to apply the "Add new feature" commit from "feature-branch" to our master branch. We use git cherry-pick for this:

git cherry-pick feature-branch

Wait, why feature-branch and not a commit hash? In this simple case, feature-branch as a commit-ish will resolve to the latest commit on feature-branch. Since we only made one commit on feature-branch, this effectively cherry-picks that commit. If you had multiple commits on feature-branch and wanted to pick a specific one, you would use the commit hash of that specific commit instead. For example, you could find the commit hash using git log feature-branch --oneline and then use git cherry-pick <commit_hash>.

When you run git cherry-pick feature-branch, Git does the following:

  1. Find the commit: It identifies the latest commit on "feature-branch" (or the commit you specified by hash).
  2. Apply changes: It tries to apply the changes introduced by that commit to your current branch (master).
  3. Create a new commit: If successful (no conflicts), Git creates a new commit on your master branch that has the same changes as the cherry-picked commit and usually the same commit message.

Let's check our commit log on the master branch:

git log --oneline

You should see a new commit on your master branch with the message "Add new feature". Importantly, this is a new commit with a different commit hash than the commit on feature-branch, even though the changes are the same.

Also, if you list the files in your master branch (e.g., using ls command), you should see the new file "feature.txt" there as well.

Important Considerations for Cherry-picking:

  • New Commit: Cherry-pick always creates a new commit. It doesn't move or copy the original commit. This is important to remember when thinking about commit history.
  • Potential Conflicts: Cherry-picking can sometimes lead to conflicts, especially if the codebases of the two branches have diverged significantly. If conflicts occur, Git will pause the cherry-pick process and ask you to resolve them, similar to merge conflicts.
  • Use Sparingly: While powerful, cherry-picking should be used judiciously. Overusing cherry-pick can make your history harder to follow, especially if you later merge the entire feature branch. It's most effective for applying specific, isolated changes or fixes from one branch to another when a full merge is not desired or appropriate.

Interactive Rebasing

Interactive rebasing is one of Git's most powerful, and potentially complex, features. It allows you to rewrite your commit history in very flexible ways. You can reorder commits, combine (squash) commits, edit commit messages, or even remove commits entirely. It's like having a fine-grained editor for your commit history.

Warning: Interactive rebasing, especially on shared branches, can be risky because it rewrites history. Never rebase commits that have already been pushed to a shared repository unless you fully understand the implications and have coordinated with your team. Rebasing is generally safer and more appropriate for cleaning up your local commits before sharing them.

Let's create a series of commits to work with so we can see interactive rebasing in action:

echo "First change" >> hello.txt
git commit -am "First change"
echo "Second change" >> hello.txt
git commit -am "Second change"
echo "Third change" >> hello.txt
git commit -am "Third change"

These commands create three new commits in quick succession, each adding a line to our "hello.txt" file. The -am shorthand in git commit -am combines staging all modified and deleted files (-a) with committing (-m). It's a convenient shortcut when you've already been tracking files and just want to commit changes to them.

Now, let's say we want to clean up these three commits. Maybe "First change" and "Second change" were actually part of the same logical change, and we want to combine them into a single commit. And maybe we want to improve the commit message for "Third change". Interactive rebasing is perfect for this.

Start an interactive rebase session using:

git rebase -i HEAD~3

Let's understand this command:

  • git rebase -i: git rebase is the command for rebasing. The -i flag stands for "interactive", which tells Git we want to perform an interactive rebase.
  • HEAD~3: This specifies the range of commits we want to rebase. HEAD refers to the current commit. ~3 means "go back three commits from HEAD". So, HEAD~3 selects the last three commits. You could also specify a commit hash instead of HEAD~3 to start the rebase from a specific point in history.

When you run this command, Git will open your default text editor with a list of the last three commits. The editor interface is where you instruct Git how to modify your history. You'll see something like this (the commit hashes will be different):

1 pick 63c95db First change
2 pick 68e7909 Second change
3 pick 5371424 Third change
4
5 ## Rebase 3bf348d..5371424 onto 3bf348d (3 commands)
6 ## 7 ## Commands:
8 ## p, pick <commit> = use commit
9 ## r, reword <commit> = use commit, but edit the commit message
10 ## e, edit <commit> = use commit, but stop for amending
11 ## s, squash <commit> = use commit, but meld into previous commit
12 ## f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
13 ##                    commit's log message, unless -C is used, in which case
14 ##                    keep only this commit's message; -c is same as -C but
15 ##                    opens the editor
16 ## x, exec <command> = run command (the rest of the line) using shell
17 ## b, break = stop here (continue rebase later with 'git rebase --continue')
18 ## d, drop <commit> = remove commit
19 ## l, label <label> = label current HEAD with a name
20 ## t, reset <label> = reset HEAD to a label
21 ## m, merge [-C <commit> | -c <commit>] <label> [## <oneline>]
22 ## .       create a merge commit using the original merge commit's
23 ## .       message (or the oneline, if no original merge commit was
24 ## .       specified); use -c <commit> to reword the commit message
25 ## 26 ## These lines can be re-ordered; they are executed from top to bottom.
27 ## 28 ## If you remove a line here THAT COMMIT WILL BE LOST.
29 ## 30 ## However, if you remove everything, the rebase will be aborted.

Understanding the Interactive Rebase Editor:

  • Commit List: The top part of the file lists the commits that are part of the rebase operation. By default, they are listed in reverse chronological order (oldest at the top, newest at the bottom).
  • Commands: Below the commit list, Git provides a list of available commands that you can use to modify each commit. The most common ones are:
    • pick (or p): Use the commit as is. This is the default.
    • reword (or r): Use the commit, but allow you to change the commit message.
    • edit (or e): Use the commit, but stop the rebase process at this commit so you can make further changes (e.g., amend files, add more changes).
    • squash (or s): Combine this commit into the previous commit in the list. The commit message of this commit will be appended to the message of the previous commit (you'll get to edit the combined message later).
    • fixup (or f): Similar to squash, but discards the commit message of this commit and just uses the message of the previous commit.
    • drop (or d): Remove this commit entirely from the history.

Our Interactive Rebase Plan:

We want to:

  1. Squash "Second change" into "First change".
  2. Reword "Third change" to have a better message.

To achieve this, modify the editor file to look like this:

pick abc1234 First change
squash def5678 Second change
reword ghi9101 Third change

Do not change the commit hashes in the editor file.

Instructions for Editing in Vim (if that's your editor):

  1. Enter Insert Mode: Press the i key. This allows you to type and edit the file.
  2. Make Changes: Use your arrow keys to navigate to the lines and change "pick" to "squash" and "reword" as shown above.
  3. Exit Insert Mode: Press the Esc key.
  4. Save and Quit: Type :wq and press Enter.

After you save and close the editor file, Git will start performing the rebase operations based on your instructions.

First, Squashing Commits:

Git will first process the "squash" command. It will combine the changes from "Second change" into "First change". Then, it will open your text editor again, this time to let you edit the combined commit message for the squashed commit. You'll see something like this in the editor:

## This is a combination of 2 commits.
## This is the 1st commit message:

First change

## This is the 2nd commit message:

Second change

## Please enter the commit message for your changes. Lines starting
## with '#' will be ignored, and an empty message aborts the commit.
## ## Date:      Tue Oct 24 10:30:00 2023 +0000
## ## interactive rebase in progress; onto <base_commit_hash>
## ## Last commands done (2 commands done):
##    pick abc1234 First change
##    squash def5678 Second change
## Next command to do (1 remaining command):
##    reword ghi9101 Third change
## You are currently rebasing branch 'master' on '<base_commit_hash>'.

This editor shows you the original commit messages of the commits being squashed. You can now edit this text to create a single, coherent commit message for the combined commit. For example, you could change it to:

Combined first and second changes: Initial setup of hello.txt

After editing the message, save and close the editor.

Next, Rewording a Commit:

Git will then move on to the "reword" command for "Third change". It will open the editor yet again, this time to let you edit the commit message for "Third change". You'll see the original message:

Third change

## Please enter the commit message for your changes. Lines starting
## with '#' will be ignored, and an empty message aborts the commit.
## ## Date:      Tue Oct 24 10:35:00 2023 +0000
## ## interactive rebase in progress; onto <base_commit_hash>
## ## Last commands done (3 commands done):
##    pick abc1234 First change
##    squash def5678 Second change
##    reword ghi9101 Third change
## Next command to do (0 remaining commands):
##    (finish)
## You are currently rebasing branch 'master' on '<base_commit_hash>'.

Change the message to something more descriptive, like:

Improved third change: Added a more descriptive line to hello.txt

Save and close the editor.

Rebase Completion:

After processing all the commands in your interactive rebase instructions, Git will finish the rebase process. You'll usually see a message like "Successfully rebased and updated refs/heads/master."

Verifying the Rebased History:

Now, check your commit log again:

git log --oneline

You should see a history that looks something like this (commit hashes will be different):

<commit_hash_third_revised> Improved third change: Added a more descriptive line to hello.txt
<commit_hash_combined> Combined first and second changes: Initial setup of hello.txt
<commit_hash_cherry_pick> Add new feature
<commit_hash_revert> Revert "Add line to be reverted"
<commit_hash_original_reverted> Add line to be reverted
<commit_hash_initial_amended> Initial commit with important note

Notice that:

  • You now have fewer commits than before (we squashed two commits into one).
  • The commit message for what was "First change" and "Second change" is now combined and more descriptive.
  • The commit message for what was "Third change" has been reworded.
  • The commit hashes of the rebased commits have changed because we rewrote history.

Important Reminders about Interactive Rebasing:

  • Local History Tool: Interactive rebase is primarily a tool for cleaning up your local commit history.
  • Avoid Rebasing Shared Branches: Do not rebase branches that have been pushed to a shared repository unless you are absolutely certain of what you are doing and have coordinated with your team. Rewriting shared history can cause serious problems for collaborators.
  • git rebase --abort: If you make a mistake during an interactive rebase, or if you get confused or encounter conflicts you can't resolve, you can always use git rebase --abort to cancel the rebase operation and return your branch to the state it was in before you started the rebase. This is a safe way to back out of a rebase if things go wrong.

Rebasing from the Root

In Step 5, we used git rebase -i HEAD~3 to rebase the last three commits. But interactive rebase can be even more powerful. git rebase -i --root allows you to interactively rebase all commits in your repository's history, starting from the very first (root) commit. This gives you the ultimate control to reshape your entire project history.

Extreme Caution: Root rebasing is an extremely powerful operation, and with great power comes great responsibility (and potential for significant problems if misused). Root rebasing on shared repositories is almost always a bad idea and should be avoided unless there is an exceptional and well-understood reason to do so, and only after careful coordination with your entire team. It completely rewrites the entire history, changing commit hashes for every commit from the root onwards. This will cause major disruptions if others are collaborating on the same repository.

For this lab, we are working in a local, isolated repository, so it's safe to experiment with root rebase to learn how it works. Just remember the warnings for real-world collaborative projects!

Let's try a root rebase:

git rebase -i --root

This command is very similar to the previous rebase, but instead of HEAD~3, we use --root. --root tells Git to consider all commits from the very beginning of the repository's history for the interactive rebase.

When you run this, your default text editor will open again, but this time it will list all commits in your repository, starting from the very first commit ("Initial commit with important note" or whatever you amended it to). You'll see a longer list now than in the previous step.

You might see something like this (your commit hashes and messages might vary based on how you've followed the previous steps):

pick <hash_initial> Initial commit with important note
pick <hash_reverted> Add line to be reverted
pick <hash_revert_commit> Revert "Add line to be reverted"
pick <hash_new_feature> Add new feature
pick <hash_combined> Combined first and second changes: Initial setup of hello.txt
pick <hash_third_revised> Improved third change: Added a more descriptive line to hello.txt

Our Root Rebase Plan:

For this exercise, let's do a couple of things with this root rebase:

  1. Change the message of the very first commit. Let's say we want to refine the message of our initial commit even further.
  2. Squash the "Revert" commit into the commit that introduced the line we reverted. This is a bit of a contrived example, but it demonstrates how you can manipulate history. We could argue that the "Revert" commit is really just part of cleaning up the initial mistake, so we could squash it into the commit that introduced the mistake. (In a real scenario, you might not always want to squash reverts, as they provide a clear record of undoing changes).

To implement this plan, modify the editor file to look like this:

reword <hash_initial> Initial commit with important note
squash <hash_reverted> Add line to be reverted
pick <hash_revert_commit> Revert "Add line to be reverted"
pick <hash_new_feature> Add new feature
pick <hash_combined> Combined first and second changes: Initial setup of hello.txt
pick <hash_third_revised> Improved third change: Added a more descriptive line to hello.txt

We changed "pick" to "reword" for the first commit and "pick" to "squash" for the second commit ("Add line to be reverted"). Leave the "Revert" commit as "pick" for now.

Save and close the editor.

First, Rewording the Initial Commit:

Git will first pause to allow you to reword the message of the initial commit. The editor will open with the current message "Initial commit with important note" (or whatever you amended it to in Step 2). Change it to something like: "Initial setup and file creation". Save and close the editor.

Next, Squashing "Add line to be reverted" into the Initial Commit:

Git will then process the "squash" command. It will combine the changes from "Add line to be reverted" into the previous commit (which is now "Initial setup and file creation"). The editor will open again for you to edit the combined commit message. You'll see the original messages of both commits. You can craft a new message that reflects the combined changes. For example, you could change the message to:

Initial setup and file creation with subsequent line removal

Save and close the editor.

Rebase Completion:

Git will continue processing the rest of the commands ("pick" for the remaining commits) and complete the root rebase.

Verifying the Root-Rebased History:

Check your commit log one last time:

git log --oneline

You should see your entire commit history, but with the changes you instructed in the root rebase:

  • The very first commit should have the new message "Initial setup and file creation with subsequent line removal" (or similar).
  • The "Add line to be reverted" commit and the "Revert" commit might still be present, or they might have been combined depending on exactly how you instructed the rebase. (In the example instructions above, we kept "Revert" as "pick", so it should still be there).
  • Commit hashes will have changed throughout the history from the root commit onwards.

Again, remember the extreme caution with root rebase, especially in shared repositories! This step is primarily for educational purposes to demonstrate the power and flexibility of interactive rebasing.

Summary

Congratulations, Git master! You've just leveled up your version control skills even further. Let's recap the powerful techniques you've learned in this lab:

  1. Amending Commits: You can now fix minor mistakes or add small changes to your most recent commit without creating a new commit, keeping your history clean for minor adjustments.
  2. Reverting Commits: You've learned how to safely undo changes by creating a new commit that reverses a previous one. This is the non-destructive way to correct mistakes in a collaborative environment.
  3. Cherry-picking: You can now apply specific commits from one branch to another, giving you fine-grained control over which changes you incorporate, perfect for selectively bringing in features or fixes.
  4. Interactive Rebasing: You've mastered the art of rewriting history, allowing you to clean up and organize your commits before sharing them with others. This includes reordering, squashing, and editing commit messages for a more polished history.
  5. Root Rebasing: You've learned how to reshape your entire project history, starting from the very first commit, giving you ultimate control over your repository's narrative. Remember the power and risk associated with this!

These advanced Git operations are incredibly powerful tools in a developer's toolkit. They allow you to maintain a clean, organized commit history, which is crucial when working on large projects or collaborating with a team. A well-maintained commit history is not just for aesthetics; it significantly improves code review, debugging, and understanding the project's evolution over time.

Other Git Tutorials you may like