EatGit - Part 5
- Preface
- Undo Commits
- Commit to Wrong Branch
- Edits on Remote
- Merge in Rebase
- Erase from History
- Summary
Preface
In Part 1, where I introduced the most common Git commands, and in Part 2, where we delved into Git internals, and Part 3, which taught us about patch files and other useful tools, or Part 4, where we completed our understanding of Git by studying remotes, branches, and how to merge or rebase them, I’ve been warning you about common mistakes and how to avoid them.
When we discussed Commits , you learned how to address the issue of accidentally creating a commit without setting up a username and email or committing with the wrong information.
After showing you how Git stores its data in the form of Objects , we saw why it’s a bad idea to change history on a branch shared with others.
We explored how to share our work independently of our branches by using Patch Files . We also learned how to fix issues where our patch files don’t apply due to mismatches between our work and others.
While learning about Force Push , we discussed why having multiple people pushing to the same branch is a bad practice. We also did some Branch Management , removing incorrect tracking branches and setting appropriate ones. During our first Rebasing , I explained how using merge to update feature branches in a rebase workflow can lead to strange behaviors.
In this section, we will learn how to recover from committing to the wrong
branch and how to handle merges in a rebase workflow. Both scenarios require
using git reset
.
Undo Commits
We already know that branches in Git are essentially pointers to specific
commits. Using the git reset
command, we can move a branch pointer to a
different commit. Let’s create a repository and add some commits to see how this
works:
mkdir repo
cd repo
git init
git config --local user.name 'Mohammad Rahimi'
git config --local user.email 'rahimi.mhmmd@outlook.com'
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
sleep 3
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
sleep 3
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
There are three versions of git reset
:
git reset --soft HEAD~
: Moves the current branch pointer to the parent commit but leaves the Working Directory and the Index unchanged.git reset --mixed HEAD~
: Moves the current branch pointer to the parent commit and updates the Index to match the previous commit, but leaves the Working Directory unchanged. This version ofgit reset
allows you to remove files that were accidentally included in a commit. Use this command to reset the last commit, then stage the files you actually want to include, and create a new commit.git reset --hard HEAD~
: Moves the current branch pointer to the parent commit, updates the Index to match the previous commit, and updates the Working Directory to match the previous commit.
Each version progressively changes more of the repository state, from just the
branch pointer with --soft
to the entire Working Directory with --hard
.
You also have the option to reset individual files. When you provide a path to
git reset
, the branch pointer remains unchanged. To reset a file to a specific
version, use the following command:
git reset abc123 -- path/to/file
This command will update the specified file to the version from the commit
abc123
, and the changes will be reflected in the Index.
Git almost never permanently deletes commits. If you perform a hard reset, you can still recover the commits. First note down your last commit hash and then follow these steps:
git log --oneline --all --graph
# * c346fee (HEAD -> main) 07:28:25
# * e703ac0 07:28:22
# * a80f490 07:28:19
git reset --hard a80f490
git log --oneline --all --graph
# * a80f490 (HEAD -> main) 07:28:19
git checkout -b recover c346fee
git log --oneline --all --graph
# * c346fee (HEAD -> recover) 07:28:25
# * e703ac0 07:28:22
# * a80f490 (main) 07:28:19
This ability to reset commits, along with using patch files, provides a way to untangle a messed-up history if ever needed.
Commit to Wrong Branch
Let’s create a commit:
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
git log --oneline --all --graph
# * 93eefd1 (HEAD -> recover) 07:52:52
# * c346fee 07:28:25
# * e703ac0 07:28:22
# * a80f490 (main) 07:28:19
We notice that instead of committing to the main
branch, we made that commit
on the recover
branch. We can use git reset
and git cherry-pick
to make
things right:
Switch to the main
branch and cherry-pick the commit:
git checkout main
git cherry-pick 93eefd1
# Auto-merging t.md
# CONFLICT (content): Merge conflict in t.md
# error: could not apply 93eefd1... 07:52:52
# ...
Resolve the conflict so that t.md
contains the following:
cat t.md
# 07:28:19
# 07:52:52
git add t.md
git cherry-pick --continue
After accepting the commit message, switch back to the recover
branch and
undo the change:
git checkout recover
git reset --hard HEAD~
Verify the history to ensure the commit is now on the main
branch:
git log --oneline --all --graph
# * 2482827 (main) 07:52:52
# | * c346fee (HEAD -> recover) 07:28:25
# | * e703ac0 07:28:22
# |/
# * a80f490 07:28:19
This is one way to recover from committing on the wrong branch. By using
git reset
and git cherry-pick
, you can move the commit to the correct branch
and clean up the original branch.
Edits on Remote
We’ve discussed why it’s a bad idea to have multiple people committing on the same branch. But what if one person commits to the same branch from different repositories? This happens to me often. For example, after pushing my work to GitHub, I sometimes notice a typo, fix it directly on the GitHub website, and commit the change there. Later, when I return to my local repository and make another commit before pulling the typo fix from GitHub, Git will complain that there are changes on the remote that I don’t have locally, and I cannot push to the remote before merging these changes.
In such situations, there’s a version of the git pull
command that can help a
lot. Let’s recreate this situation.
First, let’s create a remote repository:
cd ..
mkdir remote
cd remote
git init -b no-name
Add the remote to our original repository and push to it:
git remote add origin ../remote/.git/
git push origin main
Switch to the remote repository and create a commit on the main branch:
cd ../remote
git config --local user.name 'Mohammad Rahimi'
git config --local user.email 'rahimi.mhmmd@outlook.com'
git log --oneline --all --graph
# * 2482827 (main) 07:52:52
# * a80f490 07:28:19
git checkout main
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
At this point, we have a remote repository with a commit that the local repository doesn’t have. Now, go back to the local repository and create a commit there:
cd ../repo
git checkout main
msg=$(date +%T) && echo $msg >> t.md && git add t.md && git commit -m "$msg"
Fetch the remote status:
git fetch origin
git log --oneline --all --graph
# * 3c2a116 (HEAD -> main) 03:55:41
# | * 3cd7b4b (origin/main) 03:53:14
# |/
# * 2482827 07:52:52
# | * c346fee (recover) 07:28:25
# | * e703ac0 07:28:22
# |/
# * a80f490 07:28:19
Now that we have recreated the scenario, let’s pull in the changes from the remote repository:
git pull origin main
# From ../remote/
# * branch main -> FETCH_HEAD
# hint: You have divergent branches and need to specify how to reconcile them.
# hint: You can do so by running one of the following commands sometime before
# hint: your next pull:
# hint:
# hint: git config pull.rebase false # merge (the default strategy)
# hint: git config pull.rebase true # rebase
# hint: git config pull.ff only # fast-forward only
# hint:
# hint: You can replace "git config" with "git config --global" to set a default
# hint: preference for all repositories. You can also pass --rebase, --no-rebase,
# hint: or --ff-only on the command line to override the configured default per
# hint: invocation.
# fatal: Need to specify how to reconcile divergent branches.
Git is complaining about divergent branches. The solution to this situation is to:
git pull --rebase origin main
After resolving the conflict to the following:
cat t.md
# 07:28:19
# 07:52:52
# 03:53:14
# 03:55:41
We will have:
git log --oneline --all --graph
# * 3a34917 (HEAD -> main) 03:55:41
# * 3cd7b4b (origin/main) 03:53:14
# * 2482827 07:52:52
# | * c346fee (recover) 07:28:25
# | * e703ac0 07:28:22
# |/
# * a80f490 07:28:19
We saw how git pull --rebase origin main
helped us to get the commits made on
the remote repository.
Merge in Rebase
Mistakes can always happen. I’ve often had to fix a colleague’s Git history because they accidentally updated their feature branch by merging instead of rebasing in a rebase workflow. The fix is quite easy. Just note down the head hash and create a patch file before starting, and you will be safe. Here’s the procedure:
- Note down the head hash and create a patch file:
git checkout feat
git log -1 --format="%H"
git format-patch HEAD~
This shows the current head hash and creates a patch file from the last commit.
- Hard reset to
HEAD~
on the feature branch to get rid of the merge commit:
git reset --hard HEAD~
- Optionally apply the patch created from
git format-patch HEAD~
:
git am *.patch
- Perform the rebase by running:
git rebase main
If you skip step 3, you may have to resolve conflicts again.
- Force push to the remote repository:
git push --force origin feat
By following these steps, you can effectively clean up the history and ensure the feature branch is correctly rebased onto the main branch. This process also ensures that you can recover from any mistakes made during the fix.
Erase from History
Another mistake that can happen is accidentally committing a large file or a file that contains sensitive information into Git. Even if you remove the file and commit again, others will still have to download it with each clone, and it will be accessible through the project’s history. To truly remove a file from Git, you need to go over each commit and edit it so that the file is removed.
In the following example, I demonstrate replacing the text 07:28:19
with
redacted
in each commit:
# perform the command in a testing branch
git checkout -b test main
git checkout main
git filter-branch --tree-filter '
if [ -f t.md ]; then
sed -i 's/07:28:19/redacted/g' t.md
fi
' test
git log --patch test
And after executing the steps outlined above, you will see that 07:28:19
has
been successfully replaced with redacted
throughout the file in all commits.
commit bb83f638be36493f8a6c6dd8d8d143fbfc3f91ea (test)
Author: Mohammad Rahimi <rahimi.mhmmd@outlook.com>
Date: Fri May 31 03:55:41 2024 +0800
03:55:41
diff --git a/t.md b/t.md
index 8c6dc00..8441a33 100644
--- a/t.md
+++ b/t.md
@@ -1,3 +1,4 @@
redacted
07:52:52
03:53:14
+03:55:41
commit 2be0694cf8c8f78ec3c7f752cacfaa9963fe2a34
Author: Mohammad Rahimi <rahimi.mhmmd@outlook.com>
Date: Fri May 31 03:53:38 2024 +0800
03:53:14
diff --git a/t.md b/t.md
index e100ac8..8c6dc00 100644
--- a/t.md
+++ b/t.md
@@ -1,2 +1,3 @@
redacted
07:52:52
+03:53:14
commit 5b7e5b494a6af0a44089bbeaca414bd05924fad8
Author: Mohammad Rahimi <rahimi.mhmmd@outlook.com>
Date: Thu May 30 07:52:52 2024 +0800
07:52:52
diff --git a/t.md b/t.md
index 58853f4..e100ac8 100644
--- a/t.md
+++ b/t.md
@@ -1 +1,2 @@
redacted
+07:52:52
commit ff0d4203787c69d55c751fe32f917dd4c7c132d3
Author: Mohammad Rahimi <rahimi.mhmmd@outlook.com>
Date: Thu May 30 07:28:19 2024 +0800
07:28:19
diff --git a/t.md b/t.md
new file mode 100644
index 0000000..58853f4
--- /dev/null
+++ b/t.md
@@ -0,0 +1 @@
+redacted
Summary
In this concluding section, I’ve discussed common mistakes I’ve observed with Git and provided solutions to address each of them. By now, you should have all the necessary tools to work with Git proficiently. Additionally, you should feel confident in your understanding of Git, enabling you to resolve any issues your colleagues may encounter.
If you found this information beneficial, please consider sharing it with others. Your feedback is valuable, so if you have any suggestions for improvement, please feel free to leave a comment. Thank you for your time and attention.